In a previous article I suggested that using webparts to produce accessible or highly styled websites was very difficult and that using publishing controls or standard ASP.Net controls was the way to go. In this article I plan to show how to create a control which solves a very common requirement...listing pages in the site.
To develop your accessible SharePoint publishing site visit SPWorks to download ARF, a free development framework.
Previously I have given some examples of the SPSiteDataQuery and showed how to get the results from the query in XML. We will use these to build a control which allows you to specify the CAML for the ViewFields, Webs, Query and Lists properties of the SPSiteDataQuery class.
Create the website
For the purpose of this article will I assume you have created a standard publishing site and still have the standard layouts, sites and pages. However, the control which gets created will work with any site (WSS or MOSS).
Create the VS project
In VS 2005 create a standard C# class library and call it PublishingSamples. Once this has been created you need to setup the project to allow access the SharePoint API and to make development easier.
First open the project properties and go to the build events. Here we will add the following command to the post-build events.
copy $(TargetPath) "C:\Inetpub\wwwroot\wss\VirtualDirectories\website80"
Where 'website80' is the virtual directory of your SharePoint publishing site. This will automatically copy the DLL to the bin of the SharePoint site everytime you build the project.
Next you will need to enable the assembly signing, by clicking on the 'signing' section of the properties page and selecting new. Enter 'key' as the name of your new key.
Lastly you will need to add a reference to both Microsoft.SharePoint.dll and System.Web.dll.
Once the settings have been changed rename the Class1.cs file to SiteQuery.cs (this should automatically rename the class for you) and then ensure it inherits from System.Web.UI.Control.
public class SiteQuery : System.Web.UI.Control
Add the code to the class
Firstly we will add the properties required for the SPSiteDataQuery class...
private string _lists = "<Lists ServerTemplate='850'/>";
private string _query = "";
private string _webs = "<Webs Scope='Recursive' />";
private string _viewFields = "<FieldRef Name='Title' /><FieldRef Name='ID' /><FieldRef Name='FileRef' /><FieldRef Name='ContentType' />";
private uint _rowLimit = 10;
public uint RowLimit
{ get { return _rowLimit; } set { _rowLimit = value; } }
public string Lists
{ get { return _lists; } set { _lists = value; } }
public string Query
{ get { return _query; } set { _query = value; } }
public string Webs
{ get { return _webs; } set { _webs = value; } }
public string ViewFields
{ get { return _viewFields; } set { _viewFields = value; } }
These will allow us to set the parameters of the control in the page layout and so configure the control to list the content we want. Here I have given the some the properties some default values which will list all pages in the current site and all sub sites.
Next we need to add the code to generate the XmlDocument we will use to create the HTML. Firstly add the following using statements to the top of the class file...
using Microsoft.SharePoint;
using System.Data;
using System.Xml;
This will give us access to the classes we need. Next add a new property to the class called Document which returns an XmlDocument. This property will execute the query to return the content defined by our SPSiteDataQuery parameters.
public virtual XmlDocument Document
{
get
{
SPSiteDataQuery q = new SPSiteDataQuery();
q.Lists = Lists;
q.Query = Query;
q.Webs = Webs;
q.ViewFields = ViewFields;
q.RowLimit = RowLimit;
DataTable t = SPContext.Current.Web.GetSiteData(q);
t.TableName = "row";
DataSet ds = new DataSet("rows");
ds.Tables.Add(t);
XmlDocument oResults = new XmlDocument();
oResults.LoadXml(ds.GetXml());
return oResults;
}
}
Here we use the properties of our new class to initialize the SPSiteDataQuery class and perform our query. We use the DataSet GetXml() method (which I mentioned in a previous article) to turn our results into an XML document...this has the benefit of ensuring consistency in your XML and will make re-using XSLT much easier.
Next we need to add the properties that allow the XSL to be defined in the page layout. Add the following properties to the class...
private string _xslLocation = "/Style Library/XSL Style Sheets/";
private string _xslName = null;
public string XslLocation
{get { return _xslLocation; } set { _xslLocation = value; }}
public string XslName
{ get { return _xslName; } set { _xslName = value; }}
Here we are allowing the location of the XSLs and the name of the XSL file to be specified. The default location for the XSL is in the root of the site collection in the 'XSL Style Sheets' folder. This is where the XSL for the ContentByQuery WebPart is stored. In order to use the XSL to transform the document we need to override the Render method of the control.
First add the following using statement...
Next override the Render method...
protected override void Render(HtmlTextWriter writer)
{
XmlDocument oXsl = new XmlDocument();
oXsl.LoadXml(SPContext.Current.Site.RootWeb.GetFileAsString(XslLocation + XslName));
XslCompiledTransform oTransform = new XslCompiledTransform();
oTransform.Load(oXsl);
XsltArgumentList args = new XsltArgumentList();
foreach (string sKey in Page.Request.QueryString.AllKeys)
args.AddParam("QS-" + sKey, "", Page.Request[sKey]);
oTransform.Transform(Document, args, writer);
}
Here we retrieve the XSL document from the XSL document library, load it into a transform class and call Transform passing the Document property we created previously. The Transform method uses the HtmlTextWriter to write out the generated HTML.
Build your project and ensure it compiles.
And that, as they say, is that. This class will perform the query to find the content you require, turn that into an XML document and use XSL to tranform that into HTML. The only HTML produced will be from your XSL...SharePoint will not add anything extra.
The completed class file can be downloaded from here.
Modify web.config
Building the control will automatically copy the DLL produced to our site's bin folder. We now need to modify the web.config of the site to mark the control as safe and to enable it to run. We will also make some changes to make debugging SharePoint easier.
Before we can make the changes we need to find out the assembly's FQN (Fully Qualified Name). Microsoft explain how to do this, but I prefer to use Reflector, as I normally have it hand.
Firstly add the assembly to the SafeControls section of web.config...
<SafeControl Assembly="PublishingSamples, Version=1.0.0.0, Culture=neutral, PublicKeyToken=15a83b848317a32e" Namespace="PublishingSamples" TypeName="*" Safe="True" />
Next turn custom errors off by modifying the following line...
<customErrors mode="Off" />
Next change the SafeMode as described in a previous article...
<SafeMode MaxControls="200" CallStack="true"...
Then change the trust to Full...
<trust level="Full" originUrl="" />
Changing the trust to full will permit SharePoint to use our DLL even though it is only in the bin folder. Our DLL will run with full trust, but so will all other assemblies. This is fine for developing...it allows us to build the DLL & copy directly to the bin folder so that SharePoint will use it immediately without an IISRESET. However, for a production site the DLL should either be in the GAC or preferably configured with the correct code access security (CAS).
Save web.config and we are ready to modify the layout in SharePoint Designer and add our new control to it.
Add the control to your site
Firstly you need to register your assembly and the namespace. Add the following line to the top of your layout (or master page)...
<%@ Register Tagprefix="SQ" Namespace="PublishingSamples" Assembly="PublishingSamples, Version=1.0.0.0, Culture=neutral, PublicKeyToken=15a83b848317a32e" %>
Remember, the assembly FQN will be different for your assembly and so this should be changed to the FQN of your assembly.
Next add the control where you would like the output to appear...
<SQ:SiteQuery runat="server" XslName="List.xsl"/>
Here we have specified 'List.xsl' as the XSL to use for the transformation. This should be located in the "/Style Library/XSL Style Sheets/" folder of your site (the default specified in the class). To create this file use SharePoint designer...create the List.xsl and copy the following XSLT into it.
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<xsl:apply-templates select="rows/row"/>
</xsl:template>
<xsl:template match="row">
<h2>
<xsl:value-of select="Title"/>
</h2>
</xsl:template>
</xsl:stylesheet>
This is a fairly simple transformation which outputs the titles of the content which matched the query. This XSL will be applied to the XML which will have a format similar to below...
<rows>
<row>
<ListId>0F4344D9-37EE-4732-9B13-B6F3BAF38F91</ListId>
<WebId>617FE4B1-A040-4BE5-9B55-70B34B8A8AC4</WebId>
<ID>1</ID>
<Title>Web Page 1</Title>
<ContentType>Page</ContentType>
</row>
<row>
<ListId>0F4344D9-37EE-4732-9B13-B6F3BAF38F91</ListId>
<WebId>617FE4B1-A040-4BE5-9B55-70B34B8A8AC4</WebId>
<ID>2</ID>
<Title>Web Page 2</Title>
<ContentType>Page</ContentType>
</row>
</rows>
Save the layout (or master page) and navigate to the page...if everything has gone correctly you should see a list of all the pages in the current site.
You can then try modifying the control to display different content. The example below shows how to list news articles from the entire site collection.
<SQ:SiteQuery runat="server" XslName="List.xsl"
Query="<Where><Eq><FieldRef Name='ContentType'/><Value Type='Text'>News</Value></Eq></Where>"
Webs="<Webs Scope='SiteCollection' />" />
Conclusion
This is a fairly basic example and, in its current state, is definitely not useable on a production site. Hopefully it will give you a good start in creating your own controls which use XML and XSLT to produce HTML. In order to extend this control for production you would (at the very least) need to add error handling and caching. Caching is most important as this control could seriously affect the performance of your site without it.
Possible extensions to this class could include...
- Add a URL property to a specify site or list on which to perform the query.
- Turn it into an XmlDocument data source so that other controls on the page can consume the XmlDocument.
- Add a content type property so that only pages of a particular content type are returned. This would negate the need to specify the SPSiteDataQuery parameters on the control.
- Change it to inherit from BaseFieldControl and store the query in a column of the page...allowing the Author to define the content displayed.
- Inherit from RichLinkField and instead of using the SPSiteDataQuery to produce the XML, use the link to get the web page and produce your own XML from the columns. This way you can create your own 'Featured Article' control which gives the page Author the ability to select the article.
Essentially this class could provide a useful base on which you can build other controls...I will extend this class with some of these concepts when I get time.
If you would like to use the same concept to develop your accessible SharePoint publishing site visit SPWorks to download ARF, a free development framework.