SharePoint XSLT Web Part
After my previous post on XSLT processing, what else could follow? Of course, an XSLT web part for SharePoint!
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!