X SL Transformations (XSLT) is known for making hard things easy and easy things hard. It can simplify complex transformation logic that would be extremely tedious to implement otherwise. But at the same time, XSLT's functional programming model can sometimes make it extremely difficult to perform trivial business logic. Often extending XSLT with traditional languages like VBScript, JScript®, or any Microsoft® .NET-supported language can offer the best of both worlds (for an introduction to XSLT see the article that I cowrote with Don Box and John Lam in the August 2000 issue).
For example, consider the following XML document that contains a list of operations to perform:
<equation>
<add>3</add>
<sub>1</sub>
<mul>6</mul>
<add>8</add>
<div>4</div>
</equation>
Let's say that the goal is to evaluate the list of operations, top to bottom, assuming no operator precedence and an initial value of 0. In this case, it's like evaluating the following equation:
((((3 - 1) * 6) + 8) / 4) = 5
There are many ways to solve this problem. Depending on your background and the way you have handled this in the past, and even on the languages you typically use, you may find some solutions intuitive and others quite foreign.
JScript Solution
You've probably already worked out in your head how you could solve this problem using an imperative programming language like Visual Basic®, C++, or JScript in conjunction with the DOM. Using an imperative language that allows you to declare and update variables simplifies the task tremendously. The JScript function in Figure 1 takes a list of nodes that represent the children of the equation element and calculates the result.
To many, this code is intuitive thanks to the fact that JScript allows you to declare the result variable whose value can be updated within each iteration of the loop. This function could be called as shown here:
var doc = new ActiveXObject("MSXML2.DOMDocument.4.0");
doc.Load("numbers.xml");
var result = processEquation(doc.selectNodes("/equation/*"));
XSLT Solution
Now imagine you're using a language that allows you to declare variables, but once you assign them values, these values cannot be modified. In other words, they're not really variables, rather they're runtime-assigned constants. Functional programming languages are a class of programming languages that share this characteristic. Languages such as Scheme, ML, Haskell, XSLT, and several others fall into this category.
Because functional languages are so modular and flexible, they are often used to work on extremely complex problems (AI is an example). In addition to this, functional language implementations can be optimized in ways that are not possible for the mainstream imperative languages used today. This all comes at a price, however, since the learning curve is steep for those not used to the functional model.
For example, take the XSLT program in Figure 2, which provides the same functionality using a different algorithm. The processEquation template produces the same result as the JScript function shown in Figure 1. This example is different from the JScript version because it doesn't use a for loop and it doesn't update a variable after processing each operation. Instead, it uses recursion to process the list, relying on the call stack to keep track of the incremental result after each operation.
XSLT + JScript Solution
If you can follow the code in Figure 2 and it feels just as natural or more natural than the code in Figure 1, you may not need to read the rest of this column. But if you're like most developers, it's not the most intuitive programming model, especially for this type of simple task. No one would argue against using XSLT to implement complex XML transformation logic, but when those transformations require certain types of business logic, it can get hairier than it's worth.
In these situations, a combination of XSLT for the tree-based transformation logic and another programming language for some of the business logic can offer the best of both worlds. The XSLT 1.0 specification codifies the practice of extending XSLT programs with non-XSLT code, which can be written in any programming language supported by a given XSLT processor.
The XSLT program in Figure 3 shows how to add the JScript function from Figure 1 to the XSLT program in Figure 2 to replace the processEquation template. Not only can this approach simplify the XSLT code, in many situations it's the only possible way to implement certain functionality. For example, if you need to perform a mathematical operation that isn't supported in the XSLT language itself, you would be forced to use a custom extension (I'll show you an example of this later). If you're using XSLT to perform sophisticated transformations, it's not uncommon to run into situations that require custom extensions.
The downside to this approach is that your extended functionality will only work with processors that support the languages in use. If your XSLTs don't require portability between processors, then you don't have to concern yourself with this issue. If they do, you'll either have to bite the bullet and figure out how to implement the functionality with generic XSLT code or, when that's not possible, provide a different implementation for each of the processors you need to support.
Both the .NET and MSXML XSLT processors provide a simple mechanism for adding code written in other languages, either directly in the XSLT document or by linking to existing COM objects or .NET assemblies. Throughout the remainder of this column, I'll discuss the details of extending XSLT programs with Microsoft implementations.
XSLT 1.0 Extension Mechanism
The XSLT 1.0 specification defines two types of extensions: extension elements and extension functions. Both types of extensions provide additional functionality to the standard language and can be used like any other XSLT 1.0 element (these include xsl:transform, xsl:template, xsl:value-of, and so on) or XPath 1.0/XSLT 1.0 function (such as string, substring-before, sum, document, and so forth).
Due to XSLT 1.0's template-based model, XSLT processors need additional information to properly distinguish between static content elements and extension elements that represent additional behavior. For example, consider the following XSLT transformation:
<xsl:transform version="1.0"
xmlns:xsl="https://www.w3.org/1999/XSL/Transform"
xmlns:out="https://www.w3.org/1999/xhtml"
xmlns:ext="https://example.org/extension"
>
<xsl:template match="/">
<out:html>
<ext:doSomeFunkyMagic/>
•••
</out:html>
</xsl:template>
</xsl:transform>
In this example, the processor recognizes the elements associated with the https://www.w3.org/1999/XSL/Transform namespace (transform and template) as language-specific instructions. The other elements, html and doSomeFunkyMagic, are identified by the processor as content that should be output when the template is instantiated.
Assume now that the doSomeFunkyMagic element is designated as an extension element by a certain class of processors. In order for the processor to figure this out, it needs to know what namespaces contain extension elements in use. This is accomplished through the extension-element-prefixes attribute that can be used on the stylesheet/transform element, as shown here:
<xsl:transform version="1.0"
xmlns:xsl="https://www.w3.org/1999/XSL/Transform"
xmlns:out="https://www.w3.org/1999/xhtml"
xmlns:ext="https://example.org/extension"
extension-element-prefixes="ext"
>
<xsl:template match="/">
<out:html>
<ext:doSomeFunkyMagic/>
</out:html>
</xsl:template>
</xsl:transform>
Now when the processor executes the transformation, it can figure out that doSomeFunkyMagic represents an extension element and not just regular output.
This is not an issue with extension functions since they cannot be confused with output content, as I just illustrated. This means that if you're only using extension functions, you don't need to use the extension-element-prefixes element, but you still need to qualify the function names with an extension namespace:
<xsl:transform version="1.0"
xmlns:xsl="https://www.w3.org/1999/XSL/Transform"
xmlns:out="https://www.w3.org/1999/xhtml"
xmlns:ext="https://example.org/extension"
>
<xsl:template match="/">
<out:html>
<xsl:value-of select="ext:doSomeFunkyMagic()"/>
</out:html>
</xsl:template>
</xsl:transform>
All of the built-in XPath 1.0 and XSLT 1.0 function names are unqualified, which means they're always serialized as NCNames and assumed to be from no namespace. Hence, if the processor ever encounters a prefixed function name, it automatically assumes that it's an extension function.
The XSLT 1.0 specification also provides a mechanism that allows transformations to query the processor for extension support. Specifically, it defines the element-available and function-available functions to determine if the executing processor actually supports an extension before attempting to use it. Using these functions in combination with if or choose elements makes it possible to write XSLT transformations that can take advantage of certain extensions without completely sacrificing portability, as shown in Figure 4.
XSLT 1.0 also provides a fallback element as a somewhat cleaner alternative to providing backup functionality. You place the fallback functionality inside the fallback element and you can use it inside of any extension element that may not be supported. Figure 5 shows the previous example ported to use the fallback element.
Although the specification provides support for using processor-specific extensions, it does not define a standard mechanism for implementing them. The mechanism varies from processor to processor, but most of the modern implementations do provide this support today (a standard approach will probably make its way into a future XSLT specification). Let's look at how this works in the Microsoft MSXML 4.0 and .NET XSLT implementations.
msxsl:script
The current Microsoft XSLT processors make it possible to implement extensions either directly in the XSLT document or within out-of-band extension objects. The introduction to this column already contained some examples of the first approach. As the examples illustrated, you can embed extension code within the XSLT document through the script element that comes from the Microsoft urn:schemas-microsoft-com:xslt namespace (I'll refer to this as the msxsl:script element from now on). The msxsl:script element is an extension element itself, so you can test for its support, as discussed in the previous section.
The syntax you must use for msxsl:script, in both MSXML 4.0 and .NET, is as follows:
<msxsl:script
language = "language-name"
implements-prefix = "prefix of user's namespace">
</msxsl:script>
The language attribute specifies what language is used within the script tag, while the implements-prefix attribute controls the namespace with which the contained functions are scoped. You must use a qualified name to call one of these user-defined functions in an XPath expression.
The languages that you can use differ between the MSXML and .NET implementations. In MSXML (from version 2.0 on), you can use either VBScript or JavaScript. In .NET, however, you can use any .NET-supported language including C#, Visual Basic, JScript.NET, and even JScript, as before. The ability to use strongly typed languages like C# and Visual Basic .NET makes this an even more attractive opportunity.
Refer back to Figure 3, which shows the use of msxsl:script in conjunction with JScript. Notice that processEquation is associated with the urn:the-xml-files:xslt namespace, which is bound to the usr prefix in a namespace declaration on the transform element. The function name must be prefixed with usr whenever it's used. In that example, the function is called within a value-of select expression, like the one shown here:
<xsl:value-of select="usr:processEquation(/equation/*)"/>
Since both Microsoft XSLT implementations support JScript, the document shown in Figure 3 will work with either the MSXML or .NET version.
With both Microsoft implementations, you can write extension functions in different languages by using multiple msxsl:script elements in a single XSLT document (see Figure 6 for an example). The only issue with this is that each msxsl:script element must specify a different namespace in the implements-prefix attribute. You cannot write extension code for a single namespace in multiple msxsl:script elements. The rest of the details surrounding msxsl:script differ between the two implementations.
Mapping Types between XSLT and MSXML
The code in Figure 6 shows information being passed in both directions. Objects are passed into the functions as arguments, and an object is returned from each function as the return value. You must understand how the XPath type system maps to the JScript and VBScript type systems in order to design your extension functions properly.
The table in Figure 7 describes what each XPath type maps to in JScript and VBScript when used in function arguments. The first three types are straightforward. Strings, numbers, and Booleans in XPath map directly to strings, numbers, and Booleans in JScript and VBScript. As an example, the functions used in Figure 6 returned strings from the JScript and VBScript functions to the calling XSLT value-of expressions.
The last two types are going to require a bit more explanation. XPath node-sets map to JScript and VBScript objects that implement the DOM NodeList interface (IXMLDOMNodeList). IXMLDOMNodeList is basically just a collection interface for generic DOM nodes that supports basic traversal and length queries.
Let's look at an example of how to use IXMLDOMNodeList in an extension function. Say you need to calculate the distance between the two points in the following XML document:
<line>
<point>
<x>10</x>
<y>10</y>
</point>
<point>
<x>20</x>
<y>20</y>
</point>
</line>
There is no way to do this using standard XSLT, but with the help of an extension function and a more sophisticated math library it becomes trivial. The extension function shown in Figure 8 expects an IXMLDOMNodeList object representing a collection of two points. It calculates the distance between the points using the built-in JScript Math object:
This extension function could then be called as follows:
distance: <xsl:value-of
select="math:CalcDistance(/line/point)"/>
XSLT result tree fragments also map to IXMLDOMNodeList objects, but they only contain a single node of type NODE_DOCUMENT_FRAGMENT. In other words, you have to navigate to the IXMLDOMDocumentFragment object's children to perform the real processing. The XSLT in Figure 9 illustrates how to process a result tree fragment that's passed to the function.
This version of the extension function expects to receive a result tree fragment. Result tree fragments are generated through XSLT variable or param elements. The following XSLT snippet illustrates how to generate a result tree fragment and how to call this new version of the extension function:
•••
<xsl:variable name="points">
<point>
<x>10</x>
<y>10</y>
</point>
<point>
<x>20</x>
<y>20</y>
</point>
</xsl:variable>
<xsl:value-of select="math:CalcDistance($points)"/>
•••
The types listed in Figure 7 are the only types that can be used in function arguments. Since JScript and VBScript are not strongly typed, you must be explicit about what type of object you're passing when you're calling a function. Consider the following extension function:
function doSomething(obj1, obj2, obj3) {
// omitted for brevity
}
If the function expects the arguments to be of type string, number, and IXMLDOMNodeList, respectively, you must explicitly coerce them when calling the method, as shown here:
<xsl:value-of
select="usr:doSomething(string(./author/name),
number(./price), .)"/>
In this case, the string and number XPath functions are used to explicitly coerce the first two node-sets to the expected type. The final argument doesn't require coercion since node-sets automatically map to IXMLDOMNodeList objects.
There are more restrictions placed on function return values in MSXML. The only types that can be returned from a function are strings and numbers (see Figure 10). If you return a number, it's coerced to a JScript and VBScript Number. If you return any other primitive type, it's coerced to a JScript/VBScript String. Attempting to return an object raises an exception.
For example, the following extension function is acceptable because it returns a number.
<ms:script language="VBScript" implements-prefix="vb">
<![CDATA[
function AuthorsByState(state)
Set doc = CreateObject("MSXML2.DOMDocument.4.0")
doc.Load "authors.xml"
// returns the number of authors for given state
AuthorsByState = doc.selectNodes("//author").length
end function
]]>
</ms:script>
But this slightly modified version raises an exception since it attempts to return an IXMLDOMNodeList object to the caller:
<ms:script language="VBScript" implements-prefix="vb">
<![CDATA[
function AuthorsByState(state)
Set doc = CreateObject("MSXML2.DOMDocument.4.0")
doc.Load "authors.xml"
// returns the authors collection
FindAuthorsByState = doc.selectNodes("//author")
end function
]]>
</ms:script>
You can instantiate and call objects from within extension functions. You just can't return them to the caller.
Mapping Types between XSLT and .NET
The mechanism for building .NET extension functions is similar to what I just discussed for MSXML. However, the .NET implementation is superior for several reasons. First, you have more language choices, including strongly typed languages like C# and Visual Basic .NET. Second, extension functions can be strongly typed, which allows the processor to take care of certain coercions for you when calling functions. And finally, objects (other than string and number) can be used as return values.
Figure 11 describes the mapping between the XPath and .NET type systems. This mapping applies to both directions: function arguments and return values. The first three types map to the .NET System.String, System.Boolean, and System.Double types while node-set and result tree fragment map to XPathNavigator and XPathNodeIterator, respectively (for more details on these classes, see the September 2001 XML Files column. Int16, UInt16, Int32, UInt32, Int64, UInt64, Single, and Decimal types are automatically forced into Double, which maps to the W3C XPath number type. If any other types are used in function arguments or return values, an exception is thrown. These type mappings also apply to XSLT global parameters.
Consider the following version of the CalcDistance extension function implemented in C#:
<ms:script language="C#" implements-prefix="math">
<![CDATA[
double CalcDistance(XPathNodeIterator points) {
points.MoveNext();
double xdelta =
(double)points.Current.Evaluate("x - following::x");
double ydelta =
(double)points.Current.Evaluate("y - following::y");
return Math.Sqrt(Math.Pow(xdelta, 2) +
Math.Pow(ydelta, 2));
}
]]>
</ms:script>
Notice that the function accepts an XPathNodeIterator argument and it returns a double value. This function can be called just like the previous function, which was implemented in JScript:
<xsl:value-of select="math:CalcDistance(/line/point)"/>
This example also takes advantage of the System.Math class. Notice, however, that the code doesn't contain any C# using statements. The .NET implementation makes the namespaces listed in Figure 12 automatically available within msxsl:script extensions. Currently, you cannot use types from namespaces other than these within msxsl:script extensions. If you need to use other types, you'll have to rely on XSLT extension objects, which I'll discuss in the following sections.
Reusability
Embedding extension code directly within the XSLT document is convenient, but it can limit reusability if not designed properly. One way to reuse custom functions defined in msxsl:script is through the XSLT include element. You can create global libraries in XSLT documents that only contain extension functions. For example, the XSLT document in Figure 13 only contains an msxsl:script element, which itself contains several functions.
Assuming the name of this file is global-extensions.xsl, the following example shows how this file can be included in another XSLT document:
<xsl:transform version="1.0"
xmlns:xsl="https://www.w3.org/1999/XSL/Transform">
<xsl:include href="global-extensions.xsl"/>
•••
</xsl:transform>
This is a lot like using the ASP include element for reusing script embedded within ASP pages. Although this works, the preferred approach to reusability is XSLT extension objects.
XSLT Extension Objects
Both MSXML and .NET support implementing XSLT extension functions in compiled components. With MSXML you can call into compiled COM components, while with .NET you can call into compiled assemblies. The details differ depending on which implementation you want to use.
In the MSXML implementation, COM extension objects support setting and querying properties as well as invoking functions. As with the msxsl:script extensions, the types used in the functions and properties must adhere to the mapping rules described earlier in Figure 7 and Figure 10.
To use an extension object within an XSLT document, you need to add it to the processor's context before executing the transformation. You can do this in MSXML through the addObject method exposed by the IXSLProcessor interface. The syntax is as follows:
objXSLProcessor.addObject(obj, namespaceURI);
addObject expects a namespace identifier to associate the object with. This is equivalent to using the implements-prefix attribute on the msxsl:script element. Once the object has been added to the processor's context and associated with the supplied namespace, it can be called from XPath expressions (as shown in Figure 13).
Properties are accessed in XSLT by prefixing the property name with either get- or put- followed by parentheses. For example:
get-age(), put-age(33))
Let's look at a complete example that defines and uses an extension object. Let's assume the following Visual Basic class definition:
' Class: Person
Public name As String
Public age As Double
Public Function SayHello(ByVal other as String) As String
SayHello = "Hello " & other & ", I'm " & name
End Function
To use a Person object as an XSLT extension, you have to add it to the processor's context before executing the transformation. Figure 14 illustrates how to do this with MSXML 4.0 and Visual Basic. Since the object referenced by p is added to the processor's context before calling Transform, the XSLT document can access Person's properties and functions as long as their names are qualified by the urn:person namespace, as shown here:
<xsl:transform version="1.0"
xmlns:xsl="https://www.w3.org/1999/XSL/Transform"
xmlns:obj="urn:person"
>
<xsl:output method="text"/>
<xsl:template match="/">
name: <xsl:value-of select="obj:get-name()"/>
age: <xsl:value-of select="obj:get-age()"/>
greeting: <xsl:value-of select="obj:SayHello('Michi')"/>
</xsl:template>
</xsl:transform>
The model in the .NET implementation is almost identical except now you're calling .NET objects. The main difference is how you associate the object with the processor's context. You do this through the XsltArgumentList class, as shown here:
XslTransform xslt = new XslTransform();
xslt.Load(stylesheet);
XPathDocument doc = new XPathDocument(filename);
XsltArgumentList xslArg = new XsltArgumentList();
//Add a Person object
Person obj = new Person();
obj.name = "Michi";
obj.age = 6;
xslArg.AddExtensionObject("urn:person", obj);
xslt.Transform(doc, xslArg, Console.Out);
This XSLT document, which is used to call into this .NET extension object, would look identical to the one shown in the previous MSXML example.
Wrap-up
XSLT is a powerful functional programming language that can make difficult tasks easy and easy tasks tough. In certain situations, a combination of XSLT's functional programming model and portions of custom code written in imperative languages like JScript, C#, or Visual Basic .NET can simplify the overall XSLT document. Microsoft's XSLT implementations, in both MSXML and .NET, provide excellent support for creating this type of hybrid XSLT application.
Send questions and comments for Aaron to xmlfiles@microsoft.com. |