On returning nodeset from XSLT extension function

| 7 Comments

Update: This hack is about .NET 1.X. In .NET 2.0 you don't need it. In .NET 2.0 with XslCompiledTransform class you can return a nodeset as XPathNodeNavigator[].

As all we know, unfortunately there is a confirmed bug in .NET Framework's XSLT implementation, which prevents returning a nodeset from an XSLT extension function. Basically the problem is that XSLT engine expects nodeset resulting from an extension function to be an object of internal ResetableIterator class. Full stop :(

Some workarounds were discovered, first one - to create new interim DOM object and query it by XPath, what returns instance of ResetableIterator class. Main deficiency - loss of nodes identity, because returned nodes belong to the interim DOM tree, not to input nodeset. Another workaround, discovered by Dimitre Novatchev is to to run interim XSL transformation within an extension function - this also allows to create instance of ResetableIterator class to return.

This morning I've found another workaround, which doesn't require creation of any interim objects. It's frontal attack and someone would call it a hack, but I wouldn't. Here it is. There is internal XPathArrayIterator class in System.Xml.XPath namespace, which represents XPathNodeIterator over ArrayList and also kindly implements our beloved ResetableIterator class. So why not just instantiate it by reflection and return from an extension function, huh?

Assembly systemXml = typeof(XPathNodeIterator).Assembly;
Type arrayIteratorType = 
    systemXml.GetType("System.Xml.XPath.XPathArrayIterator");
return (XPathNodeIterator)Activator.CreateInstance(
    arrayIteratorType, 
    BindingFlags.Instance | BindingFlags.Public |
    BindingFlags.CreateInstance,
    null, new object[]{myArrayListofNodes}, 
    null);

Below is proof-of-concept extension function to filter distinct nodes from a nodeset:

Extension function impl and test class:

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

namespace Test2 {
  class Test { 
    static void Main(string[] args){
        XPathDocument doc = new XPathDocument(args[0]);
        XslTransform trans = new XslTransform(); 
        trans.Load(args[1]);
        XsltArgumentList argList = new XsltArgumentList();
        argList.AddExtensionObject("http://foo.com", 
          new MyXsltExtension());
        trans.Transform(doc, argList, new StreamWriter(args[2]));
    }
  }    
  public class MyXsltExtension {
    public XPathNodeIterator distinct(XPathNodeIterator nodeset) {
      Hashtable nodelist = new Hashtable();
      while(nodeset.MoveNext()) {
        if(!nodelist.Contains(nodeset.Current.Value)) {
          nodelist.Add(nodeset.Current.Value, nodeset.Current); 
        }
      }
      Assembly systemXml = typeof(XPathNodeIterator).Assembly;
      Type arrayIteratorType = 
        systemXml.GetType("System.Xml.XPath.XPathArrayIterator");
      return (XPathNodeIterator)Activator.CreateInstance(
          arrayIteratorType, 
          BindingFlags.Instance | BindingFlags.Public | 
          BindingFlags.CreateInstance,
          null, new object[]{new ArrayList(nodelist.Values)}, 
          null);
    }
  }
}

Source xml doc (exsl:distinct()'s example):

<doc>
   <city name="Paris"
         country="France"/>
   <city name="Madrid"
         country="Spain"/>
   <city name="Vienna"
         country="Austria"/>
   <city name="Barcelona"
         country="Spain"/>
   <city name="Salzburg"
         country="Austria"/>
   <city name="Bonn"
         country="Germany"/>
   <city name="Lyon"
         country="France"/>
   <city name="Hannover"
         country="Germany"/>
   <city name="Calais"
         country="France"/>
   <city name="Berlin"
         country="Germany"/>
</doc>

Stylesheet:

<xsl:stylesheet 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"
xmlns:ext="http://foo.com" extension-element-prefixes="ext">    
    <xsl:template match="/">
        <distinct-countries>
            <xsl:for-each select="ext:distinct(//@country)">
            <xsl:value-of select="."/>   
            <xsl:if test="position() != last()">, </xsl:if>     
         </xsl:for-each>
      </distinct-countries>
    </xsl:template>
</xsl:stylesheet>

And the result is:

<distinct-countries>
Germany, Austria, Spain, France
</distinct-countries>

I like it. Comments?

Related Blog Posts

7 Comments

Your code seems ok, except for
doc.LoadXml( "Cool" );

Any exceptions? Why do you think it doesn't work?

I would like to dynamically generate a hyperlink from C# and thought that your code would be the way to do it, but I'm having no luck.

Can you tell me if I'm on the right track with the following.

public XPathNodeIterator Test( )
{
XmlDataDocument doc = new XmlDataDocument( );

doc.LoadXml( "Cool" );

XPathNavigator nav = doc.CreateNavigator( );
XPathNodeIterator iter = nav.Select( "x" );

ArrayList nodes = new ArrayList( );

while( iter.MoveNext( ) )
{
nodes.Add( iter.Current );
}

Assembly systemXml = typeof(XPathNodeIterator).Assembly;

Type arrayIteratorType = systemXml.GetType("System.Xml.XPath.XPathArrayIterator");

return (XPathNodeIterator)Activator.CreateInstance(
arrayIteratorType,
BindingFlags.Instance | BindingFlags.Public | BindingFlags.CreateInstance,
null, new object[]{ nodes }, null);
}

Great site guys... Keep up the good work :)

Sorry, Dare :) Found out this trick only yesterday.

I don't know whether to be mad at you or jump for joy. I now have to rewrite my article again. :)

Yeah, of course you are right, it's only workaround for .NET XSLT impl.
At least we have solutions to choose now, not bad anyway.

Good work, Oleg!

Perhaps it should be mentioned that your approach works nice with .Net but is more difficult (or is this possible at all) to implement with MSXML3/4.

The solution that I sent sometimes ago works the same, regardless whether the transformation is performed in .Net or with MSXML3/4.


=====
Cheers,

Dimitre Novatchev.
http://fxsl.sourceforge.net/ -- the home of FXSL