Real-World XML

Manipulate XML Data Easily with the XPath and XSLT APIs in the .NET Framework

Dino Esposito

Code download available at:XPathandXSLT.exe(166 KB)

This article assumes you're familiar with Visual Basic .NET

Level of Difficulty123

SUMMARY

XPath is emerging as a universal query language. With XPath, you can identify and process a group of related nodes in XML-based data sources. XPath provides an infrastructure that is integral to XML support in the .NET Framework. The XPath navigation model is even used under the hood of the XSLT processor. In this article, the author reviews the implementation details of the XPath navigator and the XSLT processor and includes practical examples such as asynchronous transformations, sorted node-sets, and ASP.NET server-side transformations.

Contents

XPath Expressions
The XPath Navigator
Documents, Navigators, and Readers
Querying for Nodes
Programming with XPathNavigator
Sorting the Node-Set
A Whistle-stop Tour of XSL in .NET
The XslTransform Class
Arguments and Extension Objects
Asynchronous XSLT
The <asp:xml> Tag
Conclusion

One of the key advantages of XML is that it lets you mark portions of text with tags and attributes. You tag data inside text because you plan to retrieve it later. So how do you do this? You use XPath.

Though it doesn't have an XML-based syntax, XPath is the language defined to address parts of an XML document in a compact, relatively straightforward way. More importantly, XPath defines a common syntax so you can retrieve nodes from within classes that implement the XML document object model (DOM) as well as from XSLT.

In the Microsoft® .NET Framework, the XPath query language is fully supported through the classes defined in the System.Xml.XPath namespace. The .NET Framework implementation of XPath is based on a language parser and an evaluation engine. The overall architecture of an XPath query is similar to that of a database query. Just as with SQL commands, you prepare XPath expressions and submit them to a runtime engine for evaluation. The query is parsed and executed against an XML data source. Next, you get back some information representing the resultset of the query. An XPath expression can return a node-set (that is, an ordered collection of nodes), a Boolean value, a number, or a string.

In this article, I'll show how XPath integrates with the XmlDocument class and XSLT in the .NET Framework. I'll also dig out the intricacies of the XPathNavigator class, used by the .NET Framework to walk through XML documents.

XPath Expressions

XPath is a query language specifically designed for addressing both the elements and the text of an XML document. The XPath notation is essentially declarative. Any valid expression declares a node pattern using a notation that emphasizes the hierarchical relationship between the nodes. Similar to file system paths, XPath expressions proceed from a root (an axis in the XPath terminology) to a particular set of nodes or a value in the source document. The similarity to the file system does not end here though. XPath expressions are always evaluated in the context of a node. The context node is designated by the application and represents the starting point of the query. It is not much different from the idea of the current directory.

The context of an XPath query includes, but is not limited to, a context node and a context node-set. The context node-set is the overall set of nodes processed by the query. Typically, it is a superset of the set of nodes that is actually returned to the application. An XPath context also contains position and namespace information, variable bindings, and a standard library of functions that applications can extend. Any implementation of the XPath parser provides a function library that is used to evaluate expressions. Extension functions are defined within vendor-specific XPath implementations but can also be provided by specialized and XPath-based programming APIs such as XSL Transformation and XPointer. XPath expressions generally return node-sets, but Booleans, strings, numbers, and other types are supported.

The most frequently used type of XPath expression is the location path. A location path is an expression that looks a lot like a file system path in that it can be either absolute or relative to the context node. Absolute location paths begin with the forward slash. As shown in Figure 1, a fully qualified location path is made of three pieces: an axis, a node test, and one or more predicates. The axis information defines the initial context node-set for the expression, whereas the node test is a sequence of node names that identifies a path in the node-set. A predicate is a logical expression that defines the criteria for filtering the current node-set.

Figure 1 Location Path

Figure 1** Location Path **

An XPath expression can contain any number of predicates. If no predicate is specified then all the children of the context node are returned in the query. Otherwise, the conditions set with the various predicates are logically concatenated using a short-circuited AND operator. Notice that predicates are processed in the order in which they appear so that the next predicate works on the node-set generated by the previous one.

While being processed, an XPath expression is tokenized in subexpressions called location steps and each is evaluated individually. The XPath processor is iteratively passed the subexpression and the context node-set generated at the previous step. It returns a possibly narrowed node-set that will be used as the input argument for the next subexpression. During this process, the context node, position, and size may vary while variable and function references as well as namespace declarations remain intact. Each location step is actually a location path and, as such, can be expressed in an abbreviated or fully qualified form, as appropriate. Location steps are separated by forward slashes.

In the .NET Framework, you can use XPath expressions through methods exposed by the XmlNode class on the XmlDocument class or by means of the XPathNavigator class.

The XPath Navigator

In the .NET Framework, the XmlDocument class represents the standard XML DOM (DOM Level 2 standard) as ratified by the W3C. On each child node you reach from XmlDocument, a couple of search methods are implemented. The XmlNode class provides the SelectNodes and SelectSingleNode methods that use XPath expressions to search for nodes in the document. The methods are nearly identical to similarly named methods in the COM-based MSXML library. SelectNodes returns a list of objects whereas SelectSingleNode returns only the first object that matches the search criteria. Here's how you use SelectNodes:

