Enhancing XSL

 

By Kurt Cagle
April 2000 (Updated March 2004)

Summary: XSL provides a way of performing complex transformations on XML data. By using combinations of templates and scripting, you can significantly expand these transformations to perform precise validation and queries, incorporate subordinate documents, and generate highly sophisticated applications from any data. (36 printed pages)

In this article, I will look at the basic architecture of XSL, focusing on how XSL uses template mechanisms to match specific nodes with output. I'll also point out places where the XSL mechanism by itself suffers some limitations that can be overcome through external mechanisms.

Contents

Enhancing XSL Incorporating Script Playing with Parameters Making Your Objects XSL Friendly Summary

Enhancing XSL

Here's a bit of a conundrum for you: What do XSL and DNA have in common? Now, I'm not talking about Microsoft® Windows® DNA. Here I'm talking about the other kind of DNA, deoxyribonucleic acid, the substance out of which we and all other life forms on the planet are constructed.

Give up? The answer is actually pretty simple-both are examples of matching templates. DNA works by providing sequences of shapes that amino acids floating around in the mitochondria need to match. You put enough of these amino acids together, following the patterns exposed by the DNA (actually by RNA, DNA's alter ego), and you start developing the large, complex proteins that are essential for life. Only amino acids that properly match the template will fit into the appropriate space, although it is possible to create similar molecules that differ from these amino acids but still fit, creating transcription errors and mutations.

The beauty of this approach is that the processes are fundamentally local-the proteins generated in the locus where eye color is determined, for example, don't care what's happening half a chromosome away. Put another way, DNA (technically, RNA transcription of proteins) is an example of a massively parallel computation; the protein sequences aren't formed one at a time but take place all over the DNA template simultaneously.

Extensible Stylesheet Language (XSL) works something like that. Within an XSL transformation, you'll find any number of templates that could match particular Extensible Markup Language (XML) patterns. These templates don't have any intrinsic notion of sequence and no preconceived concepts about the specific data that they're processing-they are simply designed to match specific patterns. In contrast to traditional programming subroutines, where there is a fairly distinct set of processing steps that take place, XSL templates could conceivably occur in any order. Like RNA transcription, they are fundamentally local.

That is not to say that the analogy should be taken too far. The one primary difference between the two processes is that DNA is truly parallel, while XSL usually does work sequentially. Within an XSL template, there is an order of sorts to what matches are determined when. Given that most computer programs are currently sequential Turing machines, this isn't really a major limitation. On the contrary, it makes it possible to create recursive structures that you can't really build with DNA (at least at the protein creation level).

Indeed, some of the most powerful XSL transformations are also the simplest, least "linear" ones. Consider the following XML structure, a list of employees from a fictional company. (I'm only going to show a few in the interest of space, but the samples that accompany this article contain a total of 20 employees.)

<!-- msdn-employees1.xml -->
<employees>
   <employee>
      <id>101</id>
      <firstName>John</firstName>
      <lastName>Coake</lastName>
      <title>President</title>
      <dateStarted>1997-11-12</dateStarted>
      <salary>324021</salary>
      <department>Administration</department>
   </employee>
   <employee>
      <id>102</id>
      <firstName>Lisa</firstName>
      <lastName>Jacobson</lastName>
      <title>Chief Executive Officer</title>
      <dateStarted>1997-08-12</dateStarted>
      <salary>329215</salary>
      <department>Administration</department>
   </employee>
   <employee>
      <id>103</id>
      <firstName>Carolyn</firstName>
      <lastName>Seeley</lastName>
      <title>Chief Financial Officer</title>
      <dateStarted>1998-03-16</dateStarted>
      <salary>232768</salary>
      <department>Finance</department>
   </employee>
   <employee>
      <id>104</id>
      <firstName>Amy</firstName>
      <lastName>Anderson</lastName>
      <title>Chief Technical Officer</title>
      <dateStarted>1998-09-16</dateStarted>
      <salary>242768</salary>
      <department>Information Technologies</department>
   </employee>
   <employee>
      <id>105</id>
      <firstName>Frank</firstName>
      <lastName>Miller</lastName>
      <title>Vice President, Marketing</title>
      <dateStarted>1998-11-03</dateStarted>
      <salary>210359</salary>
      <department>Marketing</department>
   </employee>
   <employee>
      <id>106</id>
      <firstName>Mike</firstName>
      <lastName>Seamans</lastName>
      <title>Senior Analyst</title>
      <dateStarted>1999-03-16</dateStarted>
      <salary>160204</salary>
      <department>Information Technologies</department>
   </employee>
      <!-- More employees follow -->
</employees>

Note If you are running Microsoft Internet Explorer 5.0, you can actually see the transformation directly by applying the XSL document as a style sheet to the document using a processing instruction at the beginning of the XML:

<?xml:stylesheet type="text/xsl" href="yourTransform.xsl"?>

To perform XSL transformations using Microsoft XML Core Services (MSXML) 4.0, formerly known as the Microsoft XML Parser, you need to write script code to apply the template through the use of the transformNode or the transformNodeToObject method of the XML DOM document. The first method takes the XSL document as its only parameter, while transformNodeToObject takes both the XSL document object and a target document to receive the result, as follows:

// Load data.
  var source = new ActiveXObject("Msxml2.DOMDocument.4.0");
  source.async = false;
  source.load("data.xml");
 
  // Load style sheet.
  var stylesheet = new ActiveXObject("Msxml2.DOMDocument.4.0");
  stylesheet.async = false;
  stylesheet.load("style.xsl");
 
  // Set up the resulting document.
  var result = new ActiveXObject("Msxml2.DOMDocument.4.0");
  result.async = false;
  result.validateOnParse = true;
 
  // Parse results into a result DOM Document.
  source.transformNodeToObject(stylesheet, result); 

To perform XSLT transformations using the .NET Framework, you need to use the System.Xml.Xsl.XslTransform class. The following example performs a transformation and prints the output to the console

Imports System
Imports System.IO
Imports System.Xml
Imports System.Xml.XPath
Imports System.Xml.Xsl

Public Class Sample
    Private filename As [String] = "data.xml"
    Private stylesheet As [String] = "style.xsl"

    Public Shared Sub Main()
        Dim xslt As New XslTransform()
        xslt.Load(stylesheet)
        Dim xpathdocument As New XPathDocument(filename)
        Dim writer As New XmlTextWriter(Console.Out)
        writer.Formatting = Formatting.Indented

        xslt.Transform(xpathdocument, Nothing, writer, Nothing)
    End Sub 'Main
End Class 'Sample

Perhaps the simplest of all XSL scripts is the identity transformation. This transformation takes an XML document as input and returns the same document as output. It may not be terribly exciting, but it underlies almost all XSL scripts with any power behind them.

<!-- identityTransform.xsl -->
<xsl:stylesheet xmlns:xsl='https://www.w3.org/1999/XSL/Transform'
                version="1.0" >
 <xsl:template match="@*|*|processing-instruction()|comment()|text()">
  <xsl:copy>
    <xsl:apply-templates select="*|@*|text()|processing-instruction()|comment()" />
  </xsl:copy>
</xsl:template>

</xsl:stylesheet>

This transformation looks a little cryptic, but it is fairly straightforward. The stylesheet consists of a single with the rather unwieldy pattern ="*|@*|text()|processing-instruction()|comment()". Put succinctly, it matches everything: elements, attributes, text nodes, comments, and processing instructions. The template then copies all the child nodes that apply (here, everything) and applies the same template to catch all of these elements at the next level down the tree.

This pattern ends up working recursively and can process an XML tree in a remarkably short period of time. Of course, given that you end up with what you started with, this efficiency seems to be wasted. However, the trick to working with such an arrangement is to remember that the processor will look for the first match it encounters starting from the end, which means that if you add another template to intercept a specific element, you can change elements while still retaining the older structure.

To start with, suppose that you had a reorganization in your company and the "Information Technologies" department had been renamed "Research." You could change this in your XML document by adding one more filter to the identity transformation, as follows:

<xsl:stylesheet xmlns:xsl='https://www.w3.org/1999/XSL/Transform'
                version="1.0" >

 <xsl:template match="@*|*|processing-instruction()|comment()|text()">
  <xsl:copy>
    <xsl:apply-templates select="*|@*|text()|processing-instruction()|comment()" />
  </xsl:copy>
</xsl:template>

   <!-- Match Information Technologies -->
   <xsl:template match="department[.='Information Technologies']">
      <xsl:copy>Research</xsl:copy>
   </xsl:template>
</xsl:stylesheet>

This can be useful if you're attempting to do an XSL translation to a different schema. For example, suppose that in the process of doing a reorganization you wanted to change the schema in two ways: You wanted to add a department code that reflected the name of the department; and you wanted to add a ratings code from 1 to 10 that indicated the person's performance rating, used for granting raises or setting flags for disciplinary action (along with the change in research designation). Modify the XSL a bit more, placing the generalized rules closer to the beginning and the increasingly specialized exceptions more toward the end, as in the following example:

xsl:stylesheet xmlns:xsl='https://www.w3.org/1999/XSL/Transform'
                version="1.0" >

  <xsl:output indent="yes" />
<!-- This is another simple identity function;
     It is a good starting point to write XSLT transforms. -->

 <xsl:template match="@*|*|processing-instruction()|comment()|text()">
  <xsl:copy>
    <xsl:apply-templates select="*|@*|text()|processing-instruction()|comment()" />
  </xsl:copy>
</xsl:template>

   <!-- For the general employee, add a rating of 5 -->
   <xsl:template match="employee">
      <xsl:copy>
      <xsl:apply-templates
      select="@*|*|processing-instruction()|comment()|text()"/>
      <rating>5</rating>
      </xsl:copy>
   </xsl:template>

   <!-- Match the general department and the departmentID -->
   <xsl:template match="department">
      <xsl:copy-of select="." />
      <departmentID><xsl:choose>
         <xsl:when test=".='Administration'">ADM</xsl:when>
         <xsl:when test=".='Marketing'">MRK</xsl:when>
         <xsl:when test=".='Finance'">FIN</xsl:when>
      </xsl:choose></departmentID>
   </xsl:template>


   <xsl:template match="department[.='Information Technologies']">
      <xsl:copy>Research</xsl:copy>
      <departmentID>RSC</departmentID>
   </xsl:template>

</xsl:stylesheet> 

The result is shown in msdn-employees3.xml, as follows:

<!-- msdn-employees3.xml -->
<employees>
   <employee>
      <id>101</id>
      <firstName>John</firstName>
      <lastName>Coake</lastName>
      <title>President</title>
      <dateStarted>1997-11-12</dateStarted>
      <salary>324021</salary>
      <department>Administration</department>
      <departmentID>ADM</departmentID>
      <rating>5</rating>
   </employee>
   <employee>
      <id>102</id>
      <firstName>Lisa</firstName>
      <lastName>Jacobson</lastName>
      <title>Chief Executive Officer</title>
      <dateStarted>1997-08-12</dateStarted>
      <salary>329215</salary>
      <department>Administration</department>
      <departmentID>ADM</departmentID>
      <rating>5</rating>
   </employee>
   <employee>
      <id>103</id>
      <firstName>Carolyn</firstName>
      <lastName>Seeley</lastName>
      <title>Chief Financial Officer</title>
      <dateStarted>1998-03-16</dateStarted>
      <salary>232768</salary>
      <department>Finance</department>
      <departmentID>FIN</departmentID>
      <rating>5</rating>
   </employee>
   <employee>
      <id>104</id>
      <firstName>Amy</firstName>
      <lastName>Anderson</lastName>
      <title>Chief Technical Officer</title>
      <dateStarted>1998-09-16</dateStarted>
      <salary>242768</salary>
      <department>Research</department>
      <departmentID>RSC</departmentID>
      <rating>5</rating>
   </employee>
   <employee>
      <id>105</id>
      <firstName>Frank</firstName>
      <lastName>Miller</lastName>
      <title>Vice President, Marketing</title>
      <dateStarted>1998-11-03</dateStarted>
      <salary>210359</salary>
      <department>Marketing</department>
      <departmentID>MRK</departmentID>
      <rating>5</rating>
   </employee>
   <employee>
      <id>106</id>
      <firstName>Mike</firstName>
      <lastName>Seamans</lastName>
      <title>Senior Analyst</title>
      <dateStarted>1999-03-16</dateStarted>
      <salary>160204</salary>
      <department>Research</department>
      <departmentID>RSC</departmentID>
      <rating>5</rating>
   </employee>
      <!-- More employees follow -->
</employees>

Notice a couple of subtle points here. For starters, even though the <rating> tag was applied fairly early in the XSL script, it took place after the general <xsl:apply-templates> tag. As a consequence, the rating ended up at the end of the list. Because of the generally recursive nature of these calls, achieving a specific order can take some work.

Incorporating Script

Adding scripting capability to XSL enhances its effectiveness at the cost of portability. However, there are a number of transformations that are simply not possible without the use of script. This section will explore the mechanics of incorporating script blocks into an XSL transform.

The rating system introduced in the previous section introduces an element of complexity to the script that simple matching may not always solve. For example, suppose that the schema displayed in Msdn-employees3.xml had been instituted for a while, and periodically, managers (or in the case of senior officers, shareholders) would evaluate ratings and push salaries up or down in accordance with performance. In particular, salaries would increase or decrease as indicated in the following table.

Rating Percent Increase or Decrease
10 20%
9 15%
8 10%
7 5%
4-6 0%
2-3 -5%
1 -10%

Furthermore, if a rating of 3 or less occurred, the employee would be placed upon probationary status, receiving a <probation>Warning</probation> tag in his or her record. If an employee already had a probation tag, the flag would be set to <probation>Terminate</probation> and the employee would receive a pink slip. (This is a particularly draconian company.) The following XSL stylesheet performs the operations on the list of employees:

<!-- Performance1.xsl -->

<xsl:stylesheet xmlns:xsl='https://www.w3.org/1999/XSL/Transform'
                version="1.0">

   <xsl:output indent="yes" />

   <!-- Match everything  -->
   <xsl:template match="*|@*|text()|comment()|processing-instruction()">
      <xsl:copy><xsl:apply-templates
      select="*|@*|text()|comment()|processing-instruction()"/></xsl:copy>
   </xsl:template>

   <xsl:template match="rating">
      <xsl:copy-of select="." />
      <xsl:if test=". &lt; 4">
         <xsl:choose>
            <xsl:when
            test="ancestor::employee/probation[.='Warning']">
               <probation>Terminate</probation>
            </xsl:when>
            <xsl:otherwise>
               <probation>Warning</probation>
            </xsl:otherwise>
         </xsl:choose>
      </xsl:if>
   </xsl:template>
   <xsl:template match="probation"/>
   <xsl:template match="salary">
      <xsl:copy>
      <xsl:choose>
         <xsl:when test="ancestor::employee/rating[.=10]">
      <xsl:value-of select="number(.)*( 1.20)" />
         </xsl:when>
         <xsl:when test="ancestor::employee/rating[.=9]">
      <xsl:value-of select="number(.)*( 1.15)" />
         </xsl:when>
         <xsl:when test="ancestor::employee/rating[.=8]">
      <xsl:value-of select="number(.)*( 1.10)" />
         </xsl:when>
         <xsl:when test="ancestor::employee/rating[.=7]">
      <xsl:value-of select="number(.)*( 1.05)" />
         </xsl:when>
         <xsl:when test="ancestor::employee/rating[.=3 or .=2]">
      <xsl:value-of select="number(.)*( 0.95)" />
         </xsl:when>
         <xsl:when test="ancestor::employee/rating[.=1]">
      <xsl:value-of select="number(.)*( 0.90)" />
         </xsl:when>
         <xsl:otherwise>
            <xsl:value-of select="." />
         </xsl:otherwise>
      </xsl:choose>
      </xsl:copy>
   </xsl:template>
</xsl:stylesheet>

While there is a lot going on here, concentrate for the moment on the salary match template. This provides an example of how to employ the <xsl:choose><xsl:when/><xsl:otherwise/></xsl:choose> structure. Each condition is contained within an <xsl:when> case, except when the rating is 4, 5, or 6. In this case, the default <xsl:otherwise> block simply passes the current salary back to the output stream. For any other case, an <xsl:value-of> element is used to calculate a new salary.

In the script, I used the number() function to retrieve the numeric value of the salary. The value is automatically converted into a number if the text is numeric and NaN otherwise. The value is then multiplied by the appropriate percentage. The encasing <xsl:copy> block insures that the element is copied into the <salary> tag.

I included the warning action to point up the difference between <xsl:if> and the <xsl:choose> structure. In XSL, an if block checks only one condition-if the condition is true, it evaluates; if not, it doesn't. This is useful in situations in which you need to insert code into the output stream conditionally, such as when the rating falls below 4.

The <xsl:choose> structure, on the other hand, maintains state and lets you choose between multiple possible options. It's a useful structure for cases in which you might use the select keyword in Visual Basic or switch in Java. It's also the preferred route for handling if then else statements, because

<xsl:choose>
<xsl:when test="condition>resultA</xsl:when>
<xsl:otherwise>resultA</xsl:otherwise>
</xsl:choose>

is essentially the same as

if condition
   then resultA
   else resultB
end if

You can make a fairly compelling argument about the previous XSL stylesheet, that while effective, it seems unduly complex, redundant, and verbose. It would be useful if you could simplify this code for maintenance purposes. An alternative approach involves creating a script block with a getNewSalary function written in your favorite Microsoft scripting language. Both MSXML and the System.Xml.Xsl.XslTransform class support embedding blocks of procedural code in <msxsl:script> elements. Descriptions of script blocks are provided in the MSDN library topics <msxsl:script> Element and XSLT Stylesheet Scripting using <msxsl:script> for MSXML and the XslTransform class respectively. Following is an example of how you might want to approach writing such an XSL stylesheet:

<!-- Update Salary -->
<xsl:stylesheet  xmlns:xsl='https://www.w3.org/1999/XSL/Transform'
                version="1.0" xmlns:msxsl="urn:schemas-microsoft-com:xslt"
      xmlns:ex="https://www.example.com/" 
      exclude-result-prefixes="msxsl ex"> 

  <xsl:output indent="yes" />

   <msxsl:script language="JScript" implements-prefix="ex">
<![CDATA[
function getNewSalary(salary, rating){
   
   var adjustmentArray =new Array(.90,.95,.95,1.00,1.00,1.00,1.05,1.10,1.15,1.20);
   return  salary * adjustmentArray[rating-1]; 
}
]]></msxsl:script>

   <!-- Match everything else -->
   <xsl:template match="*|@*|text()|comment()|processing-instruction()">
      <xsl:copy><xsl:apply-templates
      select="*|@*|text()|comment()|processing-instruction()"/></xsl:copy>
   </xsl:template>

   <xsl:template match="rating">
      <xsl:copy-of select="." />
      <xsl:if test=". &lt; 4">
         <xsl:choose>
            <xsl:when
            test="ancestor::employee/probation[.='Warning']">
               <probation>Terminate</probation>
            </xsl:when>
            <xsl:otherwise>
               <probation>Warning</probation>
            </xsl:otherwise>
         </xsl:choose>
      </xsl:if>
   </xsl:template>

   <xsl:template match="probation" />

   <xsl:template match="salary">
      <salary>
   <xsl:value-of select="ex:getNewSalary(number(.), number(ancestor::employee/rating))" />
      </salary>
   </xsl:template>
</xsl:stylesheet>

The <msxsl:script> block starts and ends with a CDATA declaration (<![CDATA[ ]]>) to make sure that white space is preserved internally and that symbols such as less than (<) or greater than (>) don't get mistaken for XML delimiters. The implements-prefix attribute in turn informs the parser that the methods in this script block implement the object whose namespace prefix is specified in the attribute value. That is, the function getNewSalary defined in a script block with an implements-prefix of "ex" is different from a getNewSalary defined in a script block with an implements-prefix of "ace."

There is nothing stopping you from having multiple script blocks in a document or from having multiple functions contained in the same document. Indeed, if you have large scripts or an extensive number of functions, you may find it advantageous to split them up in this manner. In general, <msxsl:script> blocks are automatically loaded into memory prior to the time the processor parses the templates. Furthermore, if you have a statement in the <msxsl:script> block that is outside a function, that statement will be evaluated automatically before the templates are parsed, which means that you can put initialization code into these script nodes and expect that it will run before any processing actually takes place.

There is one other point in the previous example that I wanted to point out, concerning the following one-line template:

   <xsl:template match="probation"/>

This wasn't a typographical error. Sometimes you want to remove tags from an XML document when using an identity transformation. In this case, I wanted to add a <probation>Warning</probation> tag if no tag had existed and the condition was right (that is, if the rating had slipped below a value of 4). However, if after one evaluation period the performance rating hadn't improved, the tag's contents would need to be changed to <probation>Terminate</probation>. The code to handle the creation of a node and the changing of a node simultaneously can get pretty hairy in XSL, so it was easier to simply not pass the old node value on and to instead create a new node later. Note one unexpected but desirable consequence of this: If a person's performance does improve, the probation tag simply will not be passed on at all, so you don't need to worry about explicitly excluding it.

Playing with Parameters

One of the reasons that I've grown so attached to XSL is that it provides a powerfully different way of programming, one based more on pattern matching and less on procedural programming. Pattern matching in combination with parameterization is a powerful tool for processing XML. Parameterization essentially implies the ability to pass information into an XSLT stylesheet so that it can change its output based on some external information.

A parameter is indicated in XSLT through the use of the <xsl:param> tag. This associates a parameter name (contained, not surprisingly, in the "name") attribute with contents that are contained in one of two places, either as a "selected" attribute or as the contained text of the parameter tags. By the way, the contents don't have to be a simple value-you can actually pass XML directly in through a parameter.

Within the XSL structure itself, a parameter's value can be retrieved in a number of different ways. In an <xsl:if> or <xsl:when> test attribute, the parameter can be accessed by placing a '$' in front of its name. The following transformation will retrieve all employees whose income is greater than $100,000:

<!-- getSalaryThresholdEmployees.xsl -->
<xsl:stylesheet xmlns:xsl="https://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:param name="salaryThreshold">100000</xsl:param>
   <xsl:template match="/">
      <xsl:apply-templates select="*"/>
   </xsl:template>
   <xsl:template match="*|@*|text()">
      <xsl:copy><xsl:apply-templates
      select="*|@*|text()"/></xsl:copy>
   </xsl:template>
<xsl:template match="employee">
<xsl:if test="salary[number(.)>$salaryThreshold]">
      <xsl:copy><xsl:apply-templates select="*"/></xsl:copy>
</xsl:if>
</xsl:template>
</xsl:stylesheet>

You can also use parameters directly to output values into the stream. The <xsl:value-of select="$paramName"> node can be used to output the text of a parameterized value, while <xsl:copy-of select="$paramName"> can be used to output XML if the parameter contains it.

In MSXML, once you set up a parameter in your XSL script, you can use the addParameter method to pass a new value to the parameter before transforming it again. The following ASP script shows how this can be used in conjunction with a Web page:

<%@script language="VBScript"%>
<%
dim xmlDoc
dim xslDoc
if session("xmlDoc")=Nothing then
set xmlDoc=createObject("MSXML2.FreeThreadedDOMDocument")
xmlDoc.load server.mapPath("Msdn-employees3.xml")
set xslDoc=createObject ("MSXML2.FreeThreadedDOMDocument")
xslDoc.load server.mapPath("getSalaryThreshholdEmployees.xsl")
set xslTemplate=createObject("MSXML2.XSLTemplate")
xslTemplate.stylesheet=xslDoc
set proc=xslTemplate.createProcessor
session("xmlDoc")=xmlDoc
session("proc")=proc
else
   set xmlDoc=session("xmlDoc")
   set proc=session("proc")
end if

' Set the source of the data
proc.input=xmlDoc
' Set the destination of the output, here to a DIV in an HTML page
proc.output=response
' Transform the document
' Retrieve the salary from the query string, and add it as a parameter
proc.addParameter "salaryThreshhold",request("salary")
proc.transform
%>

In this case, the xmlDoc containing the employees is loaded only once at the beginning of the session, as is the processor object. If the session has timed out, these items are re-created from scratch. The addParameter method then passes the salary from the IIS Request object into a parameter node. The <xsl:param> nodes are automatically appended to the document and are always first to be compiled. The last parameter of a given name is the one that the parser will use, which is why the parameter is added rather than changed explicitly (which can sometimes be a much more complicated operation).

The following example shows how to add parameters using the XslArgumentList class as input to the Transform() method on the XslTransform class.

using System;
using System.IO;
using System.Xml;
using System.Xml.XPath;
using System.Xml.Xsl;


public class Sample
{
   private const String filename = " Msdn-employees3.xml ";
   private const String stylesheet = " getSalaryThreshholdEmployees.xsl ";

   public static void Main(string[] args) 
   {
    
      //Create the XslTransform and load the stylesheet.
      XslTransform xslt = new XslTransform();
      xslt.Load(stylesheet);

      //Create the XsltArgumentList.
      XsltArgumentList xslArg = new XsltArgumentList();
         
      //Create a parameter which represents the current date and time.
      DateTime d = DateTime.Now;
      xslArg.AddParam("salaryThreshhold", "", args[0]);

      //Create the XmlTextWriter to write the output to the console.             
     XmlTextWriter writer = new XmlTextWriter(Console.Out);

     //Transform the file.
     xslt.Transform(new XPathDocument(filename), xslArg, writer, null);
     writer.Close();

  }
}

Making Your Objects XSLT Friendly

The declaration and invocation of objects within XSLT, discussed earlier, has several significant problems. One of the biggest has to do with encapsulation. An XSL document should be a world unto itself, a complete black box-you should not need to know any more about how to work with the XSL entity than its expected input (XML data source and parameters) and output (a stream of some sort). Yet by putting the method declarations (or any code for that matter) into the box, you are forced as a user of the transformation to know how the innards work.

A related problem is that any objects within the XSL document know only about the environments that can be defined within the scripting parser. This can be a real problem with such things as ASP objects in IIS, because you can't use IIS easily from within XSL (nor should you need to).

There is an alternative; you can declare a namespace extension in the XSLT stylesheet and then, using the processor, you assign an object to the namespace.

That may not seem particularly clear, but an illustration may show what I'm talking about. Suppose that I had a business object, written in Visual Basic and having a progID of "fabrikam.employeeUtils", that performed three functions:

  1. For a given employee ID, it calculated the new salary based upon the current rating and salary (fabrikam.newSalary).
  2. It tested to see if the rating had fallen below acceptable levels (fabrikam.isUnsatisfactory).
  3. It indicated whether warnings and pink slips needed to be handed out (fabrikam.notifyUnsatisfactory), returning the level of infraction as a string.

The specific implementation for these functions is largely irrelevant (although I will touch on how they are handled in a little more detail).

The new XSL parser lets you create a namespace that you can use to associate the object with the transformation. In this case, I arbitrarily set up the namespace fabrikam=https://www.fabrikam.com/employeeUtils. The namespace in and of itself serves only to render the namespace unique and doesn't explicitly need to point to anything. Once this is set up, you can make references to properties or methods as if they were paths in an XPath query. For example, <xsl:value-of select="fabrikam:newSalary(.)"/> will pass the current context to the newSalary method and then retrieve the results to be displayed in the output stream. The full transformation might look something like the following:

<!-- Update Salary with Objects-->
   <xsl:stylesheet xmlns:xsl='https://www.w3.org/1999/XSL/Transform'
                version="1.0"
xmlns:fabrikam="https://www.fabrikam.com/employeeUtils">

   <xsl:output indent="yes" />

   <!-- Match everything  -->
   <xsl:template match="*|@*|text()|comment()|processing-instruction()">
      <xsl:copy><xsl:apply-templates
      select="*|@*|text()|comment()|processing-instruction()"/></xsl:copy>
   </xsl:template>

   <xsl:template match="employee">
      <xsl:apply-templates/>
   </xsl:template>
   <xsl:template match="rating">
      <xsl:copy><xsl:value-of/></xsl:copy>
      <xsl:if test="fabrikam:isUnsatisfactory(.)">
         <probation>
<xsl:value-of select="fabrikam:NotifyUnsatisfactory(.)"/>
</probation>
      </xsl:if>
   </xsl:template>
   <xsl:template match="probation"/>
   <xsl:template match="salary">
      <xsl:copy><xsl:value-of
      select="fabrikam:newSalary(.)"/></xsl:copy>
   </xsl:template>
</xsl:stylesheet>

This is essentially the same transformation that was described in the previous section, "Incorporating Script," with the critical difference being that the script here is communicating directly with the outside world. In all of the functions described by the fabrikam namespace, you'll see that a period is being passed. This period is the current context (any other context could have been included here as well) and essentially has the characteristics of an XML DOM NodeList (a list of elements generated in response to an XPath query), typically with just one element. The implementation for the isUnsatisfactory function shows how it would be defined (in Visual Basic):

Function isUnsatisfactory(nodeList as IXMLDOMSelection) as String
   Dim ratingNode
Dim rating as String
   Dim probationNode
   Set ratingNode=nodeList(0)
   Rating=ratingNode.nodeTypedvalue
   If rating<4 then
      Set probationNode=ratingNode.selectSingleNodes(_
      "//ancestor::employer/probation")
      if probationNode is nothing then
         isUnsatisfactory="Warning"
      else
         isUnsatisfactory="Terminated"
      end if
   else
      isUnsatisfactory=""
   end if
End Function

Once the component is created, you need to use the addObject method to pass the object into the XSL transformation. This will bind the object to the namespace that you supply so that when you call the processor's transform method, the transformation can communicate with the external object, as follows:

proc.input=xmlDoc
' Set the destination of the output, here to a DIV in an HTML page
proc.output=response
' Transform the document
' Retrieve the salary from the query string, and add it as a parameter
set axeman=createObject("fabrikam.employeeUtils")
proc.addObject axeman, "https://www.fabrikam.com/employeeUtils"
proc.transform

Notice that you are passing the namespace here, not its prefix. As a consumer of the XSL template, you should not need to know what namespace prefixes it uses, only that you can pass the right object to the appropriate prefix. (If you think of the namespace as a GUID, this can help-it is just a means to match the object with its associated prefix.)

As a design note, you might want to think about declaring interface stubs in script for interface components, especially ones that you design yourself. When you apply addObject, the newer implementations will overwrite the older stubs, but the "virtual" functions (to borrow a term from Microsoft Visual C++®) will help you keep your code consistent.

This same functionality can be achieved with extension objects in the XslTransform class as described in the .NET Framework documentation topic XsltArgumentList for Stylesheet Parameters and Extension Objects. The extension object is added to the transformation using the AddExtensionObject method of the XslArgumentList class. Single nodes are passed to the methods of extension objects as instances of the System.Xml.XPath.XPathNavigator class while node lists are passed as instances of the System.Xml.XPath.XPathNodeIterator class. The example below shows how an extension object would be passed to an XSLT stylesheet.

    'Load the XML data file.
    Dim doc As XPathDocument = New XPathDocument(filename)

    'Create an XsltArgumentList.
    Dim xslArg As XsltArgumentList = New XsltArgumentList
    xslArg.AddExtensionObject(new fabrikam.employeeUtils());  

    'Create an XmlTextWriter to handle the output.             
    Dim writer As XmlTextWriter = New XmlTextWriter(Console.Out, Nothing)

    'Transform the file.
    xslt.Transform(doc, xslArg, writer, Nothing)

    writer.Close()

Summary

In Douglas Hofsteader's marvelous book about recursion and self-referential mathematics, Goedel, Escher and Bach, an Eternal Gold Braid, he hinted that much of our perception of consciousness and self is tied into structures within structures, templates that match thoughts and awareness in manifold ways. XSL has this wonderful aspect of symmetry to it that could have a profound effect upon the way that we program. Grapple with it, take time to appreciate it, attempt to see ways of expressing both the ridiculous and the sublime with it. It's worth the effort.

Kurt Cagle is a writer and developer working in Olympia, Washington. He is busy on his latest book, the XML Developer's Handbook, from Sybex, and can be reached at cagle@olywa.net or from his (shared) Web site at https://www.vbxml.com/cagle.

Disclaimer: The example companies, organizations, products, people, and events depicted herein are fictitious. No association with any real company, organization, product, person or event is intended or should be inferred.