Appendix C for details). The OM (Object Model) is also used for business rules and validation constraint.
For example, consider an order request, where the user enters all the items he or she wants to order. Adding each item, you could check if you have enough items in stock and eventually calculate the shipping date based on availability. Then, you have several business rules to implement:
Check if the item code exists.
If it exists, then check the quantity.
If it is not in stock, then set the shipping date to 10 days from today.
These rules can’t be implemented either by XML Schema or declarative business rules; you have to implement them by yourself. With InfoPath you can write scripts that respond, between others, to node-level events, that is, specific actions that occur within a single field or a whole group (e.g., section or table row). The single action can trigger three kinds of events, in a well-defined sequence:
OnBeforeChange
OnValidate
OnBeforeChange is fired after the DOM has been updated but before it has been accepted and then validated by the XML Schema. If the ReturnStatus is set to false or an exception appears in the script, InfoPath rolls back to the previous value. This event is generally used for data validation or updating status before moving forward.
OnValidate is fired after the changes to the DOM have been accepted. Since it occurs after the XML Schema validation, it is normally used for data validation and error reports.
OnAfterChange is fired after OnValidate. This event is often used for document update and calculations.
Depending on the event type, you can then choose which operation to do. Generally you’ll use OnBeforeChange and OnValidate for data validation and error management, and OnAfterChange for calculations.
The event handlers defined previously can also be used for different purposes. Normally, you implement OnBeforeChange when the control to validate is single and OnValidate when the control to validate is included in a repeating (section or table) control.
Consider a simple document containing a list of items (ItemCode and Quantity). You want to check if the ItemCode exists, and if it exists, you want to verify the quantity available in stock. Then, the quantity that the user will input cannot exceed the quantity available. You can then create your data source with a group named Items and two child elements named ItemCode and Quantity, respectively. Finally, you add a repeating table into the view bounded over the Items group.
To check whether or not the ItemCode is available, you can implement the OnBeforeChange event as shown in DataDOMEvent properties, refer to Appendix C), indicating and handling node changes. Since you need to check whether or not the ItemCode is valid, you can control the current node content (field content) of the DataDOMEvent object, as in the following:
function msoxd_my_ItemCode::OnBeforeChange(eventObj) { if(!ItemCodeExists(eventObj.Site.nodeTypedValue)) eventObj.ReturnStatus = false; } function ItemCodeExists(itemCode) { // Back-end check if(itemCode == "AAA1") return true; else return false; }
Figure 6-8: Script-based event implementation.
The function ItemCodeExists just checks whether or not the code exists. In a real-world scenario, you could check a database, Web service, or in any other back-end system. In this sample we just hard-coded the ItemCode for simplification. eventObj contains a property, Site, which is the XMLDOMNode where the event is currently processed. Then the typed value is passed to the checking function. If the ItemCode isn’t valid or doesn’t exist, you have to notify the user of this constraint violation. ReturnStatus indicates whether or notan error was detected.
Running the previous code, you get two kinds of problems. First of all, the error message is cryptic: “Invalid update: A custom constraint has been violated.” To provide a clearer error message, you can set the ReturnMessage property:
function msoxd_my_ItemCode::OnBeforeChange(eventObj) { if(!ItemCodeExists(eventObj.Site.nodeTypedValue)) { eventObj.ReturnMessage = "The item code is not valid."; eventObj.ReturnStatus = false; } }
When the user inputs a wrong item code, he or she gets the error message you defined and the item code will be deleted from the TextBox.
The second problem is caused by the repeating table control. When the control adds a new row with “Insert items above” or “Insert items below” menu commands, InfoPath creates a clone of the node used in each row with empty (or default) values. Doing that, InfoPath fires all events implemented by the fields. This means that the OnBeforeChange event handler is fired also. Since you are creating a new row with empty values, OnBeforeChange will return false and InfoPath will roll back the operation, avoiding creating a new row.
To deceive InfoPath, you can implement OnValidate instead of OnBeforeChange, because the former doesn’t roll back to the previous value:
function msoxd_my_ItemCode::OnValidate(eventObj) { if(!ItemCodeExists(eventObj.Site.nodeTypedValue)) { eventObj.ReturnMessage = "The item code is not valid."; eventObj.ReturnStatus = false; } }
If you want to provide a detailed error message, you can use the method ReportError instead of ReturnMessage:
function msoxd_my_ItemCode::OnValidate(eventObj) { if(!ItemCodeExists(eventObj.Site.nodeTypedValue)) eventObj.ReportError(eventObj.Site, "The item code is invalid", false); }
ReportError is a method of the DataDOMEvent object and lets you provide a detailed error message to the user. The first parameter is the XML node with which the error is associated, the second parameter is the short error message (shown as a ToolTip), and the third parameter indicates that this error is associated with this specific XML node or all nodes of the same type. ReportError also provides three other parameters (optional): the long description used for dialog box notification, the error code, and the notification type (modal or modeless).
function msoxd_my_ItemCode::OnValidate(eventObj) { if(!ItemCodeExists(eventObj.Site.nodeTypedValue)) eventObj.ReportError( eventObj.Site, "The item code is invalid", false, "The item code is not present in our stock. Please insert a new one.", 100, "modal"); }
You can implement script code not only for data validation but also to apply business calculations. Consider an order form where the user adds the items’ descriptions and prices. You could provide a total box that is updated each time the user changes the price of any items. Since you are working on form data, you must be sure that all information is valid; then you can do it at the last level of the node event chain, OnAfterChange:
function msoxd_my_Price::OnAfterChange(eventObj) { var total = 0.0; var i; var prices = XDocument.DOM.selectNodes("/my:Order/my:Items/my:Price"); for(i = 0; i < prices.length; i++) total += parseFloat(prices.item(i).nodeTypedValue); XDocument.DOM.selectSingleNode("//my:Total").text = total; ( }
The calculation is done over all Price elements of the document. You then have to get all Price items from the XML document. To get them, you select all Price nodes through the DOMDocument.select Nodes method, setting the XPath query. If you have a result (list of nodes), you cycle over it and calculate the total. Since, in XML, all element contents are string-based, you have to convert as float in order to add to the total value. Finally, you fill the node Total with the calculated value. The Total XML element will be reflected automatically in the form field bound to it.
In your script, you can then work over all nodes of the document, creating your business rules as needed. You can also have more complex scripts, such as invoking Web services or COM objects that delegate the calculation to a third-party business logic entity.