The XSL style sheet and the XML source can be loaded from a variety of sources, including local disk files and remote URLs. You can’t load style sheets and source documents from a stream, but because you can easily obtain an XML reader from a stream, a workaround is quickly found. Whatever the input format, the content is transformed into an XPath navigator object immediately after reading.
In light of this, passing style sheet and XML source data directly as XPath documents or navigators is advantageous from two standpoints: you save conversion time, and you work with objects whose internal storage mechanism is lighter and more compact.
Choosing optimized forms of storage like XPath documents binds you to a read-only manipulation of the data. If you need to edit the document before a transformation is performed, load it into an XmlDocument object and apply all the changes. When you have finished, pass the XmlDocument object to the XslTransform class. As you’ll recall from Chapter 6, XmlDocument implements the IXPathNavigable interface and as such can be used with the Transform method.
The Load and Transform methods have several overloads each. In all this richness of call opportunities, not all possible combinations of input and output channels are always supported. For example, you can load the source document from a URL, but only if you output to another URL or disk file. Likewise, if you want to transform to a text writer, you can’t load the source from a file. Table 7-6 and Table 7-7 provide a quick-access view of the available overloads.
Return Type | Style Sheet Source | XML Resolver |
---|---|---|
void | File or URL | No |
void | XPath document | No |
void | XPath navigator | No |
void | XML reader | No |
void | File or URL | Yes |
void | XPath document | Yes |
void | XPath navigator | Yes |
void | XML reader | Yes |
The interface of the Load method is fairly regular. It always returns void, and it supports four reading media, with or without an XML resolver object.
The programming interface of the Transform method is much less regular. The overloads that return an XML reader work only on XPath documents or navigators. The overload that manages URLs or files is an exception, perhaps provided for the sake of simplicity. The remaining overloads are grouped by the type of the output media: stream, text, or XML writer. For each of them, you can have a source XML document read from an XPath document or an XPath navigator.
The style sheet and the source XML document are two equally important arguments for the XSLT processor. The XslTransform programming interface requires that you indicate them in different steps, however. In doing so, the accent goes on a particular use—transforming multiple documents using the same style sheet.
Although optimized for a particular scenario, such a design doesn’t tax those programmers who use the style sheet for a single transformation. In this case, the only, and very minimal, drawback is that you have to write three lines of code instead of one! Look at the following class. It provides a static method for performing XSLT transformations. It doesn’t explicitly provide for style sheet reuse, but it does save you two lines of code!
public class QuickXslt { public static bool Transform( string source, string stylesheet, string output) { try { XslTransform xslt = new XslTransform(); xslt.Load(stylesheet); xslt.Transform(source, output); return true; } catch (Exception e) { return false; } }
The Transform method shown in the preceding code also catches any exceptions and flattens them into a Boolean value. Using this global method is as easy as writing the following code:
public static void Main(string[] args) { bool b = QuickXslt.Transform(args[0], args[1], args[2]); Console.WriteLine(b.ToString()); }
By design, the static Transform method accepts only disk files or URLs.
Tip
By passing an XML reader to the XslTransform class’s Load and Transform methods, you can load both the style sheet and the source document from an XML subtree. In this case, in fact, the XslTransform class will start reading from the reader’s current node and continue through the entire subtree.
Another interesting consideration that applies to XSLT concerns the process as a whole. The style sheet is always loaded synchronously. The transformation, on the other hand, can occur asynchronously—at least to some extent. Let’s see why.
The Transform method has a couple of overloads that return an XML reader, as shown here:
public XmlReader Transform(XPathNavigator input, XsltArgumentList args); public XmlReader Transform(IXPathNavigable input, XsltArgumentList args);
The signature, and the behavior, of these overloads is slightly different from the others. As you can see, the method does not accept any argument representing the output stream. The second argument can be an XsltArgumentList object, which serves other purposes that we’ll get into in the section “Creating a .NET Framework Argument List,” on page 324. The input document must be an XPath navigator or an XPath document referenced through the IXPathNavigable interface.
The output of the transformation process is not written out to a stream but created in memory and returned to the user via an XML reader. The overall transformation process works by creating an intermediate data structure (referred to as the navigator input) in which the content of the style sheet is used as the underlying surface. Any XSLT tag found in the style sheet 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. In an XSLT program, these statements are called output records, while templates play the role of subroutines. Figure 7-8 shows how the XSLT processor generates its output.
When the Transform method gets an output stream to write to, the XSLT processor loops through all the records and accumulates the text into the specified buffer. If an XML reader has been requested, the processor creates an instance of an internal reader class and returns that to the caller. The exact name of the internal reader is System.Xml.Xsl.ReaderOutput. No transformation is performed until the caller explicitly asks to read the cached output records. Figure 7-9 shows how the XSLT processor returns its output.
The ReaderOutput class builds a virtual XML tree on top of the compiled style sheet, thus making it navigable using the standard XML reader interface. When the Transform method returns, the reader is in its initial state (and therefore 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 plan and realize a number of fancy features. For example, you could provide feedback to the user, discard nodes based on run-time conditions and user roles, or cause the process to occur asynchronously on a secondary thread.
The reader interface exposes the XSLT records as XML nodes—the same XML nodes you will find by visiting the output document. The following code snippet demonstrates how to set up a user-controlled transformation:
// The XML source must be an XPath document or an XPath navigator XPathDocument doc = new XPathDocument(source); // No arg-list to provide in this case XmlReader reader = xslt.Transform(doc, null); // Perform the transformation, record by record while (reader.Read()) { // Do something }
Figure 7-10 shows the user interface of a sample application. It includes a list box control that is iteratively populated with information excerpted from the reader’s current node. Each row in the list box corresponds to an output record generated by the XSLT processor.
In the reading loop, all nodes are analyzed and serialized to XML text, as shown in the following code. In this way, each row in the list box corresponds to the line of text that is sent to an output stream if you opt for a synchronous transformation.
void ReadOutputRecords(XmlReader reader) { // Clear the list box OutputList.Items.Clear(); // Read the records while(reader.Read()) { string buf = ""; switch(reader.NodeType) { case XmlNodeType.Element: buf = String.Format("{0}<{1} {2}>", new String(‘ ’, 2*reader.Depth), reader.Name, GetNodeAttributes(reader)); break; case XmlNodeType.EndElement: buf = String.Format("{0}</{1}>", new String(‘ ’, 2*reader.Depth), reader.Name); break; case XmlNodeType.Text: buf = String.Format("{0}{1}", new String(‘ ’, 2*reader.Depth), reader.Value); break; } OutputList.Items.Add(buf); } }
The final text is indented using a padding string whose size depends on the reader’s Depth property. Node names and values are returned by the Name and Value properties. For element nodes, attributes are read using a piece of code that we examined in detail in Chapter 2:
string GetNodeAttributes(XmlReader reader) { if (!reader.HasAttributes) return ""; string buf = ""; while(reader.MoveToNextAttribute()) buf += String.Format("{0}="{1}" ", reader.Name, reader.Value); reader.MoveToElement(); return buf; }
An XSLT style sheet can declare the output format of the serialized text using the <xsl:output> statement. This statement features several attributes, the most important of which is method. The method attribute can be set with any of the following keywords: xml, html, or text. By default, the output format is XML unless the root tag of the results document equals <html>. In this case, the output is in HTML.
Differences between XML and HTML are minimal. If the output format is HTML, the XML well-formedness is sacrificed in the name of a greater programmer-friendliness. This means that, for example, empty tags will not have an end tag. In addition to method, other attributes of interest are indent, encoding, and omit-xml-declaration, which respectively indent the text, set the preferred character encoding, and omit the typical XML prolog.
If you add an <xsl:output> statement to the previously considered style sheets, the source code of the results document will be significantly different, but not its overall meaning. If you choose to output plain text, on the other hand, the XSLT processor will discard any markup text in the style sheet and output only text.
As a final note, consider that <xsl:output> is a discretionary behavior that not all XSLT processors provide and not all in the same way. In particular, when the Transform method is writing to a text writer or an XML writer, the .NET Framework XSLT processor ignores the encoding attribute in favor of the corresponding property on the object.
As mentioned, XSLT scripts can take arguments. You can declare arguments globally for the entire script or locally to a particular template. Arguments can have a default value that will make them always available as a variable in the scope. Aside from the default value, in XSLT there are no other differences between arguments and variables.
The following code shows a style sheet snippet in which a parameter named MaxNumOfRows is declared and initialized with a default value of 6:
<xsl:template match="Employee">
<xsl:param name="MaxNumOfRows" select="6" />
<xsl:if test="$MaxNumOfRows > position()">
<TR>
<xsl:apply-templates select="employeeid" />
<xsl:apply-templates select="lastname" />
<xsl:apply-templates select="title" />
</TR>
</xsl:if>
</xsl:template>
The script retrieves the argument using its public name prefixed with a dollar sign ($). In particular, the conditional statement shown here applies to the template only if five employee nodes have not yet been processed:
<xsl:if test="$MaxNumOfRows > position()"> <!-- Apply the template --> </xsl:if>
Note that you can’t use the less than sign (<) in an XSLT expression because it could confuse the processor. Instead, use the escaped version of the character: <. The greater than sign (>) can be safely used, however. If, like me, you don’t like escaped strings, you can invert the terms of the comparison.
Note
Parameters can be associated only with templates or with the global script. You can’t associate parameters with other XSLT instructions such as a <xsl:for-each>.
When you call a parameterized XSLT template, you give actual values to formal parameters using the <xsl:with-param> instruction. Here’s an example that calls the sample Employee template, giving the MaxNumOfRows argument a value of 7:
<xsl:apply-templates select="MyDataSet/NorthwindEmployees/Employee"> <xsl:with-param name="MaxNumOfRows" select="7" /> </xsl:apply-templates>
If the called template has no such parameter, nothing happens, and the argument will be ignored. The <xsl:with-param> instruction can be associated with both <xsl:apply-templates> and <xsl:call-template> instructions.
The Transform method lets you pass arguments to the style sheet using an instance of the XsltArgumentList class. When you pass arguments to an XSLT script in this way, you can’t specify what 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.
Creating an argument list is straightforward. You create an instance of the XsltArgumentList class and populate it with values, as shown here:
XsltArgumentList args = new XsltArgumentList(); args.AddParam("MaxNumOfRows", "", 7);
The AddParam method creates a new entry in the argument list. AddParam requires three parameters: the (qualified) 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 .NET Framework type you use to pack the entry into the argument list, the parameter value must correspond to a valid XPath type: string, Boolean, number, node fragment, and node-set. The number type corresponds to a .NET Framework double type, whereas node fragments and node-sets are equivalent to XPath navigators and XPath node iterators. (See Chapter 6 for more information about these data types.)
Despite what its name suggests, XsltArgumentList is not a collection-based class. It does not derive from ArrayList or from a collection class, nor does it implement any of the typical list interfaces like IList or ICollection.
The XsltArgumentList class is built around a couple of child hash tables: one to hold XSLT parameters and one to gather the so-called extension objects. An extension object is simply a living instance of a .NET Framework object that you can pass as an argument to the style sheet. Of course, this feature is specific to the .NET XSLT processor. We’ll look at extension objects in more detail in the section “XSLT Extension Objects,” on page 336.
The programming interface of the XsltArgumentList class is described in Table 7-8. It provides only methods.
Method | Description |
---|---|
AddExtensionObject | Adds a new managed object to the list. You can specify the namespace URI or use the default namespace by passing an empty string. If you pass null, an exception is thrown. |
AddParam | Adds a parameter value to the list. Must indicate the name of the argument and optionally the associated namespace URI. |
Clear | Removes all parameters and extension objects from the list. |
GetExtensionObject | Returns the object associated with the given namespace. |
GetParam | Gets the value of the parameter with the specified (qualified) name. |
RemoveExtensionObject | Removes the specified object from the list. |
RemoveParam | Removes the specified parameter from the list. |
As with parameters, the style sheet identifies an extension object through its class name and an associated namespace prefix.
3.145.9.12