XML Wrapper Template: Transform XML Documents i...

XML Wrapper Template: Transform XML Documents into Visual Basic Classes

Dave Grundgeiger and Patrick Escarcega
This article assumes you�re familiar with XML and Visual Basic
Level of Difficulty     1   2   3 
SUMMARY The XML Wrapper template described in this article transforms XML documents into Visual Basic classes, hiding the more complex parts of using the Microsoft XML parser. Developers who have little knowledge of XML or the Microsoft XML parser can use classes created with the template, thus making it easier to use XML in their projects.
      This article describes the template, shows classes in a sample application based on the template, and explains how to customize those classes to support repeating child elements. Although this project is illustrated using Visual Basic 6.0, the technique can be extended for use with other versions of Visual Basic and with other languages.
XML is a powerful, platform-independent markup language frequently used for representing data. When combined with XML schemas, it's possible to define a document type and enforce that a given document conforms to it. Many industry groups have defined and published XML schemas that dictate how XML is formatted to represent data in their respective problem domain. Applications that consume and provide their data as XML based on these schemas are able to interoperate, even though they may be implemented in different languages, and are running on different platforms from different vendors. Tool vendors are beginning to create reusable components that enable developers to create and parse XML documents, as well as their schemas. The Microsoft® XML (MSXML) component is an excellent example. It can create or parse XML-based data, and it exposes a flexible tree-based object model via the document object model (DOM), whose hierarchy relates to that of the document on which it operates.
      The drawback to using the MSXML component is that developers have to be familiar with XML as well as with the component. Because MSXML is still a relatively new technology, this may be an unreasonable expectation. For some projects, the cost of learning a new object model (and stabilizing the resulting code) may be enough to prohibit the adoption of an XML-based design. Our solution is to create XML wrapper classes—Visual Basic® classes that have the ability to serialize themselves into a schema-based XML document. We achieve this by adding XML creation and parsing functionality to each class in the application whose data we plan to share.
      In this article we will use several of the classes from a sample order entry application and its associated order entry schema to demonstrate our technique. We'll also provide a mocked-up user interface that demonstrates how these objects would be used. We'll start by describing the XML schema that the application's data is based on, then show you how we used the XML Wrapper template to create the classes in our order entry sample. We'll also show you how we customized the classes to support repeating child elements of the same schema type.

XML Wrapper Classes

      Figure 1 shows a fragment of the XML schema that our sample application will use. This fragment defines an element that contains information that might be used in describing a customer. The following XML element conforms to this schema:
<Customer ID="1234">
    <Name>J. Customer</Name>
    <Address>123 Easy St.</Address>
    <City>Paradise</City>
    <State>HI</State>
    <ZIP>99999</ZIP>
    <Telephone>(808)555-1212</Telephone>
</Customer>
      We'll start by building a class in Visual Basic, CCustomer, that represents this schema's Customer element within the application. It will have ID, Name, Address, City, State, ZIP, and Telephone properties, and can be used like this:
Dim objCustomer As CCustomer

Set objCustomer = New CCustomer

With objCustomer
    .ID = 1234
    .Name = "J. Customer"
    .Address = "123 Easy St."
    .City = "Paradise"
    .State = "HI"
    .exe = "99999"
    .Telephone = "(808)555-1212"
End With
      The use of the object should be very familiar to anyone programming in Visual Basic. What makes this object unusual is that it also exposes a property called XML. Reading the XML property of the objCustomer object after making the previous assignments generates XML that represents the current state of the object's instance, as shown here:
<Customer ID="1234">
    <Name>J. Customer</Name>
    <Address>123 Easy St.</Address>
    <City>Paradise</City>
    <State>HI</State>
    <ZIP>99999</ZIP>
    <Telephone>(808)555-1212</Telephone>
</Customer>
      The resulting XML can be used to drive an XSL transform for display in a Web browser, or it can be used as a serialization mechanism between components or disparate applications for the purpose of passing objects by value.

