Menu

XML and Visual Basic

July 12, 2000

Kurt Cagle



The Odd Couple

Table of Contents

The Odd Couple
VB, DOM, and XSLT
Controlling Dataviews with XSLT

Visual Basic is probably the most commonly used programming language in the world. Certainly Microsoft's "middle-tier" language has managed to become a powerful force for the development of client/server applications, utilities, and similar applications. In great part this is because it works on the assumption that most people do not want to spend a week and several thousand lines of code to produce "Hello, World!" programs--they want something "quick and dirty" that hides the complexity of programming and lets them concentrate on the business side of things.

VB6, the most recent version of Visual Basic, which came out as part of Microsoft's Visual Studio package, appeared on the landscape in early 1998. At the time, another language was also in the works--XML. There was almost no acknowledgement by Visual Basic that XML was a force to be reckoned with (although VB6 "web classes" created a simple form of what's now recognized as a SAX based parser, though the creators at the time hadn't been aiming to do that). As such, Visual Basic's native support for XML is non-existent--no component can read or produce it, no persistence mechanisms exist around it, no data engines work with it.

Source code

Download supporting source code for this article

However, if that really were the extent of interaction between XML and VB, then the need for an article on the two together would never have arisen! Despite not supporting it natively, VB has proven to be a remarkably effective tool for working with XML. The reason for this is actually pretty simple: the MSXML parser exposes nearly all of the same interfaces in Visual Basic as it does in C++, and the parser that it does expose is arguably one of the best on the market.

A clarification is in order here. Before getting jeered off the stage, I want to note that I'm focusing on the most recent parser, the Microsoft Web Release MSXML3 parser, which is currently still in beta. If you have not yet downloaded this toolkit, do so now! While it won't support all of the features implemented in the W3C specification until its final release later this fall, it implements enough of them that you can begin to see the real power of the W3C XSLT recommendation, XPath, schemas, and more. The older parser relies on specifications that are themselves more than a year out of date (a glacial age in this industry), and the new parser is so superior (even in beta) that there really is no comparison. As you may have guessed, it's also the code that's used in this article.

VB, DOM, and XSLT

The Microsoft parser was one of the first to utilize the notion of a document object model for XML, which in turn went a long way toward establishing the XML Document Object Model for the W3C. While the two (even now) differ somewhat in the capabilities offered, there is enough similarity that anyone familiar with the Java model will be able to jump to the MSXML model (and vice versa).

In Visual Basic, in order to be able to link in a library such as MSXML, you create a reference to it by selecting Project-->References in the development environment and choosing the relevant DLL or EXE file. In the case of the new MSXML parser, you would select the entry "Microsoft XML, v3.0". This makes the MSXML3.dll class library available for use in the development environment, which contains all of the Microsoft XML functionality.

The DOM itself is built around five primary pieces--documents, nodes, nodelists, elements, and attributes. Each of these has their own unique programmatic interfaces (specifically, DOMDocument, IXMLDOMNode, IXMLDOMNodeList, IXMLDOMElement, and IXMLDOMattribute), each of which expose a number of methods and properties. For example, the DOMDocument interface handles both the loading and saving of XML files (through .load, .loadXML--which converts an XML structured string into an internal DOMDocument--and .save), while the IXMLDOMElement interface handles referencing attributes, content text, and element children, and provides a starting point for generating XPath queries.

XPath support is a subtle part of the Microsoft DOM that isn't supported in the current W3C DOM specification, but it plays a large part in the versatility of the MSXML parser when dealing with data. XPath is probably most familiar as the language used by XSLT to select nodes within a given XML tree--in essence, to create node-sets. The terminology was not yet nailed down when Microsoft's DOM was first proposed, so Microsoft referred to these node-sets as "nodeLists." The parser supports an IXMLDOMNodeList interface that can be used to reference a list of nodes, as well as the new IXMLDOMSelection, which serves as a way to deal with the nodes as a single unit.

The ability to pull an arbitrary selection of nodes out of an XML DOM is what makes the parser so powerful, and the fact that the MSXML parser can perform XPath queries on a set of data means that it can effectively reference any nodes within the XML tree, based upon (sometimes highly sophisticated) queries. The parser contains two functions to do this: .selectNodes() and .selectSingleNode(), which retrieve a selection of nodes and the first-found node that satisfies the XPath query, relative to the node in which the function is called.

Example

