December 19, 2006

Using ms:string-compare() and the rest MS extension functions in XPath-only context

XslCompiledTransform implements the following useful MSXML extension functions. But what if you need to use them in XPath-only context - when evaluating XPath queries using XPathNavigator?

...

Function Signature and description
ms:string-compare number ms:string-compare(string x, string y[, string language[, string options]])
Performs lexicographical string comparison.
ms:utc string ms:utc(string time)
Converts the prefixed date/time related values into Coordinated Universal Time and into a fixed (normalized) representation that can be sorted and compared lexicographically.
ms:namespace-uri string ms:namespace-uri(string name)
Resolves the prefix part of a qualified name into a namespace URI.
ms:local-name string ms:local-name(string name)
Returns the local name part of a qualified name by stripping out the namespace prefix.
ms:number number ms:number(string value)
Takes a string argument in XSD format and converts it into an XPath number.
ms:format-date string ms:format-date(string datetime[, string format[, string locale]])
Converts standard XSD date formats to characters suitable for output.
ms:format-time string ms:format-time(string datetime[, string format[, string locale]])
Converts standard XSD time formats to characters suitable for output.

Here is a quick sketch on how to leverage XslCompiledTransform implementation of these functions to create custom XslContext class. The code above implments only ms:string-compare(), but other functions can be added in a similar way. Here is how you use it:

string xml = "ABCDEFGH";
XPathExpression expr = 
    XPathExpression.Compile("ms:string-compare(value[1], value[2])");
MsXsltContext ctx = new MsXsltContext();
ctx.AddNamespace("ms", "urn:schemas-microsoft-com:xslt");
expr.SetContext(ctx);
XmlDocument doc = new XmlDocument();
doc.LoadXml(xml);
XPathNavigator nav = doc.DocumentElement.CreateNavigator();
Console.WriteLine(nav.Evaluate(expr));

And here is sample MsXsltContext implementation:

using System;
using System.Xml.Xsl;
using System.Xml.XPath;
using System.Xml;
using System.Reflection;
using System.Xml.Xsl.Runtime;
using System.Globalization;

public class MsXsltContext : XsltContext
{
    // Function to resolve references to my custom functions.
    public override IXsltContextFunction ResolveFunction(string prefix, 
        string name, XPathResultType[] argTypes)
    {
        string namespaceUri = this.LookupNamespace(prefix);
        if (namespaceUri == "urn:schemas-microsoft-com:xslt")
        {
            switch (name)
            {
                case "string-compare":
                    return new MsExtensionFunction(name, 2, 4,
                        new XPathResultType[] { XPathResultType.String, 
                        XPathResultType.String, XPathResultType.String, 
                        XPathResultType.String },
                        XPathResultType.Number);
            }
        }

        return null;
    }

    public override IXsltContextVariable ResolveVariable(string prefix, 
        string name)
    {
        return null;
    }

    public override int CompareDocument(string baseUri, string nextBaseUri)
    {
        return 0;
    }

    public override bool PreserveWhitespace(XPathNavigator node)
    {
        return true;
    }

    public override bool Whitespace
    {
        get
        {
            return true;
        }
    }
}

public class MsExtensionFunction : IXsltContextFunction
{
    private XPathResultType[] argTypes;
    private XPathResultType returnType;
    private string name;
    private int minArgs;
    private int maxArgs;
    private MethodInfo method;

    public int Minargs
    {
        get
        {
            return minArgs;
        }
    }

    public int Maxargs
    {
        get
        {
            return maxArgs;
        }
    }

    public XPathResultType[] ArgTypes
    {
        get
        {
            return argTypes;
        }
    }

    public XPathResultType ReturnType
    {
        get
        {
            return returnType;
        }
    }

    public MsExtensionFunction(string name, int minArgs, 
        int maxArgs, XPathResultType[] argTypes, XPathResultType returnType)
    {
        this.name = name;
        this.minArgs = minArgs;
        this.maxArgs = maxArgs;
        this.argTypes = argTypes;
        this.returnType = returnType;
    }

    public object Invoke(XsltContext xsltContext, object[] args, 
        XPathNavigator docContext)
    {
        switch (name)
        {
            case "string-compare":
                if (method == null)
                {
                    method = typeof(XsltFunctions).GetMethod("MSStringCompare");
                }

                object[] fullArgs = new object[maxArgs];
                fullArgs[0] = ConvertToString(args[0]);
                fullArgs[1] = ConvertToString(args[1]);
                fullArgs[2] = args.Length > 2 ? ConvertToString(args[2]) : "";
                fullArgs[3] = args.Length > 3 ? ConvertToString(args[3]) : "";

                return method.Invoke(null, fullArgs);
        }
        return null;
    }

    private static string ConvertToString(object argument)
    {
        XPathNodeIterator it = argument as XPathNodeIterator;
        if (it != null)
        {
            return IteratorToString(it);
        }
        else
        {
            return ToXPathString(argument);
        }
    }

    private static string IteratorToString(XPathNodeIterator it)
    {
        if (it.MoveNext())
        {
            return it.Current.Value;
        }
        return string.Empty;
    }

    private static String ToXPathString(Object value)
    {
        string s = value as string;
        if (s != null)
        {
            return s;
        }
        else if (value is double)
        {
            return ((double)value).ToString("R", 
                NumberFormatInfo.InvariantInfo);
        }
        else if (value is bool)
        {
            return (bool)value ? "true" : "false";
        }
        else
        {
            return Convert.ToString(value, 
                NumberFormatInfo.InvariantInfo);
        }
    }
}
Don't forget to add a reference to the System.Data.SqlXml.dll.