Getting Value from XSL Parameters
June 22, 2000
If you have ever written an ASP page, you've probably engaged in two activities: reading information from some external resource and saving other (generally modified) information to some other resource, which has only incidental importance to the ultimate output that gets sent to the client. Some of this is state information—such as storing information that may have come from a query string request—while in other cases, you're updating a database from a form post, or perhaps storing a file.
When I started playing with the older XSL (the one based on Microsoft's December 1998 submission for XSL and XML Patterns), I quickly realized how much power it had to radically simplify the production of output code. An XSL filter (as I call the stylesheets) could turn a database call into a table, could safely incorporate boiler-plate HTML code, and could even perform simple processing tasks.
However, XSL had some limitations for serious, heavy-duty lifting. The biggest issue was parameters; there was no clean way to change an XSL structure without having an intimate acquaintance with the underlying code in the stylesheet. All too often, the only way around passing information into an XSLT transformation was to put parameters on the XML input. Another issue was retaining information internally about the current state of the processor. If you had XML markup that would be sent to the output stream, you couldn't save it temporarily and use it in more than one place. Finally, there was no real way to save state within the XSL environment itself; you had to take the results from the XSL transformation, filter that out with a document object model (DOM) call, and effectively lose any intermediate states that may have taken place within the filter.
The Technology Preview XML parser provides support for the newer XSL-Transformation specification (located at http://www.w3.org/TR/xslt). This newer spec contains a number of innovative features that together make XSLT far more capable than Microsoft's December 1998 version of XSL. The spec also makes XSLT a serious competitor for scripting languages as a server tool. In addition to this, Microsoft has also introduced a new way to integrate scripting code (one that is compliant with the W3C standards, by the way), which may end up changing the way we think about server-side programming altogether.
In an ideal world, an XSLT filter is a "black box"—a filter that takes one or more inputs and creates one or more outputs. You should not need to know what goes on inside that box. In a function, passing information into the black box is accomplished with parameters. In an XSL document, the process is also accomplished with parameters—specifically <xsl:param> elements. These XSLT elements serve some of the same purposes in the declarative world (a world where data streams in, gets transformed, then streams out) as function parameters serve in the procedural world (where everyday code reigns supreme). However, the two are not identical.
XSLT also includes variables. The principle difference between a variable and a parameter is that a parameter is designed to assign content from some source external to its current template, while a variable is assigned internally. This again has an analogy in a traditional function: The variables that are declared within the body of a function are typically private, and your code calling the function shouldn't know or care that such variables exist. An XSLT variable in that respect is similarly private.
The best way to think about an xsl:param or an xsl:variable element is that once you associate a parameter with a name and content, that element is immutable; you may as well have written it to a write-once read-many CD-ROM. That spot is essentially taken, and if you try to create another parameter with the same name (at least, within the same scope; more on that later), the parser will reject your attempts on the spot. This varies dramatically from parameters in most procedural languages, which can easily be modified internally to the function.
Moreover, xsl:param and xsl:variable elements actually have two distinct ways of assigning value. These can be seen in the formal description of the xsl:param object from the W3C specification:
<xsl:param name = qname select = expression> <!-- Content: template --> </xsl:param> <xsl:variable name = qname select = expression> <!-- Content: template --> </xsl:variable>
The most obvious way to set the content of an xsl:param element is to assign elements to its internal content (the material between the <xsl:param> and </xsl:param> tags). As a simple case, you can just assign text to the parameter. For example, if you wanted to pass a person's last name to a parameter, you would just include it as internal content:
Once you define a parameter in this way, you can reference it later in the XSLT by prefixing its name with a dollar sign ($) (This holds for variables as well, by the way.). For example, to insert it into a <last_name> element in a template, you'd use the xsl:value-of element:
However, you could also assign XML tags to the parameter. For example, let's say you wanted to create a complete record of a person. You could do so through a param statement:
<xsl:param name="person"> <record> <first_name>Kurt</first_name> <last_name>Cagle</last_name> <vocation>Writer</vocation> </record> </xsl:param>
Dealing with the structure in this format is a little more complicated, but not much. If you attempt to use the expression <xsl:value-of select="$person">, then all that you will write to the output stream is:
which is probably not what you intended. The <xsl:copy-of select="$person> statement, on the other hand, lets you output whole XML structures:
<record> <first_name>Kurt</first_name> <last_name>Cagle</last_name> <vocation>Writer</vocation> </record>
Here's where things get interesting. An important point to remember about parameters (and their siblings, variables) is that the parser automatically interprets a parameter or variable when it encounters it, instead of storing the matching patterns for later interpretation. If the parser encounters an XSL expression within a variable or parameter, that expression is automatically evaluated at that time and written into the space associated with the parameter's name. This curious little fact makes parameters and variables quite useful.
For example, consider the following:
<xsl:param name="first_name">Kurt</xsl:param> <xsl:param name="last_name">Cagle</xsl:param> <xsl:param name="vocation">Writer</xsl:param> <xsl:param name="record"> <first_name><xsl:value-of select="$first_name"/></first_name> <last_name><xsl:value-of select="$last_name"/></last_name> <vocation><xsl:value-of select="$vocation"/></vocation> </xsl:param>
The record parameter evaluates the values of the previous three parameters so that, regardless of what the parameters are set to, the record will reflect the update values from the parameters. It now becomes pretty easy to enter simple parameters into an XSL document to build more complex objects.
Result Tree Fragments
However, there is a major limitation to this. One of the more hotly contested debates within the W3C's XSLT committee dealt with the issue of how to treat the content of a variable. The content is that portion of an element that lies within the tags of the element (in other words, the "text" of the element). When you perform a select statement for an XSLT variable, the select statement assigns a node-set to the variable, which can be manipulated as if it were any other node-set in the document.
On the other hand, the W3C decided that even if the content within a node were well-formed, the danger of the content being ill-formed was sufficient to specify that content was its own unique type, called a result tree fragment. Such a fragment is considered a second-class citizen in XSLT. While you can perform some operations (such as getting the name of the top-most node), you cannot apply most of the XPath navigation operators to it.
Thus, for the record described above, the expression:
is illegal according to the XSLT spec, even if you had assigned the record from a select value acting on the existing input stream:
<xsl:variable name="record select="//record"/>
Then the same statement would be perfectly legal.
Note that the current behavior of the MSXML Technology Preview parser does not recognize this limitation. A fragment created from the content of a variable element is considered to have the same privileges as one from a select statement. This is an important feature—in great part, because it provides a way to access elements from the result of a named template operation (a template that can be invoked, like a function, rather than matched through a search pattern; this will be discussed in an upcoming column). If the template sorted and filtered elements, for example, this would let you iterate over the sorted elements, rather than over the unfiltered set—an important feature for creating "paged" output.
To comply with the W3C behavior in this respect, Microsoft will likely end up deprecating the use of this key functionality in the final MSXML Web release, but will probably make it available in the final MSXML Web release via an extension function as follows:
<xsl:value-of select="msxsl:node set($record)/first_name"/>
Furthermore, Microsoft is pushing to get this functionality included in the XSLT 2.0 specification that is in discussion because the ability to treat content as full-status node-sets significantly extends what XSLT can do.
The select attribute provides a second way to populate a parameter or variable. The select statement with a parameter works in exactly the same way that a select statement works anywhere else: If the expression in the attribute can be interpreted as an XPath expression, the parser will attempt to locate all nodes that satisfy the given expression, then place them into the variable. As an example, suppose that your input stream consisted of a number of records showing the names and vocations of a large number of people. You could set up a variable (there's no real need for a parameter here) to retrieve all people who are writers in the initial stream:
<xsl:variable name="writers" select="//record[vocation='Writer']">
Once you have this variable, you essentially have a node fragment with $writers as its root, and only those people who are also writers as children. If you wanted to iterate through this set, as long as you remembered that $writers was the root node, you could easily set the context to each one in turn with either an <xsl:apply-templates> or <xsl:for-each> element:
<xsl:template match="/"> <ul> <xsl:for-each select="$writers/*"> <li><xsl:value-of select="first_name"/><xsl:text> </xsl:text></xsl:value-of select="last_name"/></li> </xsl:for-each> </ul> </xsl:template>
This will create an HTML bulleted list of all of the writers in the initial stream. Note that one effect of this is that the context (where you are in the document) no longer depends on the immediate context of the template. By the way, the expression <xsl:text> </xsl:text> places a space character into the output. It is a little ugly, but it is important to remember that XSLT is optimized for generated XML, not raw text.
The select statement will automatically override the contents of a variable or parameter tag, though technically it's considered an XSLT violation to use both. Fortunately, it doesn't throw an error in the MSXML parser, as there are times when it is convenient to have both.
You can also use the select statement to evaluate expressions or to include text—although, in the latter case, you have to be careful. The XSLT parser will automatically interpret any text found within a select attribute as part of an XPath expression unless that text is specifically double quoted (with single quotes inside of double quotes, or vice versa). Thus, you could set the vocation field to "writer" as follows:
<xsl:param name="vocation" select="'writer'"/>
Evaluating expressions is a little more complex, but not by much. It is useful to think of the select attribute as the window to any XPath expression, based on the current context of the element. In addition to being able to navigate the primary DOM, XPath includes a number of functions that can be used to help evaluate expressions, and these expressions can include arithmetic, string, or other operations that significantly extend the capabilities of XSLT. For example, suppose you had an XML structure that gave the listings of items in an invoice, including the number of any given item and the cost of any one of these items (as shown below).
<lineItems> <lineItem> <code>42AC5</code> <title>Loopy Fruit Cereal</title> <amount>12</amount> <cost>4.25</cost> </lineItem> <lineItem> <code>H343A</code> <title>MicroSecond Rice</title> <amount>14</amount> <cost>2.35</cost> </lineItem> <lineItem> <code>EA198</code> <title>Crescent Toothpaste</title> <amount>18</amount> <cost>1.95</cost> </lineItem> </lineItems>
You could then assign the total cost of the entire set of line items to a variable by using some of the built-in functions intrinsic to XPath:
<xsl:variable name="lineItemSubTotals"> <xsl:for-each select="//lineItem"> <subTotal><xsl:value-of select="number(amount)*number(cost)"/></subTotal> </xsl:for-each> </xsl:variable> <xsl:variable name="lineItemsTotal"> <xsl:value-of select="sum($lineItemSubTotals/subTotal)"/> </xsl:variable>
In this case the, variable $lineItemSubTotals creates a subordinate XML structure that consists of <subTotal> elements and assigns them to the lineItemTotals variable. For the example above, it's essentially the same as:
<xsl:variable name="lineItemTotals"> <subTotal>51</subTotal> <subTotal>32.9</subTotal> <subTotal>35.1</subTotal> </xsl:variable>
The second variable, $lineItemsTotal, uses the sum() XPath function to add the item totals together, then assigns the resulting value, 119, to the lineItemsTotal variable. This differs from a procedural approach—not in the algorithm (which is quite simple), but because $lineItemsTotal, the variable containing the sum of each line-item entry, isn't consumed in the transaction. You can also change the output of the record so that it reads in the line item, then creates a new line item with the added field, yet retains the subtotal information to pull the total sum:
<xsl:variable name="newLineItems"> <xsl:for-each select="//lineItem""> <lineItem> <xsl:copy-of select="*"/> <subTotal><xsl:value-of select="number(amount)*number(cost)"/></subTotal> </lineItem> </xsl:for-each> </xsl:variable> <xsl:variable name="lineItemsTotal"> <xsl:value-of select="sum($newLineItems/subTotal)"/> </xsl:variable>
This set of code creates a new set of line items and performs the sum, illustrating ways that an XSL filter can essentially enlarge (or, for that matter, reduce) an existing XML document to incorporate intermediate processing information.
To use a parameter, you need to set it. And given that the XSLT described here is meant to operate within an ASP environment, you will have to set parameters in ASP as well. There are a number of ways to do this, depending on how you transmit the information to the server in the first place, although in most cases they involve the XML DOM.
In almost all cases, setting the actual parameter involves either setting the select property of the parameter or assigning text to that property. For example, you can set the properties to create a new record for the addRecord.xsl filter by using some DOM calls in Visual Basic Scripting Edition (VBScript)—assuming that the first_name, last_name, and vocation properties are sent as a query string:
<% firstName=request.queryString("first_name") lastName=request.queryString("last_name") vocation=request.queryString("vocation") set addRecordFilter=createObject("MSXML2.DOMDocument") addRecordFilter.async=false addRecordFilter.load server.mapPath("addRecord.xsl") set firstNameNode=addRecordFilter.selectSingleNode("//xsl:param[@name= 'first_name']") firstNameNode.setAttribute "select","'"+firstName+"'" set lastNameNode=addRecordFilter.selectSingleNode("//xsl:param[@name= 'last_name']") lastNameNode.setAttribute "select","'"+lastName+"'" set vocationNode=addRecordFilter.selectSingleNode("//xsl:param[@name= 'vocation']") vocationNode.setAttribute "select","'"+vocation+"'" set stubDoc=createObject("MSXML.DOMDocument") stubDoc.loadXML "<stub/>" response.write stubDoc.transformNode(addRecordFilter) %>
The expression "//xsl:param[@name='first_name']," for example, accesses the parameter with the name first_name, and replaces the contents of the select attribute with the values retrieved from the query string, wrapped in single quotes to force them to be evaluated as strings rather than as XPath expressions.
You can also simplify the process somewhat by using the XSLTemplate, which is part of the XML Technology Preview parser. The processor (derived from the template) provides a number of interfaces for updating parameters and objects:
<% firstName=request.queryString("first_name") lastName=request.queryString("last_name") vocation=request.queryString("vocation") set addRecordFilter=createObject("MSXML2.DOMDocument") addRecordFilter.async=false addRecordFilter.load server.mapPath("addRecord.xsl") set template=createObject("MSXML2.XSLTemplate") set template.stylesheet=addRecordFilter set processor=template.createProcessor processor.AddParameter "first_name",firstName processor.AddParameter "last_name",lastName processor.AddParameter "covation",vocation set stubDoc=createObject("MSXML.DOMDocument") stubDoc.loadXML "<stub/>" processor.input=stubDoc processor.transform processor.output=response %>
Note that the XSLT document actually doesn't care anything about the XML document that it's transforming. The document's only purpose is to force output. This is to demonstrate that you don't specifically need to use the primary input stream when working with XSLT. Again, this makes more sense with named templates than it does with variables, which is a topic for future columns. The real work is done with the document() and persistDocument() functions, which load in the records' structures, add the node (ordered) into the records, then save those structures.
The purpose here is to reduce the application-specific code as much as possible, so that you can avoid extensive code rework. One way to do this is to enumerate through both the queryString and Form collection to retrieve possible parameter values, then assign a value to a parameter if such a parameter exists.
This also opens up an interesting possibility: You can pass the name of the XSL filter as an argument in this same manner, contained in the variable "filter." In this way, the same ASP file can handle the processing on any number of XSLT filters in a much more generic fashion.
<% ' XSLServer.asp function main() xslFilter=request("filter") if right(xslFilter,4,1)<>"." Then xslFilter=xslFilter+".xsl" end if set xslDoc=createObject("MSXML2.DOMDocument") xslDoc.async=false xslDoc.load server.mapPath(xslFilter+".xsl") for each key in request.form set paramNode=xslDoc.selectSingleNode("//xsl:param[name='"+key+"']") if not (paramNode is nothing) then paramNode.setAttribute "select","'"+request(key)+"'" end if next for each key in request.queryString set paramNode=xslDoc.selectSingleNode("//xsl:param[name='"+key+"']") if not (paramNode is nothing) then paramNode.setAttribute "select","'"+request(key)+"'" end if next set stubDoc=createObject("MSXML.DOMDocument") stubDoc.loadXML "<stub/>" response.write stubDoc.transformNode(xslDoc) end main main %>
Note that a query string or form parameter will be mapped to an XSLT parameter only if the XSLT parameter already exists. This also provides a way to pass the name of an XSL file to the filter code. Simply set up a parameter called filter. Note that the code also appends the string .xsl to the filter name if no extension is otherwise provided. This touch just makes the filter seem more like a command than a file name.
Thus, the query string:
will pass that information to the addRecord.xsl filter. This gives you the basis for a reasonably powerful URL-based API convention—with a filter containing the name of the method to use, and all other query string arguments holding the associated parameters.
You can also pass XML nodes, XML node-sets (through the selection interface), and even whole DOM Documents as parameters. In this case, you simply append the element as a child to the parameter and remove the select statement:
<% ' XSLServer.asp function main() set newUserDoc=createObject("MSXML2.DOMDocument") newUserDoc.load "newUser.xml" set xslDoc=createObject("MSXML2.DOMDocument") xslDoc.async=false xslDoc.load server.mapPath("addRecord.xsl") set newRecordNode=xslDoc.selectSingleNode("//xsl:param[name='record']") newRecordNode.appendChild newUserDoc.documentElement newRecordNode.removeAttribute "select" set stubDoc=createObject("MSXML.DOMDocument") stubDoc.loadXML "<stub/>" response.write stubDoc.transformNode(xslDoc) end main main %>
This is another case where using the XSL processor makes as much, or more, sense as using the transformNode function. The principle problem with XSLT is that parsing the style sheets each time you need to perform a transformation is extremely expensive—especially given that, in many cases, you're dealing with the same XSLT script used in each case. The same functionality, rendered with the Processor object (saved in a session variable), might also be written as follows:
<% ' XSLServer.asp function main() if typename(session("addRecord"))="IXSLProcessor" then set addRecord=session("xslProc") set stubDoc=session("stubDoc") else set xslDoc=createObject("MSXML2.DOMDocument") xslDoc.async=false xslDoc.load server.mapPath("addRecord.xsl") set xslTemplate=createObject("MSXML2.XSLTemplate") set xslTemplate.stylesheet=xslDoc set addRecord=xslTemplate.createProcessor session("addRecord")=addRecord set stubDoc=createObject("MSXML.DOMDocument") stubDoc.async=false stubDoc.loadXML "<stub/>" session("stubDoc")=stubDoc end if set newUserDoc=createObject("MSXML2.DOMDocument") newUserDoc.load "newUser.xml" addRecord.addParameter "record",newUserDoc addRecord.input=stubDoc addRecord.output=response addRecord.transform end main main %>
This would do much the same as the previous two samples, but would pass a DOM tree into the XSL style sheet rather than individual strings. This can be handy when you want to pass processed nodes back into the XSLT filter.
For more information about the XSLTemplate and XSLProcess objects, see Inside MSXML3 Performance by Chris Lovett.
Variables and parameters can significantly expand what you can do with XSLT by providing a mechanism to hold interim information, giving default values for properties not passed, serving as root nodes for external documents, and exposing ways to make your style sheets a lot more dynamic. You can also use parameters in particular as vehicles for working with other components, especially in conjunction with scripting, either by providing references to them or by permitting ways to calculate device locations. Parameters should be considered a pivotal part of any XML developer's repertoire.
Kurt Cagle is a writer and developer specializing in XML and related Internet technologies. He lives with his wife and two daughters in Olympia, Washington, and can be reached either by e-mail at firstname.lastname@example.org or at his shared Web site at VB XML.