SharePoint XSLT Web Part

After my previous post on XSLT processing, what else could follow? Of course, an XSLT web part for SharePoint! Smile

Here I want to solve a couple of problems:

  • Allow the usage of XSLT 2.0;
  • Have a more flexible parameter passing mechanism than <ParameterBindings>;
  • Make the XSLT extension mechanism (parameters, functions) more usable.

Similar to XsltListViewWebPart and the others, this web part will query SharePoint and return the results processed by a XSLT style sheet. I am going to built on top of the classes introduced in the last post. Here is the SPCustomXsltWebPart (please, do give it a better name…):

public enum XsltVersion

{

    Xslt1 = 1,

    Xslt2 = 2

}

 

public class SPCustomXsltWebPart : WebPart, IWebPartTable

{

    private static readonly Regex parametersRegex = new Regex(@"@(\w+)\b", RegexOptions.IgnoreCase);

 

    [NonSerialized]

    private DataTable table;

    [NonSerialized]

    private IOrderedDictionary parameters;

 

    public SPCustomXsltWebPart()

    {

        this.AddDefaultExtensions = true;

        this.RowLimit = Int32.MaxValue;

        this.Parameters = new ParameterCollection();

        this.XsltVersion = XsltVersion.Xslt1;

    }

 

    [Category("XSLT")]

    [Personalizable(PersonalizationScope.Shared)]

    [WebBrowsable(true)]

    [WebDisplayName("XSL Version")]

    [WebDescription("The XSLT version")]

    [DefaultValue(XsltVersion.Xslt1)]

    public XsltVersion XsltVersion { get; set; }

 

    [Category("XSLT")]

    [Personalizable(PersonalizationScope.Shared)]

    [WebBrowsable(true)]

    [WebDisplayName("XSL Link")]

    [WebDescription("The URL of a file containing XSLT")]

    [DefaultValue("")]

    public String XslLink { get; set; }

 

    [Category("XSLT")]

    [Personalizable(PersonalizationScope.Shared)]

    [WebBrowsable(true)]

    [WebDisplayName("XSL")]

    [WebDescription("The XSLT content")]

    [DefaultValue("")]

    [PersistenceMode(PersistenceMode.InnerProperty)]

    public String Xsl { get; set; }

 

    [Category("Query")]

    [Personalizable(PersonalizationScope.Shared)]

    [WebBrowsable(true)]

    [WebDisplayName("Query")]

    [WebDescription("The CAML query")]

    [DefaultValue("")]

    [PersistenceMode(PersistenceMode.InnerProperty)]

    public String Query { get; set; }

 

    [Category("Query")]

    [Personalizable(PersonalizationScope.Shared)]

    [WebBrowsable(true)]

    [WebDisplayName("Row Limit")]

    [WebDescription("The row limit")]

    [DefaultValue(Int32.MaxValue)]

    public UInt32 RowLimit { get; set; }

 

    [Category("Query")]

    [Personalizable(PersonalizationScope.Shared)]

    [WebBrowsable(true)]

    [WebDisplayName("Lists")]

    [WebDescription("The target lists")]

    [DefaultValue("")]

    public String Lists { get; set; }

 

    [Category("Query")]

    [Personalizable(PersonalizationScope.Shared)]

    [WebBrowsable(true)]

    [WebDisplayName("Webs")]

    [WebDescription("The target webs")]

    [DefaultValue("")]

    public String Webs { get; set; }

 

    [Category("Query")]

    [Personalizable(PersonalizationScope.Shared)]

    [WebBrowsable(true)]

    [WebDisplayName("View Fields")]

    [WebDescription("The view fields")]

    [DefaultValue("")]

    public String ViewFields { get; set; }

 

    [Category("Query")]

    [Personalizable(PersonalizationScope.Shared)]

    [WebBrowsable(true)]

    [WebDisplayName("Query Throttle Mode")]

    [WebDescription("The query throttle mode")]

    [DefaultValue(SPQueryThrottleOption.Default)]

    public SPQueryThrottleOption QueryThrottleMode { get; set; }

 

    [Category("General")]

    [Personalizable(PersonalizationScope.Shared)]

    [WebBrowsable(true)]

    [WebDisplayName("Add Default Extensions")]

    [WebDescription("Adds the default extensions")]

    [DefaultValue(true)]

    public Boolean AddDefaultExtensions { get; set; }

 

    [PersistenceModeAttribute(PersistenceMode.InnerProperty)]

