XSLT transformations with MSXML

February 5, 2004 19:47

Anyone who uses MSXML knows that it is, amongst other things, a fast, standards compliant, XML DOM/SAX parser and XSLT engine. There are other XML parsers/XSLT engines that are equally brilliant or better (saxon and xsltproc spring to mind), but I am interested in MSXML for the purposes of this article. This article shows how to get the most out of MSXML by passing parameters to an XSLT transform and retreiving the results in the the most desirable encoding, UTF-8.

There are some caveats to performing an internationalised XSLT transform with MSXML, which I will try to address.

This article is most relevant to ASP developers. The code is written in javascript, although it can be converted to VB and VBScript, and any other language that has COM/OLE support eg. PHP, Perl, C++.

I will assume that you are reasonably aux fait with creating a simple XSLT transformation, and so will skip some of the basics in favour of the more interesting bits. If not, take a look here and here. There are many articles and tutorials that demonstrate the basics of creating an XSLT transform using MSXML. They usually go something like this:

test.xml

<?xml version="1.0" encoding="UTF-8"?>
<results>
	<result>
		<foo>1</foo>
		<bar>2</bar>
	</result>
	<result>
		<foo>3</foo>
		<bar>4</bar>
	</result>
	<result>
		<foo>5</foo>
		<bar>6</bar>
	</result>
</results>

test.xsl

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
	
	<xsl:output method="html" encoding="utf-8" omit-xml-declaration="yes"/>

	<xsl:template match="/">
		<xsl:apply-templates />
	</xsl:template>
	
	<xsl:template match="results">
		<table cellpadding="4" cellspacing="1" border="0" style="background-color:#000">
			<tr>
				<td style="background-color:#fff">Foo</td>
				<td style="background-color:#fff">Bar</td>
			</tr>
			
			<xsl:apply-templates />
		</table>
	</xsl:template>
	
	<xsl:template match="result">
		<tr>
			<xsl:apply-templates />
		</tr>
	</xsl:template>
	
	<xsl:template match="foo">
		<td style="background-color:#fff">
			<xsl:value-of select="."/>
		</td>
	</xsl:template>
	
	<xsl:template match="bar">
		<td style="background-color:#fff">
			<xsl:value-of select="."/>
		</td>
	</xsl:template>
	
</xsl:stylesheet>
I should use an XML schema or DTD for my XML, but that's outside the scope of this article. Next, I will need a function to transform the XML against the XSLT:
var PROGID_FREETHREADEDDOM = "Msxml2.FreeThreadedDOMDocument.3.0";
var PROGID_DOM = "Msxml2.DOMDocument.3.0";
var PROGID_TEMPLATE = "Msxml2.XSLTemplate.3.0";

function XslTransform(sXmlPath, sXslPath)
{
	var oXml = new ActiveXObject(PROGID_DOM);
	
	oXml.async = false;
	oXml.load(sXmlPath);
	
	var oXsl = new ActiveXObject(PROGID_FREETHREADEDDOM);
	
	oXsl.async = false;
	oXsl.load(sXslPath);

	return oXml.transformNode(oXsl);
}

This function takes two arguments:

sXmlPath - The physical path to the XML document.
sXslPath - The physical path to the XSL document.

I can call this function like this:

var result = XslTransform("c:\temp\test.xml", "c:\temp\test.xsl");

This will assign the result of the transform (a string) to the variable result.

The result will look something like this:

<table cellpadding="4" cellspacing="1" border="0" style="background-color:#000">
<tr>
<td style="background-color:#fff">Foo</td>
<td style="background-color:#fff">Bar</td>
</tr>
<tr>
<td style="background-color:#fff">1</td>
<td style="background-color:#fff">2</td>
</tr>

<tr>
<td style="background-color:#fff">3</td>
<td style="background-color:#fff">4</td>
</tr>
<tr>
<td style="background-color:#fff">5</td>
<td style="background-color:#fff">6</td>
</tr>
</table>

The function that I created previously was useful, but let's say I want to pass the text colour and background colour for the output as parameters, rather than hard-coding them in the XSLT stylesheet.

This cannot be acheived using the IXmlNode.transformNode() method, rather I have to use use MSXML's IXSLProcessor.

So, I have changed my XSL document to accept two parameters, background-color and color. I have updated the definition of my HTML table to use these parameters. My XSL document now looks like this:

test.xsl

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
	
	<xsl:output method="html" encoding="utf-8" omit-xml-declaration="yes"/>
	
	<xsl:param name="background-color">#ffffff</xsl:param>
	<xsl:param name="color">#000000</xsl:param>

	<xsl:template match="/">
		<xsl:apply-templates />
	</xsl:template>
	
	<xsl:template match="results">
		<table cellpadding="4" cellspacing="1" border="0" style="background-color:#000">
			<tr>
				<td style="background-color:{$background-color};color:{$color}">Foo</td>
				<td style="background-color:{$background-color};color:{$color}">Bar</td>
			</tr>
			
			<xsl:apply-templates />
		</table>
	</xsl:template>
	
	<xsl:template match="result">
		<tr>
			<xsl:apply-templates />
		</tr>
	</xsl:template>
	
	<xsl:template match="foo">
		<td style="background-color:{$background-color};color:{$color}">
			<xsl:value-of select="."/>
		</td>
	</xsl:template>
	
	<xsl:template match="bar">
		<td style="background-color:{$background-color};color:{$color}">
			<xsl:value-of select="."/>
		</td>
	</xsl:template>
	
</xsl:stylesheet>

Notice the <xsl:param /> elements near the top. Their values specify the defaults that will be used if we do not pass a parameter of the same name to the transform.