An example is perhaps in order here. Consider a very simple application that can read an XML document, populate a list box with the titles of each section, then, when a section is clicked, output the section's text into a text box. While perhaps not very exciting, this example illustrates how you could build more complex applications that take advantage of the XPath strategy to retrieve information.

The XML document is retained as a public variable called articleDoc, which has been initialized as a DOM interface DOMDocument (dim articleDoc as New DOMDocument).

Application user interface

Figure 1: Application User Interface

The application itself uses a simple menu, a list box, a text box, and the Common Dialog control for selecting the XML file in the first place, as laid out in Figure 1. When a user selects Open from the menu, the onMnuFileOpen event gets called, which in turn calls the function LoadDocument(). This particular routine first sets the asynchronous property of the DOM to false so that the XML document loads synchronously--execution pauses until either the document is completely loaded in memory, or an error occurs. Then it loads the requested document and sets the caption of the form (the title bar of the window) to be the title of the document:


Public articleDoc As New DOMDocument



Private Sub mnuFileOpen_Click()

    cdlg.Filter = "XML Files (*.xml,*.xsl)|*.xml;*xsl"

    cdlg.ShowOpen

    If cdlg.FileName <> "" Then

        LoadDocument cdlg.FileName

    End If

End Sub



Function LoadDocument(url As String)

    articleDoc.async = False

    articleDoc.Load url

    Me.Caption = articleDoc.selectSingleNode(_

	     "//head/title").Text

    loadSelections

End Function

Notice here the use of selectSingleNode() to retrieve the title of the head element, where the title for the document itself is contained. The .text method retrieves the text contents of the element's subtree.

Once the document is loaded, the loadSelections routine gets called. This actually populates the list box, and demonstrates the use of selectNodes for retrieving specific node information:


Public Function loadSelections()

    Dim sections As IXMLDOMNodeList

    Dim sectionNode As IXMLDOMNode

    Set sections = articleDoc.selectNodes("//section")

    SectionsList.Clear

    For Each sectionNode In sections

        SectionsList.AddItem_

			section.selectSingleNode("title").Text

    Next

    SectionsList.ListIndex = 0

End Function

The selectNodes method retrieves a node list consisting of pointers to each of the section nodes. The list control (called SectionsList) is then cleared, and VB loops through each section node in turn, adding to the control the text of the title associated with the section node. Finally, the routine selects the zeroeth index (the first position in the list), which in turn fires the Click event for the SectionsList control:


Private Sub SectionsList_Click()

    DisplayContents SectionsList.Text

End Sub



Public Function DisplayContents(sectionName As String)

    Dim sectonContent As String

    Dim sectionNode As IXMLDOMElement

    Dim buffer As String

    Dim subElt As IXMLDOMElement

    With SectionsList

        sectionName = .Text

        Set sectionNode = articleDoc.selectSingleNode(_

		     "//section[title='" + sectionName + "']")

        For Each subElt In sectionNode.selectNodes("*")

            buffer = buffer + subElt.Text + vbCrLf + vbCrLf

        Next

        SectionContentText.Text = buffer

    End With

End Function

This example serves as a good demonstration of the power of XPath. The title of each section should be distinct. As a consequence, it is possible to uniquely identify a given section by its title, then to use selectSingleNode() to retrieve that node. It should be noted that this actually echoes an equivalent paradigm in XSLT:


<xsl:param name="$sectionName"/>

<xsl:template match="section[title=$sectionName]">

	<xsl:for-each select="*">

		<xsl:value-of select="."/>

	</xsl:for-each>

</xsl:template>

One lesson from this is that if you are stymied by an XSLT problem, try writing the problem out as a procedural problem first, then apply the tags. This doesn't always work--the recursive nature of XML is often better handled by XSLT than it is by a language like VB--but it can prove a good starting point. Additionally, elements in an XML document do not always have explicit IDs associated with them, but you can use XPath expressions to pull out elements that have some unique characteristic to them (such as the title element, or you can generate IDs through such methods as the generate-id() function in XSLT).

Controlling Dataviews with XSLT

Because of the hierarchical nature of XML, it would seem easy to populate a tree, but in fact it often isn't that simple. One of the problems that comes up almost as a matter of course is that simply because XML can be shown in a treeview doesn't mean that you necessarily want every node in the tree appearing. Consider, for example, a purchase order document. There are in fact a number of different ways that you can display the data. You may actually be interested in seeing all of the nodes in the XML document to make sure that the information is in fact consistent, but you may also want to create a treeview structure that shows only the names of each product being purchased--when the node is selected, it causes more detailed information to appear in the right hand pane of the application. This is typical of most Explorer-type applications.