Dim doc As XmlDocument = New XmlDocument() doc.Load(fileName) Dim nodes As XmlNodeList nodes = doc.SelectNodes(queryString)

The SelectSingleNode method differs from SelectNodes in that it returns an individual XmlNode object. I'll have more to say about these methods in a moment.

The XmlDocument support for XPath expressions has two goals. It smoothes the transition from MSXML COM code to the .NET Framework, while giving you a built-in mechanism to search for nodes on a memory-mapped XML document. The XmlDocument query API, though, is a simple high-level wrapper. The core .NET Framework API for processing XPath expressions is built around the XPathNavigator class. The navigator is an XPath processor that works on top of any XML data store that exposes the IXPathNavigable interface. Rendered through the interface defined in the XPathNavigator class, the navigator parses and executes expressions using the Select method. Unlike the XmlDocument methods, the navigator accepts expressions both as plain text and through precompiled objects. You can programmatically access the XPathNavigator object either from the XmlDocument class or the XPathDocument class. Figure 2 illustrates the two ways to access XPath functions in the .NET Framework.

Figure 2 Accessing XPath Functions

Figure 2** Accessing XPath Functions **

The following code snippet shows how to create an XPathNavigator from an XML document and execute an XPath query:

Dim doc As XPathDocument Dim nav As XPathNavigator Dim iterator As XPathNodeIterator doc = New XPathDocument(fileName) nav = doc.CreateNavigator() iterator = nav.Select(queryString) While iterator.MoveNext() ' nav points to the node subtree End While

The navigator returns an XPath iterator object. Iterators are nothing more than specialized enumerator objects which are used to move through the returned node-set. I'll discuss iterators shortly.

Documents, Navigators, and Readers

Prior to the advent of the .NET Framework, processing an XML file was a matter of working with documents rendered according to the SAX or the XmlDocument specification.

The XPath processor, which is the engine that parses and executes XPath queries, is built under the covers of the XPathNavigator class. As shown in Figure 2, XPath calculations are always delegated to a navigator, regardless of the high-level caller API. A navigator works on top of a particular data store, which is typically an instance of the XPathDocument class. Other data stores can be used as well as long as the data store class implements the IXPathNavigable interface, like this:

public interface IXPathNavigable { XPathNavigator CreateNavigator(); }

Besides the XPathDocument class, examples of data stores include XmlDocument and XmlDataDocument.

A data store is responsible for providing a navigator to explore in-memory XML content. An XPathNavigator implementation is always store specific and is built by inheriting from the XPathNavigator abstract class. Although in practice you always program navigators through the common reference type of XPathNavigator, each data store class has its own navigator object. They are internal, undocumented classes, programmatically inaccessible, and are often implemented in fairly different ways. Figure 3 lists the real navigator classes used by the three XPath data stores defined in the .NET Framework. The document-specific navigator exploits the internal layout of the document class in order to provide the navigation API.

Figure 3 Navigator Classes

Data Store Class Corresponding Internal Navigator Class
XPathDocument System.Xml.XPath.XPathDocumentNavigator
XmlDocument System.Xml.DocumentXPathNavigator
XmlDataDocument System.Xml.DataDocumentXPathNavigator

The XPathDocument class provides a highly optimized and read-only in-memory store for XML documents. Specifically designed to implement the XPath data model, this class does not provide any identity for nodes. It simply creates an underlying tree of node references to let the navigator operate quickly and effectively. The internal architecture of the XPathDocument class looks like a linked list of node references. Nodes are managed through an internal class (XPathNode) that represents a small subset of the XmlNode class (see Figure 2).

Compared to XPathDocument, the XmlDocument class provides read-write access to the nodes of the underlying XML document. In addition, each node can be individually accessed.

The XmlDocument class also provides the ability to create a navigator object, as shown here:

Dim doc As XmlDocument = New XmlDocument() doc.Load(fileName) Dim nav As XPathNavigator = doc.CreateNavigator()

The XmlDocument's navigator class implements the IHasXmlNode interface. This interface defines one method, GetNode:

public interface IHasXmlNode { XmlNode GetNode(); }

Using this method, callers can access and interrogate the currently selected node on the XmlDocument based on the XPathNavigator's position. This feature is simply impossible to implement for navigators based on XPathDocument because it does not expose the internal structure via the XmlNode class like the XmlDocument class does. This is by design. The XPathDocument minimizes the memory footprint and does not provide for node identity.

Because the GetNode method is implemented on the XPathNavigator class on the XmlDocument, callers can take advantage of it using a type cast, as shown in the following code snippet:

Dim doc As XmlDocument = New XmlDocument() doc.Load(fileName) Dim nav As XPathNavigator = doc.CreateNavigator() Dim i As XPathNodeIterator = nav.Select(query) Dim node As XmlNode = CType(i.Current, IHasXmlNode).GetNode()

At this point, the caller program has gained full access to the node and can read and update it at will.

Finally, the XmlDataDocument class is an extension of XmlDocument which is aimed at allowing the manipulation of a relational DataSet through XML. This is a neat example of the fact that the .NET Framework navigation API can be applied to XML-based data as well as XML-like (data that's structured like XML, but isn't XML) data.