I have updated my XslTransform function to use an IXSLProcessor object. The IXSLProcessor can be created by calling oTemplate.createProcessor(), where oTemplate is a IXSLTemplate object.

I have added an extra parameter to my XslTransform function, aParams. aParams is an array of parameters to be passed to the XSLT transformation. This is analogous to passing a set of parameters to a subroutine.

My function now looks like this:

var PROGID_FREETHREADEDDOM = "Msxml2.FreeThreadedDOMDocument.3.0";
var PROGID_DOM = "Msxml2.DOMDocument.3.0";
var PROGID_TEMPLATE = "Msxml2.XSLTemplate.3.0";

function XslTransform(sXmlPath, sXslPath, aParams)
{
	var oXml = new ActiveXObject(PROGID_DOM);
	
	oXml.async = false;
	oXml.load(sXmlPath);
	
	var oXsl = new ActiveXObject(PROGID_FREETHREADEDDOM);
	
	oXsl.async = false;
	oXsl.load(sXslPath);
	
	var oTemplate, oProc;
	
	oTemplate = new ActiveXObject(PROGID_TEMPLATE);
	oTemplate.stylesheet = oXsl; 
	oProc = oTemplate.createProcessor;
	 
	oProc.input = oXml; 

	if(typeof(aParams) == 'object')
		for(var i=0; i<aParams.length; i++)
			oProc.addParameter (aParams[i][0], aParams[i][1]);
	
	oProc.transform();

	return oProc.output;
}
I can now call my function like this:
var aParams = new Array();
aParams[0] = ["background-color", "#cc0000"];
aParams[1] = ["color", "#ffffff"];

var result = XslTransform("c:\temp\test.xml", "c:\temp\test.xsl", aParams);

Again, result will contain the result of the XSLT transform, which will look something like this:

<table cellpadding="4" cellspacing="1" border="0" style="background-color:#000">
<tr>
<td style="background-color:#cc0000;color:#ffffff">Foo</td>
<td style="background-color:#cc0000;color:#ffffff">Bar</td>
</tr>
<tr>
<td style="background-color:#cc0000;color:#ffffff">1</td>
<td style="background-color:#cc0000;color:#ffffff">2</td>
</tr>

<tr>
<td style="background-color:#cc0000;color:#ffffff">3</td>
<td style="background-color:#cc0000;color:#ffffff">4</td>
</tr>
<tr>
<td style="background-color:#cc0000;color:#ffffff">5</td>
<td style="background-color:#cc0000;color:#ffffff">6</td>
</tr>
</table>

Hopefully, you can see that the values I passed to the transform have been used successfully.

There is a problem that I must deal with. When accessing the output property of IXSLProcessor, MSXML helpfully converts the result of the tranform into a UTF-16 encoded string (COM uses the BSTR type, a double-byte-per-character string, internally). Although MSXML remained true to the request to output the result as UTF-8 (see the encoding attribute of the xsl:output element in our XSL stylesheet), I can now only access it as a UTF-16 string. This will mess things up somewhat if I need, say, UTF-8.

The Microsoft documentation says that the ISXLProcessor "output property can be any object/interface that supports IStream, IPersistStream, DOMDocument, ASP IResponse, ADODB.Stream, or IMXWriter".

So if I use an ADODB.Stream in binary mode, I should be able to preserve the desired encoding of the transform output, as specified in the XSL stylesheet.

I have modified my XslTransform function to return an ADODB.Stream object instead of a string.

var PROGID_FREETHREADEDDOM = "Msxml2.FreeThreadedDOMDocument.3.0";
var PROGID_DOM = "Msxml2.DOMDocument.3.0";
var PROGID_TEMPLATE = "Msxml2.XSLTemplate.3.0";

function XslTransform(sXmlPath, sXslPath, aParams)
{
	var oXml = new ActiveXObject(PROGID_DOM);
	
	oXml.async = false;
	oXml.load(sXmlPath);
	
	var oXsl = new ActiveXObject(PROGID_FREETHREADEDDOM);
	
	oXsl.async = false;
	oXsl.load(sXslPath);
	
	var oTemplate, oProc;
	
	oTemplate = new ActiveXObject(PROGID_TEMPLATE);
	oTemplate.stylesheet = oXsl; 
	oProc = oTemplate.createProcessor;
	 
	oProc.input = oXml; 

	if(typeof(aParams) == 'object')
		for(var i=0; i<aParams.length; i++)
			oProc.addParameter (aParams[i][0], aParams[i][1]);
	
	var oStream = new ActiveXObject ("ADODB.Stream");
	
	oStream.type = 1; //binary
	oStream.mode = 3; //read/write 
	oStream.open();
	
	oProc.output = oStream;
	oProc.transform();

	return oStream;
}

So the return value of the function is now an ADODB.Stream containing the binary result of the XSLT transformation. In our case, the ADODB.Stream will contain the binary representation of a UTF-8 encoded string.

I have created a new function to write the contents of the ADODB.Stream to the ASP Response object.

function WriteStream(oStream)
{
	oStream.Position = 0;
	
	if(oStream.Size > 0)
		Response.BinaryWrite(oStream.Read());
}

I use the BinaryWrite method of the Response object.

So, I can now call my function like this:

var aParams = new Array();
aParams[0] = ["background-color", "#cc0000"];
aParams[1] = ["color", "#ffffff"];

WriteStream(XslTransform(Server.MapPath("test.xml"), Server.MapPath("test.xsl"), aParams));

The result of of the XSLT transformation gets written out straight to the client, correctly encoded as UTF-8. Alternatively, I could write the result to a file using the ADODB.Stream saveToFile method.