Here's a sample purchase order document:


<purchase_order>

  <line_items>

	<line_item>

	    <item_number>12291</item_number>

	    <name>Mermaid Laptop</name>

		<cost currency="USD">1234.45</cost>

		<description>Top of the line Mermaid model laptop, 

		with a 15" superscreen running Redhat Linux v6.8.</description>

		<count>1</count>

	</line_item>

	<line_item>

		<item_number>42583</item_number>

		<name>8X DVD Player</name>

		<cost currency="USD">199.95</cost>

		<description>8x speed CD and DVD player upgrade kit

		</description>

		<count>1</count>

	</line_item>

	<line_item>

		<item_number>98242</item_number>

		<name>Memory</name>

		<cost currency="USD">43.95</cost>

		<description>64 MB DRAM</description>

		<count>4</count>

	</line_item>

	<line_item>

		<item_number>65192</item_number>

		<name>12 GB Hard Drive</name>

		<cost currency="USD">159.95</cost>

		<description>12 GB Seagate compatible drive.

		</description>

		<count>1</count>

	</line_item>

	<line_item>

		<item_number>41221</item_number>

		<name>Porta-pen</name>

		<cost currency="USD">65.95</cost>

		<description>Stylus-based pad for mouse entry.

		</description>

		<count>1</count>

	</line_item>

 </line_items>

</purchase_order>

Coding the mechanism for displaying the XML document directly into your program isn't always the wisest course of action. It forces you to recompile your application every time you need to change the display (and distribute it, which is perhaps the worse headache), it requires a lot of fairly ugly DOM code to be able to handle the mapping of elements and attributes, and it can make managing the interfaces difficult. Rather than going this route, you may want to move much of your presentation logic into an XSLT style sheet that then transforms the XML data into a format that's easier to process.

For example, the Treeview control, which is used to display hierarchical elements in Visual Basic programs, makes use of a linear method to add new nodes to the tree:


    TreeView1.nodes.add relative,relationship,key,text,icon

You can use an XSLT style sheet to transform the hierarchical structure of an XML document directly into a flat array that can then be iterated over. In vbtree.xsl, this is accomplished by matching every element in the tree in the order encountered, then outputting a <tvwnode> that gives the node's key value, the relationship (either 0 for the first node or 4 for an element that's a child node--everything except the first node, essentially).


<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 

 version="1.0" 

 xmlns:vb="http://www.microsoft.com/vb">

 <xsl:output method="xml"/>

  <xsl:template match="/">

	<xmp>

	  <tvwnodes>

		<xsl:apply-templates select="*"/>

	  </tvwnodes>

	</xmp>

  </xsl:template>

	

  <xsl:template match="*">

	<xsl:param name="ancestor_key"/>

	<xsl:param name="display" select="'yes'"/>

	<xsl:variable name="key" select="generate-id()"/>

	<tvwnode>

	<xsl:attribute name="display"><xsl:value-of

	    select="$display"/></xsl:attribute>

	<xsl:attribute name="key">

	<xsl:value-of select="$key"/>

	</xsl:attribute>

	<xsl:choose>

	  <xsl:when test="parent::*">

		 <xsl:attribute name="relation">4</xsl:attribute>

		 <xsl:attribute name="relative">

		 <xsl:value-of select="$ancestor_key"/>

		 </xsl:attribute>

		</xsl:when>

		<xsl:otherwise>

		 <xsl:attribute name="relation">0</xsl:attribute>

		 <xsl:attribute name="relative">

		 <xsl:value-of select="$key"/>

		 </xsl:attribute>

		</xsl:otherwise>

	</xsl:choose>

	<xsl:choose>

		<xsl:when test="text()">

		 <xsl:attribute name="title">

    <xsl:value-of select="concat('&lt;',name(.),'&gt;',text())"/>

         </xsl:attribute>				

		</xsl:when>

		<xsl:otherwise>

		 <xsl:attribute name="title"><xsl:value-of

	 select="concat('&lt;',name(.),'&gt;')"/></xsl:attribute>				

		</xsl:otherwise>

	</xsl:choose>

	</tvwnode>

	<xsl:apply-templates select="*">

		<xsl:with-param name="ancestor_key" select="$key"/>

		<xsl:with-param name="display" select="$display"/>

	</xsl:apply-templates>

 </xsl:template>