If you look at the MSDN® documentation for XPath navigators, you'll see that a navigator reads data from XML-based data stores in a cursor-like fashion (forward and backward) and provides read-only access to the underlying data. In addition, it holds information about the current node and advances the internal pointer using a variety of move methods. When the navigator is positioned on a given node, all of its properties reflect the value of that node. This, of course, resembles XML readers, which I looked at in an article in the May 2003 issue of MSDN Magazine. So what's the difference between navigators and readers?

Navigators and readers are different animals. Readers are analogous to fire-hose cursors, providing basic read-only, forward-only movement. Navigators are read-only as well but they provide a significantly richer set of move methods including both forward and backward options. Navigators also provide several select methods to fine-tune the search. Readers are lower-level tools you can use to read XML-based or XML-like data, as well as build in-memory data structures. XML readers can be used to build the in-memory data structures that navigators rely upon.

Querying for Nodes

Suppose that you need to implement an XPath query in a .NET Framework-based application. Should you use XPath navigators or are you better off sticking to the XmlDocument's nodes interface? The XmlNode's SelectNodes method internally employs a navigator object to retrieve the list of matching nodes. The return value of the navigator's Select method is then used in order to initialize an internal node list object of type XPathNodeList, defined in the System.Xml.XPath namespace. As you may have already guessed, this class inherits from XmlNodeList which is a documented class. In addition, unlike SelectNodes, navigators can fully exploit compiled expressions.

The SelectNodes method always accepts the XPath expression as plain text. The string is then passed verbatim to the navigator. In only one case does the underlying navigator receive a compiled expression. If you use the overload of the SelectNodes method that handles namespace information, the XPath expression is first compiled and then passed to the processor. The overload is prototyped as follows:

Function SelectNodes( _ xpathExpr As String, _ nsm As XmlNamespaceManager) _ As XmlNodeList

The advantage of using compiled expressions becomes palpable when you frequently reuse the expression in the session, and they can be namespace aware. The XmlNamespaceManager is the class that allows the user to specify the prefix of the namespace to bind.

The SelectSingleNode method works as a special case of SelectNodes in that it returns only the first element of the returned node-set. Unfortunately, up until now, the implementation of SelectSingleNode has not been particularly efficient. If you need to locate only the first matching node, then calling SelectSingleNode or SelectNodes is nearly identical. Moreover, if you need to squeeze out every little bit of performance, you're probably better off using SelectNodes. The following pseudocode illustrates the current implementation of SelectSingleNode:

Function SelectSingleNode(xpathExpr As String) As XmlNode Dim nodes As XmlNodeList = SelectNodes(xpathExpr) Return nodes(0) End Function

The method internally calls SelectNodes and returns the first node that is a match. Note, however, that XmlNodeList is built dynamically—the next node is searched for only when requested.

A better way to query for a single node would be to pass SelectNodes with a more precise XPath expression that returns a single node. The idea is to avoid generic wildcard expressions like this:

NorthwindEmployees/Employee

You should place a stronger filter on the XPath expression so it returns a properly sized subset of nodes. To get only the first node, add an extra predicate that stops the query after the first match:

NorthwindEmployees/Employee[position() = 1]

This is an XPath best practice in general and is not due to the .NET Framework implementation in particular. The approach could be refined and generalized to size the node-set as is appropriate. This query string shows how to get the first n nodes that match:

NorthwindEmployees/Employee[position() < n+1]

I would suggest using XmlNode's SelectNodes instead of an instance of XPathNavigator to execute XPath queries when you need to perform operations on the selected nodes that assume some form of node identity. If you need to further manage the node as an instance of the XmlNode class, then using SelectNodes would simplify your code.

Programming with XPathNavigator

Let's learn a bit more about the programming interface of XPathNavigator. In general, regardless of the application-level API, the sequence of steps necessary to execute XPath queries on an XML data source is roughly the same:

  1. Get a reference to an XPath-enabled document class (for example, an instance of an XPathDocument or XmlDocument class).
  2. Create a navigator object for the specified data store.
  3. Optionally, precompile the XPath expression if you plan to reuse it later.
  4. Call the navigator's Select method to action.

The programming interface of the navigator object is defined in the XPathNavigator abstract class. Although you will mostly use the navigator object to execute XPath queries, it is worth noting that the XPathNavigator class represents a more general-purpose component. The navigator is a generic interface that acts as a cursor-like explorer of any data store that exposes its content as XML. Although functionally similar to an XML reader, the navigator is not as fast and effective for simple reads since it is a tree navigator and specializes in retrieval. If you just need to read an XML document, use an XML reader; if you need to perform queries, use a navigator. Remember, right now the navigator works on a fully memory-mapped data source.

Functionally speaking, the XPathNavigator class is not much different from a pseudo-class that just groups together all the methods needed to navigate the document contents. The big difference is in the fact that XPathNavigator is a distinct component that's completely decoupled from the document class. In other words, XPathNavigator represents an XML view of some store of data that's been mapped to an XML data model.

Figure 4 enumerates the properties available on the XPathNavigator class. Like XML readers and the XMLDocument class, XPathNavigator employs name tables to store repeated strings more efficiently. The set of properties looks like the subset of properties that characterize the current node in the XmlTextReader class.

Figure 4 XPathNavigator Properties

