| ince its inception, XML has been pitched as the premiere tool for exchanging data between disparate systems, and for storing data for dynamic presentation. However, XML has countless other uses. In this article I will illustrate how you can use XML as a structure for data collection on a series of wizard-like Web Forms. This code is based on Beta 1 of the Microsoft® .NET Framework and some of the code described here will need to be updated to work with Beta 2.|
To illustrate this process, I am going to use a fictional Italian restaurant called PizzaNow, which has a delivery service and a Web site for placing orders. Placing an order involves making menu choices from a series of Web Form pages, reviewing the order summary, and submitting the final order. The main menu with an empty order is shown in Figure 1.
Figure 1 Main Menu
Clicking on any of the four food categories (appetizers, pizzas, drinks, or desserts) brings up a selection form, or a series of forms in the case of pizzas, from which selections can be made. An example of this is shown in the appetizers form in Figure 2. The complete site diagram for the form pages is illustrated in Figure 3.
Figure 2 Appetizers
As the user makes selections from the menu forms, his choices are stored behind the scenes in an XML document. Upon final submission, this XML document will contain all the information required to prepare and fulfill the order. In this article I'll review the code that gathers the data through interaction with the .NET System.Net object model and the ASP .NET object model.
Figure 3 The Pages
To complete the example, I will discuss the interaction with the System.Xml object model and the encapsulation of its functionality in a wrapper component. I will also cover the persisting of the XML document between page requests, gathering data into an XML document and retrieving it later, and submitting the XML document for further processing (order fulfillment).
Creating an XML Wrapper The complexity of the System.Xml model requires more code than I would like to see scattered throughout my ASPX documents, so I wrote a wrapper component in C# called XMLWrapper.cs to hide the details of interacting with this model. The wrapper exposes only the higher-level functionality I need for this application. This also allows for any fine-tuning of the component to fit the needs of this application. The wrapper class functionality is shown in Figure 4.
As you can see, most of the functionality in this wrapper is designed to load and save XML, then manipulate it at a high level using an XPath expression, which I'll describe shortly. Internally, the wrapper object creates one global instance of an XmlDocument object in which it stores the XML text.
This XmlDocument is then manipulated mostly through the use of the System.Xml.DocumentNavigator object. Note that this class will be replaced with XPathNavigator in Beta 2.
XmlDocument _xmlDoc = new XmlDocument();
My XMLWrapper class is built to trap all errors and return success or failure as a Boolean from each function. I made this decision so that minimal error handling would be required by the calling application (in this case, ASP .NET). Only in cases where you care if a function succeeds or fails do you need to determine the cause of the error. For those cases, I use another class-global string called _Error, which stores any text generated by an internal exception. By querying this through the ErrorText function, I can read and react to the actual error case. (For space considerations, the examples in this article have been stripped of their try/catch error-handling code. The downloadable code, which can be found at the link at the top of this article, does contain the full error-handling functionality.)
The bulk of this article will cover the key functions of this wrapper class, how they work, and how they are used within this application. But first, you need to understand the DocumentNavigator and XPath.
Using DocumentNavigator and XPath Those familiar with the Microsoft XMLDOM and XSL patterns may want to skip this section. However, I found enough differences between how XML is handled in the Microsoft .NET Framework object model and how it was handled in the XMLDOM to justify this discussion.
The XMLDOM utilized a document object to provide a majority of the required access to an XML document, supporting searches through XSL patterns as an extension to the W3C standard. Although the .NET Framework has an XmlDocument object, it does not support such advanced manipulation of data; instead, it provides functionality to load, save, and set properties of the document itself. This will change, however, in Beta 2.
There are several options for manipulating the contents of an XmlDocument, one of which is a new object called the DocumentNavigator. The MSDN® documentation states that this class reads and writes the XML data in an XmlDocument using a cursor model. Although this is true, I think this description trivializes the real power and flexibility of this object.
In a manner similar to the way ADO supports a cursor to navigate through a recordset with the MoveNext, MovePrevious, MoveFirst, and MoveLast methods, the DocumentNavigator utilizes a cursor engine to navigate an XML document structure. However, since an XML document is a tree of data rather than a table, the methods in the DocumentNavigator object support navigating between child and parent nodes. Examples of these methods are MoveToFirst, MoveToLast, MoveToNext, MoveToPrevious, MoveToParent, MoveToFirstChild, and so on. As you can imagine, stepping up and down through an XML document by using a looping construct (in C# or Visual Basic®) and a single object is an elegant solution. Contrast this with the previous XMLDOM method of retrieving a series of separate nodes and a series of objects for each. The benefits are obvious.
Aside from this new navigational power, the DocumentNavigator supports the use of XPath expressions through the use of its SelectSingle method, which it inherits from the XmlNavigator abstract class. For those familiar with the XMLDOM object model, XPath expressions are the rough equivalent of XSL patterns. For those unfamiliar with it, XPath expressions are text strings that describe the path through an XML node tree to a particular element (node or attribute). This is best described with an example.
If I load the XML document shown in Figure 5, then the following XPath expression, which gets the title of the book whose ID attribute is 345
will return the result:
Basically, XPath expressions provide a one-shot mechanism to dig into an XML document and fetch a piece of information, without the need to navigate through a series of nodes in the tree. As a side benefit of the DocumentNavigtor's implementation of this, once it finds a node, it places the cursor at that location. Therefore, an XPath expression can be used as the starting point for a series of looping fetches. As you will see later, this is used a couple of times in my PizzaNow example application, and XPath expressions, in general, are used everywhere.
"XML in the .NET Framework"
A Template File for Orders Although it is possible to create an XML document strictly through code (using the System.Net.XmlWriter object), it is generally faster to load an XML file template that contains the basic structure of the final document. Almost every XML document structure has a defined set of required tags, and it's easier to just build this as a static XML file rather than building it through countless function calls to the XML object model. Also, if additional elements need to be added down the road, all that has to be changed is the template document, and this does not require a code recompilation.
In the case of the order XML document used in this application, every order contains appetizers, pizzas, drinks, and desserts nodes, so the placeholder tags for these items were put in a template document that gets loaded initially by the main menu page. The complete template document looks like this:
The wrapper object has a function for loading an XML document from disk into its internal XmlDocument object. It uses another System.Xml object, called an XmlTextReader, that is one of the tools that manages a stream of XML data, in this case from a file. The function looks like this:
Calling this function from ASP .NET requires just the name (and location) of the XML document. The call looks like this:
//Load XML Document from text .xml file
public void LoadXMLFile(String sURL)
XmlTextReader xmlTR = null;
//Load XML file into reader
xmlTR = new XmlTextReader(sURL);
//Load XML into Document (from reader)
Once the function executes, the XMLWrapper's internal _xmlDoc object will contain the loaded XML data waiting to be manipulated.
//Load "order template" XML from file
Setting up Persistence Loading a template XML document works well for the initial application setup, but all subsequent requests for data need to come from some form of persistent storage. Likewise, since the Web is a stateless environment, you need to save the XML document into the same persistent storage between page requests. Although the ASP .NET model has several new methods of persisting data on the server side that don't have the same drawbacks as the familiar ASP Session object, I have opted to use a client-side cookie for simplicity.
It is fortunate that the XML document objects can dump their contents into a simple string since a cookie can store only simple text strings. With a little bit of massaging (removing line feeds, tabs, and so forth), this string can be stored cleanly in a cookie. The wrapper functions in Figure 6 illustrate how to extract the XML document as a string, and how to load the string back into the XML document. The ASP .NET code snippets in Figure 7 show how to create the cookie, retrieve the value of a cookie on an ASPX page, and store it back once changes have been made.
The code in Figure 7 appears on each of the pages that need to interact with the order XML document. In the Page_Load handler, a page-scoped XMLWrapper object is populated from the cookie value. Whenever the page is exited, typically by clicking on a button, the code that extracts the text value of the order in the XMLWrapper object is executed, and this gets stored in the cookie.
Collecting Data As the user navigates the various Web Forms in the application's menu wizard, the user's selections are stored in the XML order document via the XMLWrapper methods. There are three basic single-page forms that work in an identical manner—one each for appetizers, drinks, and desserts. The Appetizers Web Form was already shown in Figure 2, and drinks and desserts pages are configured similarly.
Each of these forms uses one of the new ASP .NET CheckBoxList controls to display the list using the following ASPX syntax:
When the user clicks Select after making the selections, the ASP .NET code creates an XMLWrapper object, populates it with the current order, and updates the order with the following code (in the case of desserts):
<asp:CheckBoxList id=lstDessertItems runat="server">
The code in Figure 8, in turn, uses the UpdateNode method of the wrapper object to submit the selections to the XML order document. As you can see, the UpdateNode function accepts an XPath expression parameter for the location of the parent node, the name of the actual node to add or remove, and what to do with the node (add it or remove it) as a Boolean. When adding a node, this function invokes the VerifyNode function to see if the node already exists.
//Add or remove items in order
In VerifyNode, a new DocumentNavigator object is created and given the global XmlDocument object as a target. The SelectSingle method is then called with the XPath expression of the node in question. This returns true or false, depending on whether the node was found.
public bool VerifyNode(string sNodePattern)
//Load XML document into navigator
DocumentNavigator docNav = new DocumentNavigator(_xmlDoc);
//Select the node matching the XPath (return if it's found)
In the UpdateNode function, if the node exists, it does nothing (no changes). However, if it does not exist, it calls the AddNode function to create the node (see Figure 9). AddNode also utilizes a DocumentNavigator object to select the parent of the new node (via the XPath expression). Assuming it is found, it uses the Insert method to add a child node with the provided name and sets a flag to return true. If the parent node was not found, the global error text is set and the function returns false.
In the case where you want to remove the node during the UpdateNode function, the DeleteNode function is invoked to perform the delete (see Figure 10). The DeleteNode function works in a manner similar to AddNode. The only difference is that once the node has been selected with its XPath expression, it is removed with the DocumentNavigator Remove method, again returning success or failure.
In review, the user makes selections on a particular Web Form and clicks Select. This fires off a bunch of calls to UpdateNode with the parent node XPath expression, the node name, and the state of the appropriate checkbox. The UpdateNode then calls VerifyNode and AddNode or DeleteNode to create or remove the appropriate nodes from the order XML document.
After the forms for the pizza, drinks, and desserts are submitted, the order XML document will look like the document in Figure 11, reflecting the choices the user made.
Repopulating Forms Since this application supports the ability to return to each form and make changes to the previous selections, it needs a way to repopulate the forms with the stored values. This is the opposite of the data collection process. When a form is displayed, the order XML document is loaded, the appropriate values are read, and the form controls are set to these values.
The page event model of ASP .NET provides a Page_Load handler that is called whenever the page is about to be sent to the browser. This handler is generally a good place to create and set any objects that have page-level scope. There is also an IsPostBack property that allows you to determine if this is the first time the page has been loaded (as opposed to being displayed again via a refresh or self-submittal).
Each one of the three single-form pages just described uses the Page_Load handler in an identical manner. Code within the handler populates the form ListBoxControl with values from the XML order document. The initial startup code for the desserts.aspx page (contained in the desserts.cs file) that accomplishes this is shown in Figure 12. As you can see, all that is required is a simple call to VerifyNode to determine if each of the item nodes is in the order. Since the VerifyNode method returns true or false, this value can be provided directly to the selected property of each item in the list, which results in either checking or unchecking them.
That's about all there is to it. The XMLWrapper object hides most of the work, and this reduces the code on the ASP .NET pages to just a few lines. The reduction in code complexity makes it very clean to understand and maintain.
This is all fine for the application's single-page forms, but what happens when a form is divided across several pages to display and collect all the information? The answer to this lies in creating form groups, as detailed in the next section.
Form Groups In the sample application, creating a pizza requires a bit more work than just selecting from a list: there are sizes, types of sauces and crusts, and numerous toppings. Since the application is designed to work like a wizard, these elements displayed together result in a page that's longer than desired (scrolling is bad). Therefore, the data elements have been grouped logically and divided into three separate forms: Size, Styles, and Toppings.
The problem with this is that you need some mechanism to track which pizza is being modified as the user moves forward and back between the form pages. In addition, there must be a way to edit an already created pizza, so the mechanism will also be used to identify a preexisting pizza for modification.
When you reexamine the template order XML document presented in Figure 11, you may notice the CURRENT tag which looks like it doesn't quite belong. This tag exists to support the functionality just described. It temporarily holds a unique identifier that is used to track which pizza is currently being worked on. Each pizza tag receives an ID attribute that is used to uniquely identify them. Incorporating this value in the XPath expressions allows the selections that are made by the user to be stored with the appropriate pizza (this process will become clear as I walk through it).
When creating a new pizza, however, there is no ID value yet assigned, so you need to generate one. This is performed by the StartPizza.aspx page, which doesn't actually display anything, but rather sets up the environment in preparation for the multi-form sequence that will follow. The function that generates a new ID does so randomly (see Figure 13). The code generates an ID number from 0 to 999, which is very unlikely to be repeated in a single order. But just in case, the code checks for a pizza with that ID, and if it exists, it loops until a unique ID is generated.
The complete setup code for a new pizza then falls into five lines of code, as shown in the BeginNewPizza function in Figure 14. This creates a new pizza node, fetches a new ID, assigns the ID to the pizza by creating the ID attribute, adds a TOPPINGS child node, and sets the value in the CURRENT tag to the new ID value. Bear in mind that this last step is critical, since it tells the system which pizza will be worked on for the remainder of the form pages—in this case, the one just created.
At this point the setup work is complete, so PizzaStart.aspx simply redirects to the first page in the pizza form sequence, PizzaSize.aspx. This page uses the ASP .NET RadioButtonList control, which works in a fashion similar to the CheckBoxList control used on the previous pages. Once the user makes a selection from among the three options and clicks Next, the application moves on to the PizzaStyle.aspx form.
However, just before that happens, the selections need to be stored in the pizza tag structure. This ASP .NET code is very similar to that used before, except that it takes into account the value in the CURRENT tag:
First, the ID value of the current pizza is retrieved, then that value is integrated into the XPath expression passed to the SetAddValue XMLWrapper method. This method takes the name of the child tag as well as its value (see Figure 15).
//Get name of current pizza
sCurrentPizza = xmlLib.GetValue("//ORDER/PIZZAS/CURRENT");
//Add value for pizza size to current pizza
xmlLib.SetAddValue("//ORDER/PIZZAS/PIZZA[@id=" + sCurrentPizza +
"]", "SIZE", lstPizzaSize.SelectedItem.Value);
The SetAddValue function should be fairly straightforward since, with the exception of SetValue, it uses functions already described. It first attempts to set the value of the node. If the node does not yet exist, the SetValue function will return false. If that happens, then the code first adds the node, then once again sets its value.
The code for SetValue is shown in Figure 16. This function uses a DocumentNavigator object to find the matching node, and if found, sets its value through the InnerText property. If it's not found, it returns false (and sets the _sError string to describe why).
The remaining two forms in the pizza sequence work in a similar fashion, but use a different set of controls to provide variety. The second form, PizzaStyle.aspx, uses two DropDownList controls, and the PizzaToppings.aspx form once again uses a CheckBoxList control (with two columns). I won't go into detail describing how these last two pages work since, except for minor differences in dealing with the particulars of each control, they work in a manner identical to the first. The exception is the last page, which has a Finish button.
The code executed by the Finish button cleans up the environment because it is finished creating (or editing) this pizza. Aside from storing the form values in the order XML document, the only additional task is to clear the CURRENT tag, which is performed with a single line of code:
Editing in Form Groups The process outlined in the previous section was to create a new item via a sequence of grouped forms. In many cases you might want to edit an existing item rather than create a new one. Of course, you'd want to use the same forms to do so. The elegance of this design is that it allows you to do just that without any code changes.
There are only two minor differences between editing and creating, and they are reflected in the setup process. First, the code that creates a new pizza node is skipped. Second, instead of generating an ID value for a new pizza, the value of the one to work with is passed in and stuck into the CURRENT tag. As before, this is performed in StartPizza.aspx and looks like this:
As you can see, the ID value of the pizza you want to edit is simply passed in via the page Request.QueryString property. The remainder of the code—moving between pages, selecting and saving controls—works without modification.
//Set current pizza to passed ID value
Removing a Pizza The first three single-form pages (appetizers, drinks, and desserts) allow for the addition and removal of items simply by updating the selections on the same page. In form groups, another means of removal is needed.
Another ASPX page was created with the sole purpose of removing pizzas from the order. Like the PizzaStart.aspx page, the PizzaRemove.aspx page does not display anything; it simply removes the appropriate pizza and returns control to the main menu (which shows the updated order).
Like the edit functionality, the ID of the pizza you want to remove is passed in via the Request.QueryString property. The code to remove the pizza then consists of:
This simply uses the DeleteNode method of the XMLWrapper, described previously, to remove the appropriate pizza and all child nodes from the order XML document. Again, the design of the system makes this an almost trivial task.
Request.QueryString["ID"].ToString() + "]");
Displaying the Formatted Order At this point, the main functionality of collecting the data into an XML document has been completely described. However, it would be nice to provide a way for the user to see what she has chosen before committing the order. I have created a rudimentary order listing at the bottom of the main menu screen (see Figure 17). This is actually a separate page (Order.aspx) that gets included on the menu page.
Figure 17 Order.aspx
The purpose of this order page is to display the choices the user has selected, and provide a Place Order button. Additionally, the Remove and Edit links appear next to each pizza ordered.
The code to display these items differs slightly from what you have seen before because instead of examining each node on a per-item basis, you need to retrieve a list of all nodes under a particular parent. On past projects I have created functions that returned an XML node structure, but this breaks the encapsulation of the XMLWrapper by requiring the ASP(X) page to include XML manipulation code. In this example, I have opted instead to return a delimited string item list, which is a simpler structure for the ASPX page to work with. The wrapper method to retrieve a node list looks like Figure 18.
The GetNodeList function uses the DocumentNavigator Select method, which differs from the SelectSingle method used previously in one very key manner: it does not place the cursor on a valid selection. To do so requires executing the MoveToNextSelected method. The reason for this is that it allows you to build an elegant while loop (see Figure 18) without having to treat the first item any differently than the rest.
The while loop concatenates each node name to a string along with a semicolon. Once that is complete, the GetNodeList function examines the resulting string to see if something was added, then removes the trailing semicolon (since you only want semicolons between items). This resulting semicolon-delimited string is then returned.
This function in Figure 19 is invoked on the Orders.aspx page by calling the DisplayItems function for each of the appetizers, drinks, and desserts groups, passing in the appropriate asp:label control in which to place the generated HTML, as well as the XPath expression of the node group to get.
One thing to notice here is that the XPath expression has become a bit more complex than what has been utilized previously. In this case, you want all the child nodes of the group node provided, so you end the XPath string expression with "child::*" which uses a wildcard to say "give me every child".
Since the result of the GetNodeList method is a delimited string, I use the string Split function to break it down into an array. Then I iterate through the array, building an HTML string with each item. This HTML text is then placed in the label; thus it shows up on the order.
The pizza items work in a similar fashion, but require a more complex sequence of retrievals to get all the different node values, as well as incorporating the ID values to work with each pizza individually. I won't show all the code to do this here, since it is more of the same stuff, but you can see the GetAttributeList function that is used to get a list of all pizza IDs in Figure 20.
This works like the GetNodeList method, except that for each node, it moves to the named attribute and concatenates its value to the returned string. The XPath expression also differs slightly and looks like this:
Instead of getting all children with the wildcard character, this gets all children of PIZZA nodes.
At this point in the process, the complete order has been displayed for the user. Changes to an order can be made by a combination of navigating back to the main menu categories, or by clicking the actions next to each pizza.
Order Submission The last, and most important piece of business in any online commerce site is to make the sale. Since my goals for this article have already been accomplished, what is done with the resulting XML order will be left as an exercise for the reader.
In this case, possible actions include printing the order in the kitchen, sending it to a fulfillment house (which may or may not be the same company), transmitting it to an accounting system using Web Services, and so forth. The possibilities are really endless—another benefit of using XML as the data structure.
In the case of this example, I implemented a little piece of code that saves the XML contents to disk and then redirects the browser to that page in order to display the resulting data. This is merely a way to verify that the collected data exists and is accurate; it has no use in a real-world application. For the order selections shown in Figure 17, the generated XML looks like Figure 21.
Tying it Together To recap, the PizzaNow application consists of a series of ASP .NET Web Forms that present menu selections for an Italian restaurant delivery service. The selections made on these forms are stored in an XML data structure, which is manipulated through the use of an XMLWrapper object that uses the System.Xml object model. During the entire application, the XML data is persisted as plain text in a cookie until the order is submitted and the data is sent to some other system.
Of course, this implementation of an order system is fairly rudimentary, and does not support such things as quantity, price, and totals. However, with the concepts presented so far, enhancing the app to include these extra features should be fairly straightforward.
Alternatives My restaurant menu ordering application doesn't require long-term storage of customer information. Still. you may be wondering what alternatives there are to using XML. Well, other options include storing the data in a database, using some sort of key-value pair storage, and writing a custom data structure with custom parsing code.
Databases Although very efficient, this solution has several problems. First, complex data structures that utilize hierarchies require several related tables with complex insert and select queries. Second, determining which orders are invalid (were discontinued by the user closing the browser) is difficult, and finally, the burden of space requirements is placed on the server.
Key-value pairs There are several canned key-value objects at an ASP programmer's disposal including the Dictionary object and cookies themselves. The main problem with these is that they support no hierarchy of data, thus getting them to hold complex structures requires some methodology that needs custom code to interpret it.
Custom data structures These are proprietary data structures that a programmer develops to support the particular needs of an app. The problem with this solution, besides the time it takes to code it, is that the code is usually too specific to be reused and is difficult for others to understand and maintain.
XML offers many advantages. For example, a complete transaction can be contained within one simple string, regardless of how complex the hierarchy becomes. XML data is easy to send to another organization, as long as both parties agree on a structural format. XML data is more flexible than a relational database. It allows one-to-many relationships without a complex table structure, plus it allows similar items (records) to have different sets of fields (nodes). And finally, XML has strong prebuilt, industry-supported parsers. Both the Microsoft XMLDOM and the .NET Framework System.Xml object models support advanced manipulation that reduces the amount of work developers have to do.
Of course, there are some disadvantages. First, XML is not very good at storing huge amounts of data. Because the storage medium is really just text, loading large XML structures is not terribly efficient, and searching them can be time consuming. Analysis is difficult in XML. Where a simple SQL query can return gobs of information about a set of data, XML has no such power. The closest you can come is to search for common elements and count them or perform some sort of custom aggregation. Some of the best solutions harness the power of large backend databases, such as SQL Server™, and use XML for presentation and cross-platform compatibility.