    public ParameterCollection Parameters { get; private set; }

 

    public event EventHandler<XsltExtensionEventArgs> XsltExtension;

 

    protected XsltProvider XsltProvider

    {

        get

        {

            return this.XsltVersion == XsltVersion.Xslt1 ? DefaultXsltProvider.Instance : SaxonXsltProvider.Instance;

        }

    }

    protected virtual void OnXsltExtension(XsltExtensionEventArgs e)

    {

        var handler = this.XsltExtension;

 

        if (handler != null)

        {

            handler(this, e);

        }

    }

 

    protected override void CreateChildControls()

    {

        var xml = this.GetXml();

        var html = this.Render(xml);

        var literal = new LiteralControl(html);

 

        this.Controls.Add(literal);

 

        base.CreateChildControls();

    }

 

    private String GetXslt()

    {

        var xslt = String.Empty;

 

        if (String.IsNullOrWhiteSpace(this.Xsl) == false)

        {

            xslt = this.Xsl;

        }

        else if (String.IsNullOrWhiteSpace(this.XslLink) == false)

        {

            var doc = new XmlDocument();

            doc.Load(this.XslLink);

 

            xslt = doc.InnerXml;

        }

 

        return xslt;

    }

 

    private DataTable GetTable()

    {

        if (this.table == null)

        {

            var query = new SPSiteDataQuery();

            query.Query = this.ApplyParameters(this.Query);

            query.QueryThrottleMode = this.QueryThrottleMode;

            query.RowLimit = this.RowLimit;

            query.Lists = this.Lists;

            query.Webs = this.Webs;

            query.ViewFields = this.ViewFields;

 

            this.table = SPContext.Current.Site.RootWeb.GetSiteData(query);

            this.table.TableName = "Row";

 

            foreach (var column in this.table.Columns.OfType<DataColumn>())

            {

                column.ColumnMapping = MappingType.Attribute;

            }

        }

 

        return this.table;

    }

 

    private String ApplyParameters(String value)

    {

        var parameters = this.GetParameters();

 

        value = parametersRegex.Replace(value, x => this.GetFormattedValue(parameters[x.Value.Substring(1)]));

 

        return value;

    }

 

    private String GetFormattedValue(Object value)

    {

        if (value == null)

        {

            return String.Empty;

        }

 

        if (value is Enum)

        {

            return ((Int32)value).ToString();

        }

 

        if (value is DateTime)

        {

            return SPUtility.CreateISO8601DateTimeFromSystemDateTime((DateTime)value);

        }

 

        if (value is IFormattable)

        {

            return (value as IFormattable).ToString(String.Empty, CultureInfo.InvariantCulture);

        }

 

        return value.ToString();

    }

 

    private IOrderedDictionary GetParameters()

    {

        if (this.parameters == null)

        {

            this.parameters = this.Parameters.GetValues(this.Context, this);

        }

 

        return this.parameters;

    }

 

    private String GetXml()

    {

        var sb = new StringBuilder();

        var table = this.GetTable();

 

        using (var writer = new StringWriter(sb))

        {

            table.WriteXml(writer);

        }

 

        sb

            .Replace("<DocumentElement>", "<dsQueryResponse RowLimit='" + this.RowLimit + "'><Rows>")

            .Replace("</DocumentElement>", "</Rows></dsQueryResponse>");

 

        return sb.ToString();

    }

 

    private String ApplyXslt(String xml, String xslt, XsltExtensionEventArgs args)

    {

        return this.XsltProvider.Transform(xml, xslt, args);

    }

 

    private String Render(String xml)

    {

        if (String.IsNullOrWhiteSpace(xml) == true)

        {

            return String.Empty;

        }

 

        var xslt = this.GetXslt();

 

        if (String.IsNullOrWhiteSpace(xslt) == true)

        {

            return String.Empty;

        }

 

        var extensions = new XsltExtensionEventArgs();

 

        this.OnXsltExtension(extensions);

 

        if (this.AddDefaultExtensions == true)

        {

            var defaultExtensions = Activator.CreateInstance(typeof(Microsoft.SharePoint.WebPartPages.DataFormWebPart).Assembly.GetType("Microsoft.SharePoint.WebPartPages.DataFormDdwRuntime"));

            extensions.AddExtension("http://schemas.microsoft.com/WebParts/v2/DataView/runtime", defaultExtensions);

        }

 

        foreach (var ext in extensions.Extensions)

        {

            extensions.AddExtension(ext.Key, ext.Value);

        }

 

        var parameters = this.GetParameters();

 

        foreach (var key in parameters.Keys.OfType<String>())

        {

            extensions.AddParameter(key, String.Empty, parameters[key].ToString());

        }

 

        foreach (var param in extensions.Parameters)

        {

            extensions.AddParameter(param.Name, param.NamespaceUri, param.Parameter.ToString());

        }

 

        return this.ApplyXslt(xml, xslt, extensions);

    }

 