Property Description
BaseURI Gets the base URI of the current node
HasAttributes Indicates whether the current node has any attributes
HasChildren Indicates whether the current node has any child nodes
IsEmptyElement Indicates whether the current node is empty (<node />)
LocalName Name of the current node without the namespace prefix
Name Gets the fully qualified name of the current node
NamespaceURI Gets the URI of the namespace associated with the current node
NameTable Gets the name table associated with the navigator
NodeType Gets the type of the current node
Prefix Gets the namespace prefix associated with the current node
Value Returns a string denoting the value of the current node
XmlLang Gets the xml:lang scope for the current node

It is worth repeating that the XPathNavigator's Select method returns an iterator object whose current element is referenced back as a navigator (XPathNavigator class). To access and process node information, you can only use the properties of the navigator. The properties in Figure 4 are read-only and, more importantly, don't map to an instance of the XmlNode class. If you need to manipulate the node as an XmlNode object (for example, to apply changes) make sure that you use XmlDocument as the data store class and then cast the current element of the iterator to IHasXmlNode. From the reference type, IHasXmlNode calls the GetNode method, which returns the XmlNode instance of the underlying node. In all other cases, access to nodes is read-only.

The navigator object provides a rich set of methods that I classify in three main groups based on their functionality: select, move, and miscellaneous. The following code snippet selects the descendants of a node. The code to get the ancestors is nearly identical:

Dim doc As XPathDocument = New XPathDocument(fileName) Dim nav As XPathNavigator = doc.CreateNavigator() nav.SelectDescendants(nodeName, nsUri, selfIncluded)

SelectDescendants takes the local name of the node and selects the descendants. The nsUri variable indicates the namespace URI of the descendant nodes, if any. The selfIncluded Boolean variable is a flag that indicates whether or not the context node should be included in the node set.

Figure 5 contains the list of XPathNavigator's move methods. You can jump in either direction—forward or backward, from sibling to sibling, and according to namespace restrictions. As you may have noticed, there are three different groups of move methods: for element, attributes, and namespace nodes. Only the MoveTo and MoveToRoot methods can be called on any node, irrespective of the type. In addition, attributes and namespaces also have methods to return their value: GetAttribute and GetNamespace. When selected, the navigator's Name property returns the namespace prefix. The Value property returns the URI.

Figure 5 XPathNavigator Move Methods

Method Description
MoveTo Moves to the same position as the specified XPathNavigator object
MoveToAttribute Moves to the specified attribute of the current node
MoveToFirst Moves to the first sibling of the current node
MoveToFirstAttribute Moves to the first attribute of the current node
MoveToFirstChild Moves to the current node's first child
MoveToFirstNamespace Moves to the first namespace in the current element node
MoveToId Moves to the node with an attribute of type ID whose value matches the given string
MoveToNamespace Moves to the namespace node with the specified prefix in the current element node; any namespace node is seen as an attribute node with the xmlns name; the real name of the namespace node is the prefix
MoveToNext Moves to the next sibling of the current node
MoveToNextAttribute Moves to the next attribute of the current node
MoveToNextNamespace Moves to the next namespace in the current element node
MoveToParent Moves to the parent of the current node
MoveToPrevious Moves to the previous sibling of the current node
MoveToRoot Moves to the root node of the document

Figure 6 groups all the other methods defined on the XPathNavigator class. A few of these methods have to do with XPath expressions. An XPath expression is a string that represents a location path, though it is a bit more than a plain command string. It has a surrounding context that the XPathExpression class encapsulates. The context of an expression includes the return type and the namespace information. The XPathExpression class is not publicly creatable. To get a new instance of it, you must take an XPath string expression and compile it into an XPathExpression object. The following code snippet shows how to compile an expression and display its expected return type:

Dim expr As XPathExpression = nav.Compile(xpathExpr) Console.WriteLine(expr.ReturnType.ToString()) nav.Select(expr)

Figure 6 More XPathNavigator Methods

Method Description
Clone Clones the navigator and returns a new object with the same current node
ComparePosition Compares the position of the current navigator with the position of the specified XPathNavigator
Compile Compiles an XPath expression
Evaluate Evaluates the given XPath expression and returns the result
GetAttribute Gets the value of the specified attribute, if such an attribute exists on the current node
GetNamespace Gets the URI of the specified namespace prefix, if such a namespace exists on the current node
IsDescendant Indicates whether the specified navigator is a descendant of the current one; a navigator is a descendant of another navigator if it is positioned on a descendant node
IsSamePosition Indicates whether the current navigator is at the same position as the specified navigator
Matches Determines whether the current node matches the specified XPath expression

A compiled XPath expression can be consumed by the Select, Evaluate, and Matches methods. The term "compile" here does not mean that the XPath expression becomes an executable. More simply, the action of compiling must be seen as the process that produces an object by collecting various pieces of information. An expression can return values of types other than a node-set. In this case, you use the Evaluate method to evaluate the expression and then cast the generic object returned to the specific type. Select is a more specific method in that it assumes that the return type is a node-set and inserts the nodes in an iterator.

Sorting the Node-Set

An interesting extension built into the XPathExpression class is the ability to sort the node-set before it gets passed back to the caller. To add a sorting algorithm, you call the AddSort method on the XPathExpression object. AddSort is available in two flavors:

Sub AddSort(expr As Object, comparer As IComparer) Sub AddSort(expr As Object, order As XmlSortOrder, _ caseOrder As XmlCaseOrder, lang As String, _ dataType As XmlDataType)

