Introduction
The XSL transformation is a powerful way of visualizing XML. The presence of extension objects makes it even more powerful. However it turns out that extension objects are very limited. They have no way of retrieving the current context of the transformation or the current document. Furthermore their return type is limited to only numbers and strings (returning an IXPathNavigable
is restricted. See here for more details). This article presents a work around for the first problem.
Why use context aware XPath functions
A simple but very powerful application of such is the eval
function. It evaluates XPath queries contained in a string or variable. For example imagine you have the following:
<xsl:variable name='vTest'>123456</xsl:variable>
<xsl:variable name= 'vQuery'>$vTest</xsl:variable>
If you take the value of vQuery
using xsl:value-of
you would get $
vTest
as a result. On the other hand <xsl:value-of select='eval($vTest)'/>
will evaluated to 123456. There is no such function built-in so we will write it our selves.
Background
To read this article you should have good knowledge in C# and the Microsoft's .NET XML parser.
How it works
It looks like Microsoft have had the idea of allowing the user to write context aware functions. Have a look at the 2 classes IXsltContextFunction
and XsltContext
in the System.Xml.Xsl
namespace. You can make your own context and function classes derived from the above 2, but you can use them only in programmatic XPath queries. An article I found in Microsoft's KB "recomends" that you use extension objects instead of IXsltContextFunction
derived classes for writing your own XPath functions. Well actually it turns out that this not a recommendation - it is the only way to do it. There is no way of adding a context aware XPath extension function to the transformation. I decompiled the code that resolves functions in an XSLT (System.Xml.Xsl.XsltCompileContext.ResolveFunction
) an here is what I got:
public virtual IXsltContextFunction ResolveFunction(string prefix,
string name, System.Xml.XPath.XPathResultType[] argTypes){
IXsltContextFunction local0;
string local1;
object local2;
MethodInfo local3;
local0 = null;
if (prefix == String.Empty)
local0 = XsltCompileContext.s_FunctionTable.get_Item(name)
as IXsltContextFunction;
else {
local1 = this.LookupNamespace(prefix);
if (local1 == "urn:schemas-microsoft-com:xslt" && name == "node-set")
local0 = XsltCompileContext.s_FuncNodeSet;
else {
local3 = this.GetExtentionMethod(local1, name, argTypes, local2);
if (local2 == null)
throw new XsltException("Xslt_ScriptInvalidPrefix", prefix);
if (local3 != null)
local0 = new FuncExtension(local2, local3);
}
}
if (local0 == null)
throw new XsltException("Xslt_UnknownXsltFunction", name);
if ((int) argTypes.Length < local0.Minargs ||
local0.Maxargs < (int) argTypes.Length)
throw new XsltException("Xslt_WrongNumberArgs", name,
Convert.ToString((int) argTypes.Length));
return local0;
}
As you can see there are 3 types of functions that the XSLT can resolve:
- Ones that are in the root
XML
namespace
- The
msxsl:node-set
function
- Functions on extension objects
From the code above, it is visible that the only way of inserting a context aware function is to put it in the static hashtable s_FunctionTable
field. As you have already guessed the System.Xml.Xsl.XsltCompileContext
is a protected class and so is the s_FunctionTable
. To access it, we will use reflection:
static Hashtable XslFunctionTable
{
get
{
Assembly xmlAssembly = Assembly.LoadWithPartialName("System.Xml");
Type tXsltCompileContext = xmlAssembly.GetType
("System.Xml.Xsl.XsltCompileContext");
FieldInfo finf = tXsltCompileContext.GetField("s_FunctionTable",
BindingFlags.NonPublic | BindingFlags.Static);
return finf.GetValue(null) as Hashtable;
}
}
Once we have the hash table we can easily add or remove functions from it. The hashtable is initialized in the static constructor of System.Xml.Xsl.XsltCompileContext
so we should avoid adding and removing functions in static constructors
Drawbacks
This is actually a hack. It works fine on .NET framework 1.0 but it may not work in future releases of the framework. I hope Microsoft fixes this problem in future. Also you can add functions only in the root XML
namespace.
Possible improvements
The System.Xml
assembly is located using it's partial name. This is not good because future versions of the .NET framework will use newer XML parsers and the hack wont work. The full name should be used instead.