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!

     

    ASP.NET Web Forms Prompt Validator

    For those still using Web Forms and Microsoft’s validation framework, like yours truly – and I know you’re out there! -, it is very easy to implement custom validation by leveraging the CustomValidator control. It allows us to specify both a client-side validation JavaScript function and a server-side validation event handler.

    In the past, I had to ask for confirmation before a form was actually submitted; the native way to ask for confirmation is through the browser’s confirm function, which basically displays a user-supplied message and two buttons, OK and Cancel. I wrapped it in a custom reusable validation control, which I am providing here:

       1: [DefaultProperty("PromptMessage")]

       2: public sealed class PromptValidator : CustomValidator

       3: {

       4:     [DefaultValue("")]

       5:     public String PromptMessage { get; set; }

       6:  

       7:     protected override void OnPreRender(EventArgs e)

       8:     {

       9:         var message = String.Concat("\"", this.PromptMessage, "\"");

      10:  

      11:         if ((this.PromptMessage.Contains("{0}") == true) && (this.ControlToValidate != String.Empty))

      12:         {

      13:             message = String.Concat("String.format(\"", this.PromptMessage, "\", args.Value)");

      14:         }

      15:  

      16:         this.ClientValidationFunction = String.Concat("new Function('sender', 'args', 'args.IsValid = confirm(", message, ")')");

      17:         this.EnableClientScript = true;

      18:  

      19:         base.OnPreRender(e);

      20:     }

      21: }

    A sample usage without any target control might be:

       1: <web:PromptValidator runat="server" PromptMessage="Do you want to submit your data?" ErrorMessage="!"/>

    And if you want to specifically validate a control’s value:

       1: <web:PromptValidator runat="server" PromptMessage="Do you want to accept {0}?" ErrorMessage="!" ControlToValidate="text" ValidateEmptyText="true"/>

    When submitting your form, you will get a confirmation prompt similar to this (Chrome):

    image

    Generating GDI+ Images for the Web

    .NET’s Graphics Device Interface (GDI+) is Microsoft’s .NET wrapper around the native Win32 graphics API. It is used in Windows desktop applications to generate and manipulate images and graphical contexts, like those of Windows controls. It works through a set of operations like DrawString, DrawRectangle, etc, exposed by a Graphics instance, representing a graphical context and it is well known by advanced component developers. Alas, it is rarely used in web applications, because these mainly consist of HTML, but it is possible to use them. Let’s see how.

    Let’s start by implementing a custom server-side control inheriting from Image:

       1: public class ServerImage: Image

       2: {

       3:     private System.Drawing.Image image;

       4:

       5:     public ServerImage()

       6:     {

       7:         this.ImageFormat = ImageFormat.Png;

       8:         this.CompositingQuality = CompositingQuality.HighQuality;

       9:         this.InterpolationMode = InterpolationMode.HighQualityBicubic;

      10:         this.Quality = 100L;

      11:         this.SmoothingMode = SmoothingMode.HighQuality;

      12:     }

      13:

      14:     public Graphics Graphics { get; private set; }

      15:

      16:     [DefaultValue(typeof(ImageFormat), "Png")]

      17:     public ImageFormat ImageFormat { get; set; }

      18:

      19:     [DefaultValue(100L)]

      20:     public Int64 Quality { get; set; }

      21:

      22:     [DefaultValue(CompositingQuality.HighQuality)]

      23:     public CompositingQuality CompositingQuality { get; set; }

      24:

      25:     [DefaultValue(InterpolationMode.HighQualityBicubic)]

      26:     public InterpolationMode InterpolationMode { get; set; }

      27:

      28:     [DefaultValue(SmoothingMode.HighQuality)]

      29:     public SmoothingMode SmoothingMode { get; set; }

      30:

      31:     protected override void OnInit(EventArgs e)

      32:     {

      33:         if ((this.Width == Unit.Empty) || (this.Height == Unit.Empty) || (this.Width.Value == 0) || (this.Height.Value == 0))

      34:         {

      35:             throw (new InvalidOperationException("Width or height are invalid."));

      36:         }

      37:

      38:         this.image = new Bitmap((Int32)this.Width.Value, (Int32)this.Height.Value);

      39:         this.Graphics = System.Drawing.Graphics.FromImage(this.image);

      40:         this.Graphics.CompositingQuality = this.CompositingQuality;

      41:         this.Graphics.InterpolationMode = this.InterpolationMode;

      42:         this.Graphics.SmoothingMode = this.SmoothingMode;

      43:

      44:         base.OnInit(e);

      45:     }

      46:

      47:     protected override void Render(HtmlTextWriter writer)

      48:     {

      49:         var builder = new StringBuilder();

      50:

      51:         using (var stream = new MemoryStream())

      52:         {

      53:             var codec = ImageCodecInfo.GetImageEncoders().Single(x => x.FormatID == this.ImageFormat.Guid);

      54:

      55:             var parameters = new EncoderParameters(1);

      56:             parameters.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, this.Quality);

      57:

      58:             this.image.Save(stream, codec, parameters);

      59:

      60:             builder.AppendFormat("data:image/{0};base64,{1}", this.ImageFormat.ToString().ToLower(), Convert.ToBase64String(stream.ToArray()));

      61:         }

      62:

      63:         this.ImageUrl = builder.ToString();

      64:

      65:         base.Render(writer);

      66:     }

      67:

      68:     public override void Dispose()

      69:     {

      70:         this.Graphics.Dispose();

      71:         this.Graphics = null;

      72:

      73:         this.image.Dispose();

      74:         this.image = null;

      75:

      76:         base.Dispose();

      77:     }

      78: }

    Basically, this control discards the ImageUrl property and replaces it with a Data URI value generated from a stored context. You need to define the image’s Width and Height and you can also optionally specify other settings such as the image’s quality percentage (Quality), compositing quality (CompositingQuality), interpolation (InterpolationMode) and smoothing modes (SmootingMode). These settings can be used to improve the outputted image quality.

    Finally, you use it like this. First, declare a ServerImage control on your page:

       1: <web:ServerImage runat="server" ID="image" Width="200px" Height="100px"/>

    And then draw on its Context like you would in a Windows application:

       1: protected override void OnLoad(EventArgs e)

       2: {

       3:     this.image.Graphics.DrawString("Hello, World!", new Font("Verdana", 20, FontStyle.Regular, GraphicsUnit.Pixel), new SolidBrush(Color.Blue), 0, 0);

       4:

       5:     base.OnLoad(e);

       6: }

    Basically, this control discards the ImageUrl property and replaces it with a Data URI value generated from a stored context. You need to define the image’s Width and Height and you can also optionally specify other settings such as the image’s quality percentage (Quality), compositing quality (CompositingQuality), interpolation (InterpolationMode) and smoothing modes (SmootingMode). These settings can be used to improve the outputted image quality.

    Finally, you use it like this. First, declare a ServerImage control on your page:

       1: <web:ServerImage runat="server" ID="image" Width="200px" Height="100px"/>

    And then draw on its Context like you would in a Windows application:

       1: protected override void OnLoad(EventArgs e)

       2: {

       3:     this.image.Graphics.DrawString("Hello, World!", new Font("Verdana", 20, FontStyle.Regular, GraphicsUnit.Pixel), new SolidBrush(Color.Blue), 0, 0);

       4:

       5:     base.OnLoad(e);

       6: }

    The result is this IMG tag with a Data URI content, that you can save or copy to the clipboard:

    image

    Pretty sleek, don’t you think? Winking smile