    void IWebPartTable.GetTableData(TableCallback callback)

    {

        callback(this.GetTable().DefaultView);

    }

 

    PropertyDescriptorCollection IWebPartTable.Schema

    {

        get { return TypeDescriptor.GetProperties(this.GetTable().DefaultView); }

    }

}

This class extends the basic WebPart class and adds a couple of properties:

  • XsltVersion: the XSLT version to use, which will result in either my DefaultXsltProvider or the SaxonXsltProvider being used;
  • XslLink: the URL of a file containing XSLT;
  • Xsl: in case you prefer to have the XSLT inline;
  • Query: a CAML query;
  • Webs: the webs to query;
  • Lists: the lists to query;
  • ViewFields: the fields to return;
  • RowLimit: maximum number of rows to return;
  • QueryThrottleMode: the query throttle mode;
  • AddDefaultExtensions: whether to add the default extension functions and parameters;
  • Parameters: a standard collection of ASP.NET parameter controls.

The web part uses SPSiteDataQuery to execute a CAML query. Before the query is executed, any parameters it may have, in the form @ParameterName,  are replaced by actual values evaluated from the Parameters collection. This gives some flexibility to the queries, because, not only ASP.NET includes parameters for all the common sources, it’s very easy to add new ones. The web part knows how to format strings, enumerations, DateTime objects and in general any object implementing IFormattable; if you wish, you can extend it to support other types, but I don’t think it will be necessary.

An example usage:

<web:SPCustomXsltWebPart runat="server" XslLink="~/Style.xslt">

    <Query>

        <Where><Eq><FieldRef Name='Id'/><Value Type='Number'>@Id</Value></Eq></Where>

    </Query>

    <Parameters>

        <asp:QueryStringParameter Name="Id" QueryStringField="Id" Type="Int32" />

    </Parameters>

</web:SPCustomXsltWebPart>

Notice that one of the Xsl or the XslLink properties must be set, and the same goes for the Query.