The expr argument denotes the sort key. It can be a string representing a node name or another XPathExpression object that evaluates to a node name. In the first overload, the comparer argument refers to an instance of a class that implements the IComparer interface. The interface supplies a Compare method, which is actually used for comparing a pair of values. Use this overload if you need to specify a custom algorithm to sort nodes.

The second overload always performs a numeric or text comparison according to the value of the dataType argument. In addition, you can specify a sorting order (ascending or descending) and even the sort order for uppercase and lowercase letters (or you can ignore capitalization completely by using the value XmlCaseOrder.None). Finally, the lang argument specifies which language to use for comparison. The language name should also specify the locale. For example, to indicate U.S. English, it is preferable to use "us-en," rather than simply "en."

Let's delve a bit deeper into the IComparer interface. To sort arrays of objects, the .NET Framework provides a few predefined comparer classes including Comparer and CaseInsensitiveComparer. The comparer class compares objects (mostly strings) with respect to case. CaseInsensitiveComparer does the same but ignores case. To use both classes in your code, be sure to import the System.Collections namespace. The Comparer class has no public constructor but provides a singleton instance through the Default static property. For example:

expr.AddSort("lastname", Comparer.Default);

If needed, you can also create your own comparer class. This code shows a rather trivial Visual Basic® .NET implementation:

Class MyOwnStringComparer Implements IComparer Public Function Compare(x As Object, y As Object) _ As Integer Implements IComparer.Compare Dim strX As String = CType(x, String) Dim strY As String = CType(y, String) Return String.Compare(strX, strY) End Sub End Class

The Compare method should return 0 if the two strings are equal; it returns a value greater than 0 if x precedes y, and a negative value if y precedes x.

The class can also be defined within the body of your application and does not require a separate assembly. This code associates a custom comparer with an XPath expression:

Dim comp As MyOwnStringComparer = New MyOwnStringComparer() expr.AddSort("lastname", comp)

Figure 7 shows an application of this technique. After creating a navigator for an XML doc, the sample console app compiles the expression to use. The assumed data source has this schema:

<MyDataSet> <NorthwindEmployees> <Employee> <employeeid>...</employeeid> <lastname>...</lastname> <firstname>...</firstname> <title>...</title> </Employee> ••• </NorthwindEmployees> </MyDataSet>

Sorting by a single node is easy—you just pass the node name to the AddSort method. It's more complicated to sort by multiple fields. The idea is that you indicate a comma-separated list of node names. However, the argument string must be an XPath expression that evaluates to the comma-separated list of node names. If you simply specify the sorting key as, say, "title, lastname" you'll get a runtime error because the XPath processor mistakes it for the actual node name. What's really needed is an expression that the XPath processor would translate at run time into the desired title and lastname, like this:

Dim sortKey As String = "concat(concat(title, ','), lastname)"

The concat keyword identifies one of the predefined helper functions that XPath implementations provide.

Figure 7 Sort and Compare

Imports System Imports System.Xml Imports System.Xml.XPath Imports System.Collections Imports Microsoft.VisualBasic Class SortNodeSetApp Public Shared Sub Main(args As String()) Try ' Must sort? Dim mustSort As String = "" If args.Length > 0 Then mustSort = args(0) End If ' Get the XML file to work on (assumes a certain schema) Dim fileName As String = "data.xml" ' Create the XPath navigator Dim doc As XPathDocument = New XPathDocument(fileName) Dim nav As XPathNavigator = doc.CreateNavigator() ' Compile a standard XPath expression Dim expr As XPathExpression expr = nav.Compile("/MyDataSet/NorthwindEmployees/Employee") ' Set the sort key (title + lastname) Dim sortKey As String = "concat(concat(title, ','), lastname)" ' Add sort information (use the default comparer) If mustSort = "/sort" Then expr.AddSort(sortKey, Comparer.Default) ' As an alternative, use custom comparer ' Dim comp As MyOwnStringComparer = ' New MyOwnStringComparer() ' expr.AddSort(sortKey, comp) ' Also use the other AddSort overload expr.AddSort(sortKey, ' XmlSortOrder.Ascending, XmlCaseOrder.None, "", ' XmlDataType.Text) End If ' Perform the query Dim iterator As XPathNodeIterator = nav.Select(expr) ' Output the results Console.WriteLine("") ' Iterate on the node set While iterator.MoveNext() ' The iterator scrolls the nodes in the node set. ' The navigator lets you drill down into each node's ' subtree. Must clone the navigator to avoid misplacing the ' main navigator Dim nav2 As XPathNavigator = iterator.Current.Clone() ' The output has the form: ' <Employee> ' <employeeid>...</employeeid> ' <lastname>...</lastname> ' <firstname>...</firstname> ' <title>...</title> ' </Employee> ' : Dim buf As String = "" ' Select the <employeeid> node and read the current value nav2.MoveToFirstChild() buf += nav2.Value + ". " ' Select the <lastname> node and read the current value nav2.MoveToNext() buf += nav2.Value ' Select the <firstname> node and read the current value nav2.MoveToNext() buf += ", " + nav2.Value ' Select the <title> node and read the current value nav2.MoveToNext() buf += " " + Chr(9) + "[" + nav2.Value + "]" ' Write out the results Console.WriteLine(buf) End While Catch e As Exception Console.WriteLine("Error:" + vbTab + "{0}" + vbCrLf, e.Message) End Try Return End Sub ' Provides a custom comparer Class MyOwnStringComparer Implements IComparer Public Function Compare(x As Object, y As Object) As Integer Implements IComparer.Compare Dim strX As String = CType(x, String) Dim strY As String = CType(y, String) Return String.Compare(strX, sTry) End Function End Class End Class