Building a Wrapper Class

      Now you're ready to see how to build an XML wrapper class. In the sample code for this article is a Visual Basic class template file called XML Wrapper.cls, which contains the implementation for a class called CXMLWrapper. This class contains all of the code necessary to wrap XML creation and parsing functionality into your application classes.
      Our technique uses the MSXML parser to read and write XML, so you will need this component on your system. To obtain the MSXML parser, go to http://msdn.microsoft.com/xml and follow the link to download the most recent release of MSXML. Run the downloaded executable on your machine to install and register the MSXML component. Also, make sure that you have referenced the component in your Visual Basic project's References dialog.
      Using the class template feature in Visual Basic, we copied the XML Wrapper class to the class templates folder in Visual Basic (usually located at \Template\Classes under the Visual Basic installation folder). The template class becomes available for adding to Visual Basic projects, as shown in Figure 2.

Figure 2 Add XML Wrapper Template Class
Figure 2 Add XML Wrapper Template Class

      Now using the CXMLWrapper class as our guide we added a new class to our project and named it CCustomer. We followed six steps to modify the CCustomer class based on the Customer XML element shown in Figure 3.
  1. Edit the NODE_NAME constant to reflect the name of the element being wrapped.
  2. Add properties to the class to represent the wrapped element's attributes and child elements.
  3. Add code to the class's CreateNode method for each attribute and child element to build an XML DOM node.
  4. Add code to the class's CreateNode method for each attribute and child element to read an XML DOM node.
  5. Add code to the Class_Initialize subroutine to instantiate any child objects.
  6. Add code to the Class_Terminate subroutine to release the child objects instantiated in Class_Initialize.
      First, within the code of the new class, we edited the NODE_NAME constant declaration so that it reflects the name of the element being wrapped. For our example, we want to map the CCustomer class to the Customer element in our XML, so the NODE_NAME constant declaration looks like this:
Private Const NODE_NAME = "Customer"
      Next, we added properties to the class to represent the wrapped element's attributes and child elements. Each attribute and child element is represented by a property having the same name as the attribute or child element. For example, our sample XML schema indicates that the Customer element has an attribute called ID, which has a data type of i4 (4-byte integer). To represent this attribute, we added a property called ID to the CCustomer class, with a data type of Long. The template class has the following template code and comments as a guide:
'******* SAMPLE ATTRIBUTE DECLARATION *******
'Copy the following two lines to declare a property that represents an
'attribute. Replace "MyAttribute" with the name of the attribute, and
'replace "MyDataType" with the appropriate data type for the attribute.
'Public MyAttribute As MyDataType
'Private Const ATTRIBUTE_MyAttribute = "MyAttribute"
      To use this code from the template, we copied and uncommented the last two lines, changing MyDataType to the type of the attribute being mapped (in this case, Long). We then changed the three occurrences of MyAttribute to the name of the attribute we're mapping. Here is the result for the ID attribute.
Public ID As Long
Private Const ATTRIBUTE_ID = "ID"
In addition to declaring the ID property, a constant is declared to hold the attribute name. This constant is used later in the code to reference the attribute in the XML DOM hierarchy.
      Child elements are handled in a similar way, but are a little more complicated. We make a distinction between child elements that contain data of some base type, such as string or integer, and child elements that contain nested child elements. For the remainder of this article we use the term "data element" to refer to a child element that contains data of some base type, and the term "nested element" to refer to a child element that contains child elements within themselves. Note that these terms are ours only—they are not typically used when speaking of XML child elements.
      The template code for data elements looks like this:
'******* SAMPLE DATA ELEMENT DECLARATION *******
'Copy the following two lines to declare a property that represents a
'child element that will contain a value of some base type, i.e., that
'will not itself contain further child elements. Replace "MyDataElement"
'with the name of the child element, and replace "MyDataType" with the
'appropriate data type for the value that will be held in the child
'element.
'Public MyDataElement As MyDataType
'Private Const ELEMENT_MyDataElement = "MyDataElement"
      To use this code, we change MyDataType to the type of the Element's content (in this case, String), and then change all three occurrences of MyDataElement to the name of the XML element we're mapping to. For example, our Customer element's Name element is represented by this code:
Public Name As String
Private Const ELEMENT_Name = "Name"
      In order to support nested child elements, our classes themselves will have to be nested. Because our Customer element doesn't contain any nested elements, we will use the Order element as an example. Our Order element contains a Customer element. Because the Customer element also has child elements, it is represented not as a base data type, but as a class called CCustomer. Our COrder class must therefore make use of this template code:
