Monday, February 13, 2006

Extreme SharePoint Design: Dynamic Style Sheets

Both WSS and SPS provide mechanisms for altering the presentation of pages using custom style sheets. While WSS provides a relatively simple template structure for improving the stock look and feel on a site-by-site basis, SPS customizations must be applied on a portal-wide basis (using the custom style sheet field in Site Settings) or for each individual site definition. If you employ user controls in your site definitions to create custom headers, navigation elements, etc. this means that each site definition must reference it's own separate control. This makes code maintenance difficult and time consuming.

But perhaps the most glaring deficiency in employing custom styles is the lack of style continuity in administration pages. No matter how eye-catching your design is, as soon as the user attempts to manage portal content, change settings, create a list, or do one of a hundred other administrative tasks, the styles revert back to the boring out-of-the-box template (this holds true for both WSS sites and SPS portal areas).

In order to overcome this limitation, a method of dynamically switching styles for sites, areas, and administration pages is needed Fortunately, the SharePoint object model provides several functions that can be leveraged to create a style switcher based on site definition, site template, user id, group, or any number of other parameters depending upon your requirements.

To begin with, if you are not already employing user controls in your custom site definitions, you should consider doing so as soon as possible. They are flexible, reusable, and easy to maintain. Perhaps most importantly, they also provide access to server-side scripting which is disabled on standard ASPX pages within SharePoint. Creating a user control is simple - modify a base portal page (such as the default page for a cloned site definition) until you have the desired look and feel, then extract that code into a blank HTML page with all <HTML>, <HEAD> and <BODY> tags removed. Add the proper registrations to the top of the page (see example below) and save it into a centrally-accessible directory (such as /bin or /_layouts/1033) with an .ASCX extension. In the default page, add a page registration for the file and a code reference in the body of the page and you're done. You can now reference the user control from any portal page - changes made within the control will be viewable on all pages which reference it. Now you only one page to maintain instead of dozens.

Here's an example of a user control (header.ascx) that creates a custom portal header:


<%@ Control Language="c#" AutoEventWireup="false" TargetSchema="http://schemas.microsoft.com/intellisense/ie5" %>
<%@ Register Tagprefix="WebPartPages" Namespace="Microsoft.SharePoint.WebPartPages" Assembly="Microsoft.SharePoint, Version=11.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register Tagprefix="SPSWC" Namespace="Microsoft.SharePoint.Portal.WebControls" Assembly="Microsoft.SharePoint.Portal, Version=11.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register Tagprefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=11.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<table width="100%" border="0" cellpadding="0" cellspacing="0" ID="Table2">
<tr>
<td width="100%" coldiv="4" height="14" class="spsHeader_Banner"></td>
</tr>
<tr>
<td width="40" height="80" align="center" valign="middle" class="spsHeader_Logo"><img src="/_layouts/images/Header_Left.gif"></td>
<td width="100" valign="middle" class="spsHeader_Name"><SPSWC:WebProperty Property="SiteTitle" runat="server" /></td>
<td width="99%" valign="bottom" class="spsHeader_Title">Home</td>
<td width="350" valign="bottom" class="spsHeader_Actions">
<table height="100%" width="100%" border="0" cellpadding="0" cellspacing="0" ID="Table3">
<tr>
<td coldiv="2" height="99%" valign="top" align="right" class="spsHeader_MySite" nowrap ><SPSWC:PageHeader id="PageHeaderID" runat="server" PageContext="SitePage" ShowTitle="false" Mode="LinksOnly" /></td>
</tr>
<tr>
<td width="75" valign="middle" align="right" class="spsHeader_Search">Search</td>
<td valign="middle" width="99%" align="right" class="spsHeader_SearchBox"><SPSWC:RightBodySectionSearchBox id="RightBodySectionSearchBoxID" runat="server" SearchResultPageURL="SEARCH_HOME" FrameType="None" /></td>
</tr>
</table>
</td>
</tr>
<tr>
<td coldiv="4" width="100%">
<div id="spsHeader_NavMenu">
<SPSWC:CategoryNavigationWebPart runat="server" id="HorizontalNavBar" />
</div>
</td>
</tr>
<tr>
<td height="1" coldiv="4" width="100%" class="spsHeader_Separator"></td>
</tr>
</table>

Note the page registrations at the top of the file - these provide access to SharePoint's built-in functions and the object model. On the default page, add a reference to the control like this:



<%@ Register Tagprefix="CUSTOMUC" Tagname="Header" src="/bin/Header.ascx" %>

Then insert the control reference in the body of the page, like so:


<CUSTOMUC:Header id="Header" runat="server"></CUSTOMUC:Header>

Once you have the control in place, you can add code to dynamically change style sheets. For example, let's assume that you have a style sheet for each standard portal definition - custom_sps.css, custom_msite.css, custom_topic.css, custom_news.css, and so on. In order for the header to have a different look for each area, the style sheet reference in the file must change based on which site definition the page being viewed is derived from. To make this happen, add the following code to the top of the control, just after the page registrations:



<%
if (SPControl.GetContextWeb(Context).WebTemplate == "SPS")
{
Response.Write("<link rel='stylesheet' type='text/css' href='/_layouts/" +
System.Threading.Thread.CurrentThread.CurrentUICulture.LCID + "/styles/custom_SPS.css'>");
}
else if (SPControl.GetContextWeb(Context).WebTemplate == "SPSMSITE")
{
Response.Write("<link rel='stylesheet' type='text/css' href='/_layouts/" +
System.Threading.Thread.CurrentThread.CurrentUICulture.LCID + "/styles/custom_msite.css'>");
}
else if (SPControl.GetContextWeb(Context).WebTemplate == "SPSTOPIC")
{
Response.Write("<link rel='stylesheet' type='text/css' href='/_layouts/" +
System.Threading.Thread.CurrentThread.CurrentUICulture.LCID + "/styles/custom_spstopic.css'>");
}
else if (SPControl.GetContextWeb(Context).WebTemplate == "SPSNEWS")
{
Response.Write("<link rel='stylesheet' type='text/css' href='/_layouts/" +
System.Threading.Thread.CurrentThread.CurrentUICulture.LCID + "/styles/custom_news.css'>");
}
else
{
Response.Write("");
}
%>

To add additional site definitions, insert an extra "else if" statement for each definition name (WSS sites use the STS definition). The key to this script is the value of GetContextweb(Context).WebTemplate method. This method returns the site definition name for the page being viewed. For individual WSS site collections, you can also use GetContextWeb(Context).Site.RootWeb method. What makes this method even more useful is that the site definition/root web value persists on administration pages (see Extreme SharePoint Design: Customizing SharePoint Administration Pages for detailed instructions on how to employ user controls on administration pages).

Pages which call the new header will inherit the base styles, the custom styles applied at the portal level, and the definition-specific file referenced in the dynamic style sheet script. Due to the cascading principle of CSS, the last-referenced file takes precedence, insuring that the proper style is displayed. By utlizing dynamic style sheet switching you will create a more consistent user experience and reduce the amount of maintenance required for custom site definitions.