In Figure 7, you also see how to use iterators effectively. A key point to notice when working with XPath iterators is that your code passes over all the nodes in the node-set one at a time.

You'll probably need to drill down into the subtree of each of these nodes. To do so, first clone the navigator to avoid misplacing the main navigator, thus compromising the outermost loop:

Dim iterator As XPathNodeIterator = nav.Select(expr) While iterator.MoveNext() Dim nav2 As XPathNavigator = iterator.Current.Clone() nav2.MoveToFirstChild() ••• nav2.MoveToNext() ••• End While

The Current property on the iterator returns the current node in the node-set. It evaluates to an instance of the XPathNavigator class and can be cloned using the Clone method.

A Whistle-stop Tour of XSL in .NET

An XML transformation is a user-defined algorithm that attempts to express the semantics of a given document in another (equivalent) syntax. The transformation process consists of the rendering of the source document based on the structure of the stylesheet. The stylesheet is a declarative and user-defined document that contains the set of rules that will be used to transform one document into another document. XSL refers to a meta-language designed for expressing stylesheets for XML documents. XSL files were originally conceived as the XML counterpart of HTML cascading stylesheets (CSS). In light of this, XSL was designed as an extensible and user-definable tool to render an XML document in HTML for display purposes. The growing complexity of stylesheets, as well as the advent of XML schemas, led to XSLT. Today, XSL is just a blanket term for a number of derived technologies that altogether better qualify and implement the original idea of styling XML documents. The various components that fall under the umbrella of XSL are the actual software entities that you use in your code: XSLT, XPath, and XSL Formatting Objects (XSLFO).

An XSLT program is a generic set of transformation rules whose output can be any text-based language, including HTML, RTF, and so on. As mentioned, XPath is a query language that XSLT programs employ to select specific parts of the source XML document. The result of XPath expressions is then parsed and elaborated by the XSLT processor. Normally, the XSLT processor works sequentially on the source document, but hands it off to XPath if access to a particular group of nodes is requested.

In the .NET Framework, the core class for XSLT is XslTransform. Located in the System.Xml.Xsl namespace, the class implements the XSLT processor. You make use of the class in two steps: first you load the stylesheet in the processor and then you apply transformations to as many source documents as you need. The XslTransform class only supports the XSLT 1.0 specification. The C# code in Figure 8 implements a command-line XSLT transformer. It takes three command-line arguments—the XML source, the XSLT stylesheet, and the output file—to set up the processor and save the results of the transformation to the output file.

Figure 8 XSLT Transformer

using System; using System.Xml; using System.Xml.Xsl; class QuickXslTransformer { public QuickXslTransformer(string source, string stylesheet, string output) { XslTransform xslt = new XslTransform(); xslt.Load(stylesheet); xslt.Transform(source, output); } public static void Main(string[] args) { try { QuickXslTransformer o; o = new QuickXslTransformer(args[0], args[1], args[2]); } catch (Exception e) { Console.WriteLine("Unable to apply the XSLT transformation."); Console.WriteLine("Error:\t{0}", e.Message); Console.WriteLine("Exception: {0}", e.GetType().ToString()); } return; } }

The XslTransform Class

The XslTransform class supplies two methods specific to its activity—Load and Transform. The class is guaranteed to operate in a thread-safe way only during transform operations. In other words, although an instance of the class can be shared by multiple threads, only the Transform method can be safely called from multiple threads. The Load method is not thread-safe. The Transform method reads the shared state and can run concurrently from multiple threads. Figure 9 shows a portion of the internal architecture of the class. After loading the stylesheet, the XSLT processor needs to modify its state to reflect the loaded document. The operation does not occur atomically within the virtual boundaries created by a lock statement. As a result, concurrently running threads could, in theory, access the same instance of the XSLT processor and break the data consistency. The load operation is thread-sensitive because it alters the global state of the object.

Figure 9 XSL Transform Class

Figure 9** XSL Transform Class **

In version 1.0 of the .NET Framework, the XslTransform class has a link-demand permission set attached. A link demand specifies which permissions direct callers must have in order to run the code. Caller rights are checked during just-in-time compilation:

[PermissionSet(SecurityAction.LinkDemand, Name="FullTrust")] public sealed class XslTransform { ••• }