'******* SAMPLE NESTED ELEMENT DECLARATION *******
'Copy the following line to declare a property that represents a child
'element that will contain further child elements. Replace
'"MyNestedElement" with the name of the child element.
'Public MyNestedElement As CMyNestedElement
      To use this code, we replaced the two occurrences of the string "MyNestedElement" with the name of the nested element and uncommented the code. Our COrder class now contains the following code to represent our nested Customer element:
Public Customer As CCustomer
      For step five, we added code to the CreateNode method to handle each attribute and child element. The purpose of the CreateNode method is to build up an XML DOM node based on the values found in the class's properties, as well as its nested child objects. Let's start by taking a look at the CreateNode's method signature.
      The CreateNode method accepts an IXMLDOMNode interface as its one optional parameter. This is so our classes can call the createNode methods of their nested classes and have the resulting DOM node attached to a single tree of DOM nodes. If an XML DOM node is passed in the Parent parameter of the CreateNode method, then our code will create a new node representing its properties, then add it as a child to the passed-in node. If the Parent parameter is missing or passed as Nothing, then the method creates a top-level document node.
      Whether or not a parent node is passed in, the class's properties are saved as children of the new node. To save attributes and data elements into the new node, our helper functions, NodeAppendAttribute and NodeAppendChildElement, are used. (This code is located near the end of the template class.) To save nested elements, the CreateNode method of the nested element class is called.
      Consider again the CCustomer class in our example, which wraps the Customer element. Customer elements in our schema have an ID attribute. As you saw in the previous step, we added code to expose an ID property from the CCustomer class. Now we need to add code to the CreateNode method to save the value of the ID property into an attribute of the newly created node. The template code that is used for saving attributes is:
'******* SAMPLE CODE TO SAVE AN ATTRIBUTE *******
'Copy the following line to save an attribute. Replace "MyAttribute"
'with the name of the attribute.
'NodeAppendAttribute dom, node, ATTRIBUTE_MyAttribute, MyAttribute
      For our CCustomer class, we replaced "MyAttribute" with "ID", and then uncommented the code to arrive at the following:
NodeAppendAttribute dom, node, ATTRIBUTE_ID, ID
This line of code invokes the NodeAppendAttribute procedure to create a new attribute in the node's attributes collection, naming it ID (recall that this is the value of the ATTRIBUTE_ID constant), and assigning it the value held in the class's ID property.
      Similarly, the template code to save data elements is:
'******* SAMPLE CODE TO SAVE A DATA ELEMENT *******
'Copy the following line to save a child element that will contain
'a value of some base type. Replace the string "MyDataElement" with
'the name of the child element.
'NodeAppendChildElement dom, node, ELEMENT_MyDataElement, MyDataElement
      We followed the same procedure we used for the attribute, replacing both occurrences of "MyDataElement" with the actual element name. For example, here's how our Name element is handled in our CCustomer class:
NodeAppendChildElement dom, node, ELEMENT_Name, Name
      Finally, the template code for saving a nested element is:
'******* SAMPLE CODE TO SAVE A NESTED ELEMENT *******
'Copy the following line to save a child element that will contain
'further child elements. Replace the string "MyNestedElement" with the
'name of the child element.
'MyNestedElement.CreateNode node
      Again, we followed a procedure similar to the one used for the attribute and changed the name of "MyNestedElement" to "Customer" to arrive at the following line of code:
Customer.CreateNode node
      The CCustomer class's complete CreateNode function from CCustomercls is shown in Figure 4.
      In the sixth step, we add code to the class's LoadNode method for each attribute and child element. The purpose of the LoadNode method is to read an XML DOM node and set the class's properties based on the values found in the node. As before, commented-out template code is given for loading attributes, data elements, and nested elements. The complete LoadNode method from our CCustomer class is shown in Figure 5. The LoadNode method uses our helper function, GetNodeText, to retrieve the text value of the named attribute or child element.
      Next, we need to add code to the Class_Initialize subroutine to instantiate any child objects. Recall that child objects are used for representing child elements that themselves contain attributes and additional nested elements. Our CCustomer class has no child objects, so this subroutine is left blank in that class. The code from our COrder class's Class_Initialize subroutine is shown here:
Private Sub Class_Initialize()
    'TODO: Instantiate any child element objects.
    'Copy the following line and replace "MyNestedElement" with the name
    'of the child element.
    Set OrderItems = New COrderItems
    Set Customer = New CCustomer
    'Set MyNestedElement = New CMyNestedElement
End Sub
This class contains two child classes: COrderItems and CCustomer, which in turn wrap the OrderItems and Customer elements, respectively.
      The last step is to add code to the Class_Terminate subroutine to release the child objects instantiated in Class_Initialize. Again, our CCustomer class doesn't need any code in this subroutine because it doesn't have any child objects, however our COrder class's Class_Terminate subroutine looks like this:
Private Sub Class_Terminate()
    'TODO: Release any child object references.
    'Copy the following line and replace "MyNestedElement" with the name
    'of the child element.
    Set OrderItems = Nothing
    Set Customer = Nothing
    'Set MyNestedElement = Nothing
End Sub
      That's all there is to it. Now that our Customer class has been created, it reads and writes XML without the class's user having any knowledge of how it's done.

Repeating Child Elements

      So far, the class we've described supports child elements that appear exactly once. However, the OrderItem element in our sample schema can appear zero or more times within its parent OrderItems element. To support this, it is necessary to customize our COrderItems class. This section describes the steps to add support for repeating child elements of the same type.
      First, we add a new class to our project based on the CXMLWrapper class template and name it COrderItems. Within the code of the new class, find the NODE_NAME constant declaration, and edit NODE_NAME so that it is declared as OrderItems, as shown here:
Private Const NODE_NAME = "OrderItems"
      Immediately following the NODE_NAME declaration, add the following block of code.
'This block of code has been added to support repeating <OrderItem>
'elements. This is not part of the standard template.

    'The repeating items are stored in a collection.
    Private m_colItems As Collection

'End of code to support repeating <OrderItem> elements.
This declares a collection object that will be used to hold the repeating COrderItem objects.
      In the CreateNode function, place the following code immediately after the TODO comment. This loops through the COrderItem objects, saving them to the DOM node.
'This block of code has been added to support repeating <OrderItem>
'elements. This is not part of the standard template.

    'Save the repeating <OrderItem> elements.

    Dim objOrderItem As COrderItem

    For Each objOrderItem In m_colItems
        objOrderItem.CreateNode node
    Next

    Set objOrderItem = Nothing

'End of code to support repeating <OrderItem> elements.
      In the LoadNode function, place the block of code shown in Figure 6 immediately following the TODO comment. This loads COrderItem objects from the DOM node.
      In the Class_Initialize event handler, place the following code immediately after the TODO comment. This initializes the collection object that will hold the COrderItem objects.
'This block of code has been added to support repeating <OrderItem>
'elements. This is not part of the standard template.

    'Instantiate the collection object that will hold the COrderItem
    'objects.
    Set m_colItems = New Collection

'End of code to support repeating <OrderItem> elements.
      In the Class_Terminate event handler, place this block of code immediately following the TODO comment. This releases the collection object.
'This block of code has been added to support repeating <OrderItem>
'elements. This is not part of the standard template.

'Release the collection of items. 
'Set m_colItems = Nothing

'End of code to support repeating <OrderItem> elements.
      At the very end of the class file, add the code in Figure 7. This adds collection semantics to the class, including the ability to enumerate the order items using For Each�Next loops.
      Figure 8 shows how our COrderItems object is accessed. It shows the CreateDummyOrder function, taken from our order_business project. Note in particular how a COrderItem is created and initialized first, then added to the COrderItems collection.

The Sample Application

      To illustrate these concepts, we have provided a simple order entry system that implements four classes created with our technique, two of which we've already explored. It also provides a user interface and mocked-up business/data tier that show how these objects would be used.
      To begin, load the project group xml.vbg in Visual Basic. This group contains three projects:
  • Order_xml, which implements the classes that wrap our sample schema, and is the focus of this article. The sample schema is shown in Figure 3.
  • Order_business, which implements a mocked-up business/data tier. It contains a single class, COrderAccessor, whose job is to load and save orders. It's the loading and saving operations that are mocked up; there is no actual database with our order entry sample application.
  • Order_ui, which implements a hypothetical presentation tier. It contains a single form, frmOrder, whose job is to exercise the other two projects.
      To run the sample application, select Start from the Run menu. This runs the startup project order_ui. When the program starts up, you'll notice that there aren't any orders open.
      To open the sample application's single order record, click the Open Order button to open. This will update the display, as shown in Figure 9. The sample application allows you to add line items to this order, but doesn't provide a way to add new orders or to delete this order.