</xsl:stylesheet>

The origin of the keys is worth discussing. While some XML structures contain identifiers, not all do. Rather than relying on IDs, the transformation essentially handles the process of determining which node was selected by auto-generating keys. Note that it will be necessary to map the keys to the source document in some manner. Since a selectNodes("*") statement will walk the source document in the same order it did to generate the target document, keys are attached as attributes to each source node in the same order as well, making a match. Of course, this assumes that no sorting takes place until after the keys are initially assigned to the document.

Populating the tree itself then involves using the style sheet to transform the source code into a form that can be easily processed. In the case of the new MSXML parser, this is handled through the agency of a processor, which includes a compiled style sheet. The LoadFilter function takes a path to the URL, loads it into an XML document object, then creates a new processor based upon this style sheet. In compiled form, the transformation is considerably faster than it would be otherwise, and you can essentially change transformations on the fly (something discussed below).


Public Sub LoadFilter(filterPath As String)

    Dim xslDoc As FreeThreadedDOMDocument

    Dim template As XSLTemplate

    

    Set xslDoc = New FreeThreadedDOMDocument

    Set template = New XSLTemplate

    xslDoc.async = False

    xslDoc.Load filterPath

    Set template.stylesheet = xslDoc

    Set Proc = template.createProcessor

End Sub

Once the processor is created, it is a global variable that can be invoked by the populateTree function. This can be applied to the source document (which is also contained in a global variable called sourceDoc) to create a new document that's stored in TargetDoc. This transformed is then iterated over to backfill keys into the source document--to make sure that a node in the treeview control corresponds to a node in the original document. If a node isn't intended to be seen (it has a display attribute value of 'no'), then the node is not reflected into the TreeView control.

Here are the treeview population instructions, transformed from the original purchase order document:


