Let's take a look at how to extend the capabilities of our Web-based application from Chapter 13 so that it allows the user to edit and review the XML data. If we allow the user to change the values of the XML data, the new values will need to be validated. If the data is validated on the server, this validation can be done as each field is typed in or after all the data has been input. Performing server-side validation as data is input often creates time delays on the client as each field is validated. On the other hand, sending data to the server when all the information is input makes it difficult for the user to find the exact fields that were incorrect. The best solution is to create business services components that run on the client that can validate the data after it has been entered.
NOTE
If you validate data on the client, you would still have to validate the data on the server a second time for security reasons, as a malicious hacker could submit invalid data.
HTC enables Web developers to implement behaviors that expose properties, methods, and custom events. As you will see in the example later in this chapter, properties placed inside an HTC will allow us to perform validation whenever the value of a property is changed.
Using an HTC for our validation code offers us several advantages. To begin with, the code we will use to validate a particular XML document could be reused in every Web application that uses XML based on the same schema or DTD. Also, the HTC will be located on the client, so this validation will not require any trips to the server. Finally, if the validation rules change, only the HTC needs to be changed, not the HTML application that uses the HTC.
Before we create our example, let's look at the different elements that can be used in an HTC document. These elements are listed in the following table:
HTC Elements
Element | Description |
---|---|
COMPONENT | Defines the document as being an HTC document; is the root element of an HTC document (HTC can also be used.) |
ATTACH | Attaches an event in the source document to a function in the component |
METHOD | Defines a method that can be called by the source document |
EVENT | Defines an event that can be raised by the HTC |
PROPERTY | Defines a property that can be accessed by the source document |
GET | Defines the function that will be called when retrieving the value of a property |
PARAMETER | Defines a parameter for a function |
PUT | Defines the function that will be called when setting the value of a property |
The example we will create saves the values of each of the fields in properties in an HTC. The put functions associated with the properties in the HTC will validate each new value. Thus, we will use the HTC to validate any changes to a field. If the change is invalid, we will reset the value of the field back to its original value by using the value that is currently in the property. We'll prefix the elements with the public namespace to identify the properties in the HTC as public properties. Public properties can be get or put by external documents. Let's begin by creating a document called ValidatePO.htc that includes the following code:
<public:COMPONENT> <public:PROPERTY NAME="PartNo" PUT="putPartNo" GET="getPartNo"/> <public:PROPERTY NAME="Quantity" PUT="putQuantity" GET="getQuantity"/> <public:PROPERTY NAME="UOM" PUT="putUOM" GET="getUOM"/> <public:PROPERTY NAME="UnitPrice" PUT="putUnitPrice" GET="getUnitPrice"/> <public:PROPERTY NAME="Discount" PUT="putDiscount" GET="getDiscount"/> <public:PROPERTY NAME="Total" PUT="putTotal" GET="getTotal"/> <public:PROPERTY NAME="Error" PUT="putError" GET="getError"/> <script language="VBScript"> Dim strPartNo Dim strQuantity Dim strUOM Dim strDiscount Dim strTotal Dim strUnitPrice Dim strError Function putPartNo (newValue) If newValue = "" Then strError = "The part number is invalid." Else strPartNo = newValue End If End function Function putQuantity (newValue) If newValue = "" Then strError = "The quantity is invalid." Else If IsNumeric(newValue ) Then strQuantity = newValue Else strError = "The quantity must be numeric." End If End If End function Function putUnitPrice (newValue) If newValue = "" Then strError = "The unit price is invalid." Else If IsNumeric(newValue) Then strUnitPrice = newValue Else strError = "The unit price must be numeric." End If End If End function Function putUOM (newValue) If newValue = "" Then strError = "The UOM is invalid." Else If (LCase(newValue) = "each") or _ (LCase(newValue) = "case") Then strUOM = newValue Else strError = "The UOM can only be each or case." End If End If End function Function putDiscount (newValue) If newValue = "" Then strError = "The discount is invalid." Else If IsNumeric (newValue) Then strDiscount = newValue Else strError = "The discount must be numeric." End If End If End function Function putTotal (newValue) If newValue = "" Then strError = "The total is invalid." Else If IsNumeric (newValue) Then strTotal = newValue Else StrError = "The total must be numeric." End If End If End function Function putError (newValue) StrError = newValue End function Function getPartNo () getPartNo = strPartNo End function Function getQuantity () getQuantity = strQuantity End function Function getUOM () getUOM = strUOM End function Function getDiscount () getDiscount = strDiscount End function Function getTotal () getTotal = strTotal End function Function getUnitPrice () getUnitPrice = strUnitPrice End function Function getError () getError = strError End function </script> </public:COMPONENT>
As you can see, public properties are used in the document. The advantage of using properties is they allow us to validate values when a user attempts to change the value of the property. If the value is invalid, we do not allow the property's value to change and raise an error. In the case of the HTC above, we do not actually raise an error but instead set the error property to a string that explains the error.
The put functions in this code are used to check that the new values are valid. If they are not valid, the functions set strError to a value that explains the error. If the value is valid, the functions set a private variable to the value. Each put function is associated with the PUT attribute of the corresponding property. For example, the putPartNo function is associated with the PUT attribute of the PartNo property. When you assign a value to the PartNo property, the function putPartNo is called.
The get functions will be used when a certain value is set equal to the property. For example, when you use the partNo property, the getPartNo function will be called. PartNo will return the private variable. Although we do not perform any validation in the get functions, you can add validation to these functions, too.
In the source document that will use the HTC, we will not use data binding for our text boxes. Instead, we will use DHTML to give us full control of the form as we did in the example in >Chapter 13. We will write script to fill the text boxes with user input, to move through the records, and to set and get the properties in the HTC document. Here is the first part of the HTML document, showing the FillText function and the ondatasetcomplete event:
<html> <head> <xml src="NorthwindPO2.xml" id="NorthwindDSO"></xml> <style type="text/css"> .FieldName {font-family:Arial,sans-serif; font-size:12px; font-weight:normal} .POValidate {behavior:url (ValidatePO.htc);} </style> <script language="JScript"> function FillText() { txtPartNo.value=NorthwindDSO.recordset.fields("partno").value; txtQuantity.value=NorthwindDSO.recordset.fields("qty").value; txtUOM.value=NorthwindDSO.recordset.fields("uom").value; txtDiscount.value=NorthwindDSO.recordset.fields ("discount").value; txtUnitPrice.value=NorthwindDSO.recordset.fields ("unitPrice").value; txtTotal.value=NorthwindDSO.recordset.fields ("totalAmount").value; } function NorthwindDSO.ondatasetcomplete() { htcSpan.PartNo=NorthwindDSO.recordset.fields("partno").value; if (!htcSpan.Error=="") { alert(htcSpan.Error); } htcSpan.Quantity=NorthwindDSO.recordset.fields("qty").value; if (!htcSpan.Error=="") { alert(htcSpan.Error); } htcSpan.UOM=NorthwindDSO.recordset.fields("uom").value; if (!htcSpan.Error=="") { alert(htcSpan.Error); } htcSpan.UnitPrice=NorthwindDSO.recordset.fields ("unitPrice").value; if (!htcSpan.Error=="") { alert(htcSpan.Error); } htcSpan.Discount=NorthwindDSO.recordset.fields ("discount").value; if (!htcSpan.Error=="") { alert(htcSpan.Error); } htcSpan.Total=NorthwindDSO.recordset.fields ("totalAmount").value; if (!htcSpan.Error=="") { alert(htcSpan.Error); } htcSpan.Error=""; FillText(); }
The FillText function is used to place the values of the recordset into the textboxes. It is called when the data has been loaded and when the user moves to another row. The DSO object raises the ondatasetcomplete event when all the data has arrived on the client. When this event is raised, we first set all the properties equal to the field values. Because we are setting the properties, the put functions in the HTC are used. As mentioned previously, the put functions also validate the new values. If there is an error, the property isn't set to the new value and the user gets an error message. This code doesn't fix the error, it only alerts the user to the problem.
To use the HTC document, we need to bind it to a tag on the form. We use the span tag called htcSpan to bind the HTC document. The htcSpan is bound by setting its class attribute to POValidate, the style linked to the HTC document. Once the span tag is bound to the HTC document, it inherits the properties and methods contained in the HTC document. Thus, the properties can be referenced by using the name for the tag followed by a dot followed by the name of the property. For example, htcSpan.PartNo refers to the PartNo property in the document.
In the second part of the HTML document, we'll use functions that will be associated with the onBlur event of all the text boxes. When the user tabs out of a text box, the onBlur event will be called. If the value in the text box is different from the value of the field in the recordset, the user has changed the value. If the value has been changed, we will set the field to this new value. Changing the value of the field will result in the oncellchange event being raised by the DSO object. We will validate the change in the oncellchange event.
function partNoChange() { if(NorthwindDSO.recordset.fields("partno").value!= txtPartNo.value) { NorthwindDSO.recordset.fields("partno").value= txtPartNo.value; } } function QuantityChange() { if (NorthwindDSO.recordset.fields("qty").value!= txtQuantity.value) { NorthwindDSO.recordset.fields("qty").value= txtQuantity.value; } } function UOMChange() { if (NorthwindDSO.recordset.fields("uom").value!=txtUOM.value) { NorthwindDSO.recordset.fields("uom").value=txtUOM.value; } } function UnitPriceChange() { if (NorthwindDSO.recordset.fields("unitPrice").value!= txtUnitPrice.value) { NorthwindDSO.recordset.fields("unitPrice").value= txtUnitPrice.value; } } function DiscountChange() { if (NorthwindDSO.recordset.fields("discount").value!= txtDiscount.value) { NorthwindDSO.recordset.fields("discount").value= txtDiscount.value; } } function TotalChange() { if (NorthwindDSO.recordset.fields("totalAmount").value!= txtTotal.value) { NorthwindDSO.recordset.fields("totalAmount").value= txtTotal.value; } }
The third part of the HTML document shows how to use the oncellchange event that is fired whenever a field is changed. We will use the dataFld property of the event object to get the name of the field that has changed. Using a switch statement, we will find which field has been changed and set the property for that field equal to the new value. If the change is valid, the property will be set to the new value. If the value is not valid, the property will not be changed and the error property will be set to a string describing the error. If the change is invalid, we will also reset the value in the text box to the original value that is still stored in the property and then use the select method of the text box to move focus back to the field that was in error.
function NorthwindDSO.oncellchange() { switch (event.dataFld) { case "partno": htcSpan.PartNo=txtPartNo.value; if (!htcSpan.Error=="") { txtPartNo.value=htcSpan.PartNo; txtPartNo.select(); } break; case "qty": htcSpan.Quantity=txtQuantity.value; if (!htcSpan.Error=="") { txtQuantity.value=htcSpan.Quantity; txtQuantity.select(); } break; case "uom": htcSpan.UOM=txtUOM.value; if (!htcSpan.Error=="") { txtUOM.value=htcSpan.UOM; txtUOM.select(); } break; case "unitPrice": htcSpan.UnitPrice=txtUnitPrice.value; if (!htcSpan.Error=="") { txtUnitPrice.value=htcSpan.UnitPrice; txtUnitPrice.select(); } break; case "discount": htcSpan.Discount=txtDiscount.value; if (!htcSpan.Error=="") { txtDiscount.value=htcSpan.Discount; txtDiscount.select(); } break; case "totalAmount": htcSpan.Total=txtTotal.value; if (!htcSpan.Error=="") { txtTotal.value=htcSpan.Total; txtTotal.select(); } break; default: htcSpan.Error = "Invalid element text"; } if (!htcSpan.Error=="") { alert (htcSpan.Error); htcSpan.Error=""; } }
The last part of the HTML document shows the move functions and the rest of the HTML code. We will code the move functions using JScript instead of using VBScript as we did in Chapter 13 so that we can see how to code in both scripting languages. First the DSO object is moved, and then the FillText function is called to set the values in the text boxes.
function MoveNext() { NorthwindDSO.recordset.moveNext(); if (NorthwindDSO.recordset.eof) { NorthwindDSO.recordset.moveFirst(); } FillText(); } function MovePrevious() { NorthwindDSO.recordset.movePrevious(); if (NorthwindDSO.recordset.bof) { NorthwindDSO.recordset.MoveLast(); } FillText(); } function MoveLast() { if (!NorthwindDSO.recordset.eof && !NorthwindDSO.recordset.bof) { NorthwindDSO.recordset.moveLast(); FillText(); } } function MoveFirst() { if (!NorthwindDSO.recordset.eof && !NorthwindDSO.recordset.bof) { NorthwindDSO.recordset.moveFirst(); FillText(); } } </script> </head> <body> <!--This is the span element that gives the reference to the HTC component.--> <span id="htcSpan" class="POValidate"> </span> <!--We place the values in a table to make them look neater.--> <table cellpadding="5"> <tr> <td> <div class="FieldName">Part No</div> </td> <td> <!--The onBlur event is associated with the partNoChange listed above.--> <div class="FieldName"><input type="text" id="txtPartNo" onBlur="partNoChange()"></div> </td> </tr> <tr> <td> <div class="FieldName">Quantity</div> </td> <td> <div class="FieldName"><input id=txtQuantity name=txtQuantity onBlur="QuantityChange()"></div> </td> </tr> <tr> <td> <div class="FieldName">UOM</div> </td> <td> <div class="FieldName"><input id=txtUOM name=txtUOM onBlur="UOMChange()"></div> </td> </tr> <tr> <td> <div class="FieldName">Unit Price</div> </td> <td> <div class="FieldName"><input id=txtUnitPrice name=txtUnitPrice onBlur="UnitPriceChange()"></div> </td> </tr> <tr> <td> <div class="FieldName">Discount</div> </td> <td> <div class="FieldName"><input id=txtDiscount name=txtDiscount onBlur="DiscountChange()"></div> </td> </tr> <tr> <td> <div class="FieldName">Total</div> </td> <td> <div class="FieldName"><input id=txtTotal name=txtTotal onBlur="TotalChange()"></div> </td> </tr> </table> <!--The buttons are also placed in a table so that they appear neater in the document.--> <table> <td><input id=cmdMoveNext name=cmdMoveNext type=button value="Move Next" onClick="MoveNext()"></input></td> <td><input id=cmdMovePrevious name=cmdMovePrevious type=button value="Move Previous" onClick="MovePrevious()"></input></td> <td><input id=cmdMoveFirst name=cmdMoveFirst type=button value="Move First" onClick="MoveFirst()"></input></td> <td><input id=cmdMoveLast name=cmdMoveNext type=button value="Move Last" onClick="MoveLast()"></input></td> </table> </body> </html>