Figure 9 Order Information
Figure 9 Order Information

      The tree view on the left side of the window shows the hierarchy of the order record. The root represents the order as a whole, and the leaves represent individual order line items. The right side of the window displays information about the item that has been selected on the left side, as shown in Figure 10.

Figure 10 Line Item Detail
Figure 10 Line Item Detail

      The display on the right is created by generating HTML on the fly and feeding it to a WebBrowser control for display. (The WebBrowser control is added to the Visual Basic toolbox by setting a reference to Microsoft Internet Controls.)
      The HTML is generated in a two-step process. First, an XML representation of the data is retrieved from the appropriate XML wrapper object by accessing its XML property. If you look in the code of the CCustomer class you'll see there's an XML property. The XML is retrieved from the CCustomer object by accessing this property. The Property Get XML subroutine simply calls CreateNode to retrieve the DOM Document Node representing the class and its hierarchy, and then returns its XML value. For displaying a line item, the same procedure is followed on the COrderItem object. They each have an XML property for retrieving an XML representation of the object's data.
      The second step in generating the HTML is to transform this XML using an MSXML object and one of the XSL files that have been supplied with the project (order.xsl for transforming the XML retrieved from a COrder object and orderitem.xsl for transforming the XML retrieved from a COrderItem object). For brevity (and because this article isn't about XSL transformations) the XSL files aren't reproduced here, but they're included in the download, which can be accessed from the link at the top of this article. The result of the transformation is HTML that is then passed to the WebBrowser control.

Figure 11 Raw XML
Figure 11 Raw XML

      To view the XML that drives the display, click the View Raw XML button. The right-side display changes to show the nontransformed XML, as shown in Figure 11. Click the View Raw XML button again to return to normal display. If the XML appears as a single line on your computer rather than in the hierarchical format shown in Figure 11, you don't have Microsoft Internet Explorer 5.0 installed. You can still view the entire XML string by using the horizontal scrollbar, it's just not formatted as well.

Potential Enhancements

      There are several enhancements that could be made to our technique, depending on your needs. Note that some of these enhancements would require significant modifications to the template class.
      The first enhancement is to represent all elements as classes. In the technique presented here, elements that don't have child elements are represented merely as properties in the parent element's class. In this arrangement, such elements can't have attributes. By representing all elements as classes, this limitation is overcome.
      Another option is to use a different naming scheme for exposing class properties. If your XML schema has element names that are the same as Visual Basic keywords, you will not be able to use those element names as property names.
      You can also make the template class persistable. In the class's ReadProperties and WriteProperties event handlers, save only the class's XML property to the PropertyBag.
      Finally, you could write a Visual Basic add-in that reads an XML schema and generates appropriate wrapper classes based on the template class.

Conclusion

      We have used the technique presented in this article in real-world production applications. It allows quick and easy development of XML-based applications, even while supporting complex XML hierarchies. One of the most important benefits of the classes created from the template wrapper class provided with this article is that developers using them can take advantage of the power of MSXML without knowing its object model.
For related articles see:
XML Schemas Developer�s Guide
XML Code Generator: Generating Visual Basic Classes from XML Schemas

For background information see:
http://msdn.microsoft.com/xml

Dave Grundgeiger is a consultant at Tara Software Inc. (http://www.tarasoftware.com) in Madison, WI, where he spends his days immersed in cool Microsoft technologies. Dave is the author of CDO & MAPI Programming with Visual Basic (O'Reilly & Associates, 2000).
Patrick Escarcega (the best looking guy in Madison, WI) is a consultant for Tara Software. Patrick can be reached at patricke@tarasoftware.com.

From the January 2001 issue of MSDN Magazine

Page view tracker