XML and Visual Basic
by Kurt Cagle
|
Pages: 1, 2
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('<',name(.),'>',text())"/>
</xsl:attribute>
</xsl:when>
<xsl:otherwise>
<xsl:attribute name="title"><xsl:value-of
select="concat('<',name(.),'>')"/></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="<purchase_order>"/>
<tvwnode display="yes" key="IDw2OT5" relation="4"
relative="ID02OT5" title="<line_items>"/>
<tvwnode display="yes" key="ID03OT5" relation="4"
relative="IDw2OT5" title="<line_item>"/>
<tvwnode display="yes" key="IDw3OT5" relation="4"
relative="ID03OT5" title="<item_number>12291"/>
<tvwnode display="yes" key="ID04OT5" relation="4"
relative="ID03OT5" title="<name>Mermaid Laptop"/>
<tvwnode display="yes" key="IDw4OT5" relation="4"
relative="ID03OT5" title="<cost>1234.45"/>
<tvwnode display="yes" key="ID06OT5" relation="4"
relative="ID03OT5" title="<description>Top
of the line Mermaid model laptop, with a 15"
superscreen running Redhat Linux v6.8."/>
<tvwnode display="yes" key="IDw6OT5" relation="4"
relative="ID03OT5" title="<count>1"/>
<tvwnode display="yes" key="ID07OT5" relation="4"
relative="IDw2OT5" title="<line_item>"/>
<tvwnode display="yes" key="IDw7OT5" relation="4"
relative="ID07OT5" title="<item_number>42583"/>
<tvwnode display="yes" key="ID08OT5" relation="4"
relative="ID07OT5" title="<name>8X DVD Player"/>
<tvwnode display="yes" key="IDw8OT5" relation="4"
relative="ID07OT5" title="<cost>199.95"/>
<tvwnode display="yes" key="ID0aOT5" relation="4"
relative="ID07OT5" title="<description>8x
speed CD and DVD player upgrade kit"/>
<tvwnode display="yes" key="IDwaOT5" relation="4"
relative="ID07OT5" title="<count>1"/>
<tvwnode display="yes" key="ID0bOT5" relation="4"
relative="IDw2OT5" title="<line_item>"/>
<tvwnode display="yes" key="IDwbOT5" relation="4"
relative="ID0bOT5" title="<item_number>98242"/>
<tvwnode display="yes" key="ID0cOT5" relation="4"
relative="ID0bOT5" title="<name>Memory"/>
<tvwnode display="yes" key="IDwcOT5" relation="4"
relative="ID0bOT5" title="<cost>43.95"/>
<tvwnode display="yes" key="ID0eOT5" relation="4"
relative="ID0bOT5" title="<description>64 MB DRAM"/>
<tvwnode display="yes" key="IDweOT5" relation="4"
relative="ID0bOT5" title="<count>4"/>
<tvwnode display="yes" key="ID0fOT5" relation="4"
relative="IDw2OT5" title="<line_item>"/>
<tvwnode display="yes" key="IDwfOT5" relation="4"
relative="ID0fOT5" title="<item_number>65192"/>
<tvwnode display="yes" key="ID0gOT5" relation="4"
relative="ID0fOT5" title="<name>12 GB Hard Drive"/>
<tvwnode display="yes" key="IDwgOT5" relation="4"
relative="ID0fOT5" title="<cost>159.95"/>
<tvwnode display="yes" key="ID0iOT5" relation="4"
relative="ID0fOT5" title="<description>12 GB
Seagate compatible drive."/>
<tvwnode display="yes" key="IDwiOT5" relation="4"
relative="ID0fOT5" title="<count>1"/>
<tvwnode display="yes" key="ID0jOT5" relation="4"
relative="IDw2OT5" title="<line_item>"/>
<tvwnode display="yes" key="IDwjOT5" relation="4"
relative="ID0jOT5" title="<item_number>41221"/>
<tvwnode display="yes" key="ID0kOT5" relation="4"
relative="ID0jOT5" title="<name>Porta-pen"/>
<tvwnode display="yes" key="IDwkOT5" relation="4"
relative="ID0jOT5" title="<cost>65.95"/>
<tvwnode display="yes" key="ID0mOT5" relation="4"
relative="ID0jOT5" title="<description>
Stylus-based pad for mouse entry."/>
<tvwnode display="yes" key="IDwmOT5" relation="4"
relative="ID0jOT5" title="<count>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('<',name(.),'>',text())"/>
</xsl:attribute>
</xsl:when>
<xsl:otherwise>
<xsl:attribute name="title">
<xsl:value-of
select="concat('<',name(.),'>')"/>
</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.