<tvwnodes>

	<tvwnode display="yes" key="ID02OT5" relation="0"

	 relative="ID02OT5" title="&lt;purchase_order&gt;"/>

	<tvwnode display="yes" key="IDw2OT5" relation="4" 

	relative="ID02OT5" title="&lt;line_items&gt;"/>

	<tvwnode display="yes" key="ID03OT5" relation="4" 

	relative="IDw2OT5" title="&lt;line_item&gt;"/>

	<tvwnode display="yes" key="IDw3OT5" relation="4"

	relative="ID03OT5" title="&lt;item_number&gt;12291"/>

	<tvwnode display="yes" key="ID04OT5" relation="4"

	relative="ID03OT5" title="&lt;name&gt;Mermaid Laptop"/>

	<tvwnode display="yes" key="IDw4OT5" relation="4"

	relative="ID03OT5" title="&lt;cost&gt;1234.45"/>

	<tvwnode display="yes" key="ID06OT5" relation="4" 

	relative="ID03OT5" title="&lt;description&gt;Top

	of the line Mermaid model laptop, with a 15&quot;

	superscreen running Redhat Linux v6.8."/>

	<tvwnode display="yes" key="IDw6OT5" relation="4"

	 relative="ID03OT5" title="&lt;count&gt;1"/>

	<tvwnode display="yes" key="ID07OT5" relation="4" 

	relative="IDw2OT5" title="&lt;line_item&gt;"/>

	<tvwnode display="yes" key="IDw7OT5" relation="4" 

	relative="ID07OT5" title="&lt;item_number&gt;42583"/>

	<tvwnode display="yes" key="ID08OT5" relation="4" 

	relative="ID07OT5" title="&lt;name&gt;8X DVD Player"/>

	<tvwnode display="yes" key="IDw8OT5" relation="4" 

	relative="ID07OT5" title="&lt;cost&gt;199.95"/>

	<tvwnode display="yes" key="ID0aOT5" relation="4"

	relative="ID07OT5" title="&lt;description&gt;8x

	speed CD and DVD player upgrade kit"/>

	<tvwnode display="yes" key="IDwaOT5" relation="4" 

	relative="ID07OT5" title="&lt;count&gt;1"/>

	<tvwnode display="yes" key="ID0bOT5" relation="4"

	relative="IDw2OT5" title="&lt;line_item&gt;"/>

	<tvwnode display="yes" key="IDwbOT5" relation="4" 

	relative="ID0bOT5" title="&lt;item_number&gt;98242"/>

	<tvwnode display="yes" key="ID0cOT5" relation="4" 

	relative="ID0bOT5" title="&lt;name&gt;Memory"/>

	<tvwnode display="yes" key="IDwcOT5" relation="4" 

	relative="ID0bOT5" title="&lt;cost&gt;43.95"/>

	<tvwnode display="yes" key="ID0eOT5" relation="4"

	relative="ID0bOT5" title="&lt;description&gt;64 MB DRAM"/>

	<tvwnode display="yes" key="IDweOT5" relation="4" 

	relative="ID0bOT5" title="&lt;count&gt;4"/>

	<tvwnode display="yes" key="ID0fOT5" relation="4" 

	relative="IDw2OT5" title="&lt;line_item&gt;"/>

	<tvwnode display="yes" key="IDwfOT5" relation="4" 

	relative="ID0fOT5" title="&lt;item_number&gt;65192"/>

	<tvwnode display="yes" key="ID0gOT5" relation="4" 

	relative="ID0fOT5" title="&lt;name&gt;12 GB Hard Drive"/>

	<tvwnode display="yes" key="IDwgOT5" relation="4"

	relative="ID0fOT5" title="&lt;cost&gt;159.95"/>

	<tvwnode display="yes" key="ID0iOT5" relation="4"

	relative="ID0fOT5" title="&lt;description&gt;12 GB

	Seagate compatible drive."/>

	<tvwnode display="yes" key="IDwiOT5" relation="4"

	relative="ID0fOT5" title="&lt;count&gt;1"/>

	<tvwnode display="yes" key="ID0jOT5" relation="4"

	relative="IDw2OT5" title="&lt;line_item&gt;"/>

	<tvwnode display="yes" key="IDwjOT5" relation="4"

	relative="ID0jOT5" title="&lt;item_number&gt;41221"/>

	<tvwnode display="yes" key="ID0kOT5" relation="4"

	relative="ID0jOT5" title="&lt;name&gt;Porta-pen"/>

	<tvwnode display="yes" key="IDwkOT5" relation="4" 

	relative="ID0jOT5" title="&lt;cost&gt;65.95"/>

	<tvwnode display="yes" key="ID0mOT5" relation="4"

	relative="ID0jOT5" title="&lt;description&gt;

	Stylus-based pad for mouse entry."/>

	<tvwnode display="yes" key="IDwmOT5" relation="4" 

	relative="ID0jOT5" title="&lt;count&gt;1"/>

</tvwnodes>

The function populateTree() contains code that maps the source and target keys, and then actually populates the TreeView control:


Public Sub populateTree()

    Dim template As New XSLTemplate

    Dim targetDoc As New FreeThreadedDOMDocument

    Dim node As IXMLDOMElement

    Dim sourceNodeList As IXMLDOMNodeList

    Dim targetNodeList As IXMLDOMNodeList

    Dim sourceNode As IXMLDOMElement

    Dim targetNode As IXMLDOMElement

    Dim key As String

    Dim index As Long

    Dim txt As String

        

    Proc.input = sourceDoc

    Proc.output = targetDoc

    Proc.Transform

    Set sourceNodeList = sourceDoc.selectNodes("//*")

    Set targetNodeList = targetDoc.selectNodes("//tvwnode")

    index = 0

    For Each sourceNode In sourceNodeList

        Set targetNode = targetNodeList(index)

        sourceNode.setAttribute "key",_

  			targetNode.getAttribute("key")

        index = index + 1

    Next

    TreeView1.Nodes.Clear

    For Each node In targetDoc.selectNodes("//tvwnode")

        txt = ""

        key = node.getAttribute("key")

        Set sourceNode = sourceDoc.selectSingleNode(_

		      "//*[@key='" + key + "']")

        If sourceNode.selectNodes("text()").length > 0 Then

            txt = sourceNode.Text

        End If

        If node.getAttribute("display") = "yes" Then

            If node.getAttribute("relation") = 0 Then

                TreeView1.Nodes.Add , node.getAttribute(_

				"relation"), node.getAttribute("key"), _

				node.getAttribute("title")

           Else

                TreeView1.Nodes.Add node.getAttribute(_

				"relative"), node.getAttribute("relation"),_

				node.getAttribute("key"), _

				node.getAttribute("title")

            End If

        End If

    Next

End Sub