Hope you find this useful, and let me know how it works!

     

    XSLT Processing in .NET

    God only knows why, but the .NET framework only includes support for XSLT 1.0. This makes it difficult, but not impossible, to use the more recent 2.0 version: a number of external libraries exist that can help us achieve that.

    I wanted to make this easier for us developers, namely:

    1. To have a common interface for abstracting typical functionality – transform some XML with some XSLT;
    2. Be able to inject parameters;
    3. Be able to inject custom extension functions.

    I first set out to define my base API:

    [Serializable]

    public abstract class XsltProvider

    {

        public abstract String Transform(String xml, String xslt, XsltExtensionEventArgs args);

        public abstract Single Version { get; }

    }

     

    [Serializable]

    public sealed class XsltExtensionEventArgs

    {

        public XsltExtensionEventArgs()

        {

            this.Extensions = new Dictionary<String, Object>();

            this.Parameters = new HashSet<XsltParameter>();

        }

     

        public IDictionary<String, Object> Extensions

        {

            get;

            private set;

        }

     

        public ISet<XsltParameter> Parameters

        {

            get;

            private set;

        }

     

        public XsltExtensionEventArgs AddExtension(String @namespace, Object extension)

        {

            this.Extensions[@namespace] = extension;

            return this;

        }

     

        public XsltExtensionEventArgs AddParameter(String name, String namespaceUri, String parameter)

        {

            this.Parameters.Add(new XsltParameter(name, namespaceUri, parameter));

            return this;

        }

    }

    The XsltProvider class only defines a version and a transformation method. This transformation method receives an XML and an XSLT parameters and also an optional collection of extension methods and parameters. There’s a singleton instance to make it easier to use, since this class really has no state.

    An implementation using .NET’s built-in classes, for XSLT 1.0, is trivial:

    [Serializable]

    public sealed class DefaultXsltProvider : XsltProvider

    {

        public static readonly XsltProvider Instance = new DefaultXsltProvider();

     

        public override Single Version

        {

            get { return 1F; }

        }

     

        public override String Transform(String xml, String xslt, XsltExtensionEventArgs args)

        {

            using (var stylesheet = new XmlTextReader(xslt, XmlNodeType.Document, null))

            {

                var arg = new XsltArgumentList();

     

                foreach (var key in args.Extensions.Keys)

                {

                    arg.AddExtensionObject(key, args.Extensions[key]);

                }

     

                foreach (var param in args.Parameters)

                {

                    arg.AddParam(param.Name, param.NamespaceUri, param.Parameter);

                }

     

                var doc = new XmlDocument();

                doc.LoadXml(xml);

     

                var transform = new XslCompiledTransform();

                transform.Load(stylesheet);

     

                var sb = new StringBuilder();

     

                using (var writer = new StringWriter(sb))

                {

                    var results = new XmlTextWriter(writer);

     

                    transform.Transform(doc, arg, results);

     

                    return sb.ToString();

                }

            }

        }

    }

    For XSLT 2.0, we have a number of options. I ended up using Saxon-HE, an open-source and very popular library for .NET and Java. I installed it through NuGet:

    image

    Here is a possible implementation on top of my base class:

    [Serializable]

    public sealed class SaxonXsltProvider : XsltProvider

    {

        public static readonly XsltProvider Instance = new SaxonXsltProvider();

     

        public override Single Version

        {

            get { return 2F; }

        }

     

        public override String Transform(String xml, String xslt, XsltExtensionEventArgs args)

        {

            var processor = new Processor();

     

            foreach (var key in args.Extensions.Keys)

            {

                foreach (var function in this.CreateExtensionFunctions(args.Extensions[key], key))

                {

                    processor.RegisterExtensionFunction(function);

                }

            }

     

            var document = new XmlDocument();

            document.LoadXml(xslt);

     

            var input = processor.NewDocumentBuilder().Build(document);

     

            var xsltCompiler = processor.NewXsltCompiler();

     

            var xsltExecutable = xsltCompiler.Compile(input);

     

            var xsltTransformer = xsltExecutable.Load();

     

            foreach (var parameter in args.Parameters)

            {

                xsltTransformer.SetParameter(new QName(String.Empty, parameter.NamespaceUri, parameter.Name), CustomExtensionFunctionDefinition.GetValue(parameter.Parameter));

            }

     

            using (var transformedXmlStream = new MemoryStream())

            {

                var dataSerializer = processor.NewSerializer(transformedXmlStream);

     

                xsltTransformer.InputXmlResolver = null;

                xsltTransformer.InitialContextNode = processor.NewDocumentBuilder().Build(input);

                xsltTransformer.Run(dataSerializer);

     

                var result = Encoding.Default.GetString(transformedXmlStream.ToArray());

     

                return result;

            }

        }

     

        private IEnumerable<ExtensionFunctionDefinition> CreateExtensionFunctions(Object extension, String namespaceURI)

        {

            foreach (var method in extension.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static).Where(x => x.IsAbstract == false))

            {

                yield return new CustomExtensionFunctionDefinition(method, namespaceURI, extension);

            }

        }

     

        class CustomExtensionFunctionDefinition : ExtensionFunctionDefinition

        {

            private readonly String namespaceURI;

            private readonly MethodInfo method;

            private readonly Object target;

     

            public CustomExtensionFunctionDefinition(MethodInfo method, String namespaceURI, Object target)

            {

                this.method = method;

                this.namespaceURI = namespaceURI;

                this.target = target;

            }

     

            public override XdmSequenceType[] ArgumentTypes

            {

                get

                {

                    return this.method.GetParameters().Select(x => this.GetArgumentType(x)).ToArray();

                }

            }

     

            private XdmSequenceType GetArgumentType(ParameterInfo parameter)

            {

                return new XdmSequenceType(GetValueType(parameter.ParameterType), XdmSequenceType.ONE);

            }

     

            internal static XdmAtomicValue GetValue(Object value)

            {

                if (value is String)

                {

                    return new XdmAtomicValue(value.ToString());

                }

     

                if ((value is Int32) || (value is Int32))

                {

                    return new XdmAtomicValue(Convert.ToInt64(value));

                }

     

                if (value is Boolean)

                {

                    return new XdmAtomicValue((Boolean)value);

                }

     

                if (value is Single)

                {

                    return new XdmAtomicValue((Single)value);

                }

     

                if (value is Double)

                {

                    return new XdmAtomicValue((Double)value);

                }

     

                if (value is Decimal)

                {

                    return new XdmAtomicValue((Decimal)value);

                }

     

                if (value is Uri)

                {

                    return new XdmAtomicValue((Uri)value);

                }

     

                throw new ArgumentException("Invalid value type.", "value");

            }

     

            internal static XdmAtomicType GetValueType(Type type)

            {

                if (type == typeof(Int32) || type == typeof(Int64) || type == typeof(Int16))

                {

                    return XdmAtomicType.BuiltInAtomicType(QName.XS_INTEGER);

                }

     

                if (type == typeof(Boolean))

                {

                    return XdmAtomicType.BuiltInAtomicType(QName.XS_BOOLEAN);

                }

     

                if (type == typeof(String))

                {

                    return XdmAtomicType.BuiltInAtomicType(QName.XS_STRING);

                }

     

                if (type == typeof(Single))

                {

                    return XdmAtomicType.BuiltInAtomicType(QName.XS_FLOAT);

                }

     

                if (type == typeof(Double))

                {

                    return XdmAtomicType.BuiltInAtomicType(QName.XS_DOUBLE);

                }

     

                if (type == typeof(Decimal))

                {

                    return XdmAtomicType.BuiltInAtomicType(QName.XS_DECIMAL);

                }

     

                if (type == typeof(Uri))

                {

                    return XdmAtomicType.BuiltInAtomicType(QName.XS_ANYURI);

                }

     

                throw new ArgumentException("Invalid value type.", "value");

            }

     

            public override QName FunctionName

            {

                get { return new QName(String.Empty, this.namespaceURI, this.method.Name); }

            }

     

            public override ExtensionFunctionCall MakeFunctionCall()

            {

                return new CustomExtensionFunctionCall(this.method, this.target);

            }

     

            public override Int32 MaximumNumberOfArguments

            {

                get { return this.method.GetParameters().Length; }

            }

     

            public override Int32 MinimumNumberOfArguments

            {

                get { return this.method.GetParameters().Count(p => p.HasDefaultValue == false); }

            }

     

            public override XdmSequenceType ResultType(XdmSequenceType[] argumentTypes)

            {

                return new XdmSequenceType(GetValueType(this.method.ReturnType), XdmSequenceType.ONE);

            }

        }

     

        class CustomExtensionFunctionCall : ExtensionFunctionCall

        {

            private readonly MethodInfo method;

            private readonly Object target;

     

            public CustomExtensionFunctionCall(MethodInfo method, Object target)

            {

                this.method = method;

                this.target = target;

            }

     

            public override IXdmEnumerator Call(IXdmEnumerator[] arguments, DynamicContext context)

            {

                var args = new List<Object>();

     

                foreach (var arg in arguments)

                {

                    var next = arg.MoveNext();

                    var current = arg.Current as XdmAtomicValue;

                    args.Add(current.Value);

                }

     

                var result = this.method.Invoke(this.target, args.ToArray());

     

                var value = CustomExtensionFunctionDefinition.GetValue(result);

                return value.GetEnumerator() as IXdmEnumerator;

            }

        }

    }

    You can see that this required considerable more effort. I use reflection to find all arguments and the return type of the passed extension objects and I convert them to the proper types that Saxon expects.

    A simple usage might be:

    //sample class containing utility functions

    class Utils

    {

        public int Length(string s)

        {

            //just a basic example

            return s.Length;

        }

    }

     

    var provider = SaxonXsltProvider.Instance;

     

    var arg = new XsltExtensionEventArgs()

        .AddExtension("urn:utils", new Utils())

        .AddParameter("MyParam", String.Empty, "My Value");

     

    var xslt = "<?xml version='1.0'?><xsl:transform version='2.0' xmlns:utils='urn:utils' xmlns:xsl='http://www.w3.org/1999/XSL/Transform'><xsl:template match='/'>Length: <xsl:value-of select='utils:Length(\"Some Text\")'/> My Parameter: <xsl:value-of select='@MyParam'/></xsl:template></xsl:transform>";

    var xml = "<?xml version='1.0'?><My><SampleContents/></My>";

     

    var result = provider.Transform(xml, xslt, arg);

    Here I register an extension object with a public function taking one parameter and also a global parameter. In the XSLT I output this parameter and also the results of the invocation of a method in the extension object. The namespace of the custom function (Length) must match the one registered in the XsltExtensionEventArgs instance.

    As always, hope you find this useful! Winking smile