The permission set attribute for the XslTransform class is expressed by name and points to one of the built-in permission sets—FullTrust. What does this mean to you? Only callers (direct callers are involved with the check, not caller's callers) with fully trusted access to all the local resources can safely call into the XSLT processor. For example, if you call the XSL processor over a network share, a security exception is raised. In version 1.1 of the .NET Framework, the permission set has been removed.

In the overall behavior of the .NET Framework XSLT processor, three stages can be clearly identified: the load of the stylesheet document, the setup of the internal state, and the transformations. The first two stages occur within the context of the Load method. Of course, you cannot call the Transform method before a previous call to Load has successfully terminated. The Load method always works synchronously, so that when it returns you can be sure that the loading step has actually been completed. You will not receive any return value that denotes the failure or success of the operation. However, whenever something goes wrong with the Load method, some exceptions are thrown. In particular, you will get an exception of type FileNotFoundException if you are pointing to a missing stylesheet and a more generic exception of type XsltCompileException if the XSLT script contains some errors. An XsltCompileException exception provides you with the line position and number where the error occurred in the stylesheet.

The input stylesheet can be loaded from four different sources—URL, XML reader, XPathDocument, and XPathNavigator. Regardless of the source, as its first action the Load method expresses the source as an XPathNavigator object. The stylesheet must be compiled and considering the compiler's architecture, a navigator is an extremely efficient object. "Compiling" is a process that simply excerpts some information out of the original stylesheet and stores it in handy data structures for further use. The whole set of these data structures is said to be the state of the XSLT processor. Figure 10 shows the flow of the Load method.

Figure 10 Load Method Flow

Figure 10** Load Method Flow **

The stylesheet compiler populates three internal data structures with the data read from the source. The object referenced as the compiled stylesheet object represents a sort of index of the stylesheet content. The other two objects are tables containing compiled versions of the XPath queries to execute and the actions that the various templates require.

The Transform method takes at least two explicit arguments—the source XML document and the output stream—plus a couple of implicit parameters. The compiled stylesheet object is, of course, one of the implicit input arguments. The second implicit parameter is the content of the XslTransform's XmlResolver property, which is designed to resolve external resources. The Transform method can also take a third explicit argument, an object of class XsltArgumentList. The argument contains the namespace-qualified arguments used as input to the transformation process.

The XML source document is normalized as an XPathNavigator and passed in down to the XSLT processor. Interestingly, the Transform method has two types of overloads. Some of the overloads work as void methods and simply write to the specified stream. Others work as functions and specifically return an XML reader object. As I'll discuss in a moment, this feature offers a very interesting opportunity: implementing asynchronous XSLT transformations. In Figure 11, you can see the flow of execution for the Transform method.

Figure 11 Transform Method

Figure 11** Transform Method **

The Transform method also lets you pass arguments to the stylesheet using an instance of the XsltArgumentList class. When you pass arguments to an XSLT script in this way, you cannot specify which template call will actually use those arguments. You just pass arguments globally to the XSLT processor. The internal modules responsible for processing templates will then read and import those arguments as appropriate:

XsltArgumentList args = new XsltArgumentList(); args.AddParam("MaxNumOfRows", "", 7);

The AddParam method creates a new entry in the argument list. The method requires three parameters: the name of the parameter, the namespace URI (if the name is qualified by a namespace prefix), and an object representing the actual value. Regardless of the CLR type you use in order to pack it into the argument list, the parameter value must correspond to a valid XPath type: string, Boolean, number, node-fragment, and node-set. The number corresponds to a double type, whereas node-fragments and node-sets are equivalent to XPathNavigators and XPath node iterators.

Arguments and Extension Objects

XsltArgumentList is not a collection-based class. It doesn't derive from a collection class or implement any of the typical list interfaces like IList or ICollection. The XsltArgumentList class is built around a couple of hash tables—one to hold XSLT parameters and one to gather extension objects. An extension object is a living instance of a .NET object that you can pass as an argument to the stylesheet. For example, XSLT scripts can be extended in various ways to achieve functionality that the embedded XPath library doesn't supply. The XslTransform class supports the <xsl:eval> instruction, which allows you to insert VBScript into a stylesheet. An alternative for embedding custom code into the script is the <msxsl:script> element. This new instruction supports managed languages and provides access to the entire .NET Framework:

<msxsl:script language = "language" implements-prefix = "prefix"> ••• </msxsl:script>

Supported languages include C#, Visual Basic, and JScript®. The language attribute is not mandatory and, if not specified, defaults to JScript. The implements-prefix attribute is mandatory, however. It declares a namespace and associates the user-defined code with it. The namespace must be defined somewhere in the stylesheet. In addition, to make use of the <msxsl:script> instruction, the stylesheet must include the following namespace:

xmlns:msxsl=urn:schemas-microsoft-com:xslt

To define a simple script, you declare the extra namespaces in the stylesheet's root. For example:

<xsl:stylesheet version="1.0" xmlns:xsl="https://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" xmlns:dino="urn:dino-scripts">

This declaration is necessary to call the <msxsl:script> instruction. The namespace simply groups some user-defined scripts under a single roof. The prefix "dino" is now necessary to qualify any calls to any functions defined in an <msxsl:script> block. In the body of the stylesheet, you call any function defined in an <msxsl:script> block:

<xsl:template match="lastname"> <TD style="border:1px solid black"> <xsl:value-of select="dino:PrepareName(., ../firstname)" /> </TD> </xsl:template>

If you enclose parameters in quotation marks, they will be treated as literal values. To ensure that the managed function receives node values, use the same expressions you would use with the select attribute of an <xsl:value-of> instruction.

The <msxsl:script> instruction is not always the best solution. It supports only a limited set of namespaces which are imported as part of the XSLT processor. For example, you cannot use the System.Data namespace. Script is good for one-off operations such as parsing strings or retrieving the current time. For a more powerful alternative, consider extension objects.

An extension object is simply a managed class that has some public methods. The only requirement is that callable methods accept arguments that are of, or can be coerced to, XPath types. Unlike embedded scripts, which are natively defined in the body of the stylesheet, extension objects are external resources that must be plugged into the stylesheet in some way. Extension object arguments must be passed as shown in the following code:

ExtensionObject o = new ExtensionObject(); // *** set properties on the object if needed XsltArgumentList args = new XsltArgumentList(); args.AddExtensionObject("urn:dino-objects", o); XslTransform xslt = new XslTransform(); xslt.Transform(doc, args, writer);

In the XSLT script, you reference methods on the extension object as you would with an embedded script. In the following code snippet, DoSomething is a method on an extension object associated with the namespace prefixed by "dino":

<xsl:template match="lastname"> <TD style="border:1px solid black"> <xsl:value-of select="dino:DoSomething(., ../firstname)" /> </TD> </xsl:template>

Methods of extension objects are processed as static method calls, meaning that if you have multiple objects with the same method name you are better off using different namespaces.

Using extension objects is preferable to using embedded scripts for at least two reasons. First, extension objects provide much better code encapsulation, not to mention the possibility of class reuse and the fact that any managed type can be used. Second, you end up with more compact, layered stylesheets that benefit from more seamless code maintenance.

Asynchronous XSLT

The Transform method has a couple of overloads that return an XML reader:

XmlReader Transform(XPathNavigator input, XsltArgumentList args); XmlReader Transform(IXPathNavigable input, XsltArgumentList args);

The signature and the behavior of these overloads are slightly different from the other overloads. First, the input document must be an XPathNavigator or an XPathDocument. More importantly, the methods do not accept any argument representing the output stream. In fact, the output of the transformation process is not written out to a stream, but rather created in a stream and returned to the user through an XML reader.

The overall XSLT process in the .NET Framework works by creating an intermediate data structure (the input navigator) in which the content of the stylesheet is used as the underlying foundation. Any <xsl> tag found in the stylesheet source is replaced with expanded text or any sequence of calls that results from embedded templates. The final output looks like a compiled program in which direct statements are interspersed with calls to subroutines. These statements are called output records, while templates play the role of subroutines. When the Transform method gets an output stream to write to, the XSLT processor loops through all the records and flushes the text into the stream. If an XML reader has been requested, the processor creates an instance of an internal reader class and returns that to the caller. No transformation is performed until the caller explicitly asks to read the cached output records (see Figure 12).

Figure 12 XSL Transformation Process

Figure 12** XSL Transformation Process **

When the Transform method returns, the reader is in its initial state, meaning that it is not yet initialized for reading. Each time you pop an element from the reader, a new output record is properly expanded and returned. In this way, you have total control over the transformation process and can implement a number of fancy features. For example, you could provide feedback to the user, discard nodes based on runtime conditions and user roles, or cause the process to happen asynchronously on a secondary thread. For an example of asynchronous transformation, see the code download for this article.

The <asp:xml> Tag

Before I conclude, I want to review a special ASP.NET server control identified by the <asp:xml> tag. The control is the declarative counterpart of the XslTransform class. You use the XML server control to embed XML documents in a Web page. The control is handy when you need XML data islands for the client to consume. Data islands are XML data referenced or included in an HTML page. The XML data can be included inline within the HTML or stored in an external file. By combining this control's ability with a stylesheet that does browser-specific transformations, you can transform server-side XML data into browser-proof HTML. For example, the following code embeds the content of the specified XML file, as transformed by the stylesheet in the page:

<div> <asp:xml runat="server" id="xmldata" documentsource="data.xml" transformsource="ie5.xsl" /> </div>

The XML server control can be configured programmatically and accepts the source XML through a string (DocumentContent property) as well as through an XmlDocument object (Document property). The transformation element can be provided through a URL to an XSL file or through an instance of the XslTransform class (Transform property). If needed, you can also indicate arguments using the TransformArgumentList property. This component, along with the HttpBrowserCapabilities class, provides a great solution for a common need—data-driven Web pages and browser-specific output.

Notice that only well-formed XML data can be used with the <asp:xml> control. More simply, if you need to flush out the content of any file, you can use the Response.WriteFile method.

Conclusion

In this article, I analyzed XPath as the language of choice for performing XML queries in managed applications and discussed several aspects of its implementation. In the .NET Framework, the XPath runtime provides a common infrastructure for other pieces of the puzzle—the first of which is XSLT. I also examined key aspects of the XSLT processor and supplied a few interesting applications of it such as asynchronous processing and the ASP.NET control.

For background information see:
Applied XML Programming for Microsoft .NET by Dino Esposito (Microsoft Press, 2002)
Essential XML Quick Reference: A Programmer's Reference to XML, XPath, XSLT, XML Schema, SOAP, and More by Aaron Skonnard and Martin Gudgin (Addison-Wesley, 2001)
XPathNavigator over Different Stores
MSDN Web Services Developer Center

Dino Esposito is an instructor and consultant based in Rome, Italy. He is the author of Building Web Solutions with ASP.NET and ADO.NET and Applied XML Programming for Microsoft .NET, both from Microsoft Press (2002). Reach Dino at dinoe@wintellect.com.