By associating the treeview keys and the source doc keys, you can retrieve relevant information about the source just by dereferencing the content. The GetSourceNode() function does that--you pass a key as an argument, and it returns the node associated with the key in the source document. You can then use the resulting XML node to display output.


Public Function GetSourceNode(key As String) As IXMLDOMElement

    Dim node As IXMLDOMElement

    Set node = sourceDoc.selectSingleNode(_

	"//*[@key='" + key + "']")

    Set GetSourceNode = node

End Function

Creating Alternative Views

One advantage of abstracting away the document processing into Proc is that you can change XSLT processors on the fly, altering the presentation layer. The XSLT filter described above can be easily modified to handle the specific case of showing the trees only down to the line_item level, with the name of the product being the name of the line_item. The view_line_items.xsl filter will do precisely that:


<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"_

 version="1.0" xmlns:vb="http://www.microsoft.com/vb">

  <xsl:output method="xml"/>

   <xsl:template match="/">

	<xmp>

	<tvwnodes>

		<xsl:apply-templates select="*"/>

	</tvwnodes>

	</xmp>

   </xsl:template>



   <xsl:template match="*">

	<xsl:param name="ancestor_key"/>

	<xsl:param name="display" select="'yes'"/>

	<xsl:variable name="key" select="generate-id()"/>

	<tvwnode>

	<xsl:attribute name="display"><xsl:value-of

	   select="$display"/></xsl:attribute>

	<xsl:attribute name="key"><xsl:value-of 

	   select="$key"/></xsl:attribute>

	<xsl:choose>

		<xsl:when test="parent::*">

			<xsl:attribute name="relation">4</xsl:attribute>

			<xsl:attribute name="relative">

			<xsl:value-of select="$ancestor_key"/>

			</xsl:attribute>

		</xsl:when>

		<xsl:otherwise>

			<xsl:attribute name="relation">0</xsl:attribute>

			<xsl:attribute name="relative">

			<xsl:value-of select="$key"/>

			</xsl:attribute>

		</xsl:otherwise>

	</xsl:choose>

	<xsl:choose>

		<xsl:when test="text()">

			<xsl:attribute name="title">

			<xsl:value-of select="

			concat('&lt;',name(.),'&gt;',text())"/>

			</xsl:attribute>				

		</xsl:when>

		<xsl:otherwise>

			<xsl:attribute name="title">

			<xsl:value-of

			 select="concat('&lt;',name(.),'&gt;')"/>

			 </xsl:attribute>				

		</xsl:otherwise>

	</xsl:choose>

	</tvwnode>

	<xsl:apply-templates select="*">

		<xsl:with-param name="ancestor_key" select="$key"/>

		<xsl:with-param name="display" select="$display"/>

	</xsl:apply-templates>

	</xsl:template>

	

<!-- this section is new. Note that you still must call apply-templates to

make sure that the keys stay consistent between source and transformed

documents -->

  <xsl:template match="line_item">

	<xsl:param name="ancestor_key"/>

	<xsl:param name="display"/>



	<xsl:variable name="key" select="generate-id()"/>

	<tvwnode display="yes" key="{$key}" 

	relation="4" relative="{$ancestor_key}" title="{name}" />

	<xsl:apply-templates select="*">

		<xsl:with-param name="ancestor_key" select="$key"/>

		<xsl:with-param name="display" select="'no'"/>

	</xsl:apply-templates>

  </xsl:template>	

</xsl:stylesheet>

This differs from the original stylesheet in the addition of a template to catch line_item elements. It maps its own title to this node (in this case the content of the subordinate name element) then calls apply-templates again, but with display="no". This insures that no subordinate elements are consequently displayed in the tree.

What is happening here is that the XSLT style sheets have started providing some of the functionality of VB subroutines. In the case above, the subroutine affected the presentation of the data. However, you could similarly create XSLT templates that do such tasks as ordering the source data in the first place, filtering it based upon XPath queries, loading multiple streams of information in and combining them, all before presenting the data.

To put it another way, XSLT can serve as a mechanism for imposing business rules upon the XML display data. As an example, if you had a number of purchase orders that were all concatenated into a large XML stream, you could create a filter that would only present line items for items with prices greater than $1000, regardless of the item. You could similarly present only those line items for a given product, to see what kind of sales had taken place on that item. While this could be accomplished through Visual Basic routines, in fact it is much easier to use XSLT to transform the information with the appropriate filters, since this is dynamic information.