Centralize Master page and Alternate CSS settings

Goals:

  • Control all Master pages, Application Master pages and the Alternate CSS url's from code and being able to change these settings at run-time
  • Have only a single copy of the Master page(s) and the alternate stylesheet(s) in your whole farm (ideally on the file system in the _/layouts folder)
  • Apply a custom Master page and stylesheet to Non-Publishing teamsites
  • Apply a different Master page and stylesheet to the application pages (_/layouts pages)

To achieve this I want to use a HttpModule to set the Masterpage and alternate CSS settings at run time. This is probably the only way to change the application.master (do some Googling on it if you don't believe me).

This HttpModule should determine for every request what combination of stylesheet and master should be used.

To update the Masterpage you can easily tell the current page to use another master at runtime:

Page.MasterPageFile = "~/layouts/custom.master";

This works for both regular pages and application pages, although you probably want to use different master pages for the two.

Updating the Alternate CSS is quite a bit harder. This is because the CSS link is rendered by a separate control inside the masterpage:

<SharePoint:CssLink runat="Server"/>

If you dive into the source of this control by using Reflector you'll find a Render() method in which the actual CSS links are written to the screen.

This Render method will write several CSS links from several sources:

1st: It will ask the current SPContext for its CssReferences. This is an internal collection; I didn't find a way to modify this collection

2nd: It writes a link from its own private string m_primaryCssUrl. This is probably the link to core.css. Because it is private you cannot and you do not want to change it. I believe this string is assigned in a obfuscated method called SetDefaults() because it is not assigned anywhere else.

3th:  It writes a link from its own pubic property DefaultURL, finally a public property we can set! Setting this property can look like:

<SharePoint:CssLink runat="Server" DefaultUrl="~/layouts/custom.master"/>

You'll hard-code this property in the Masterpage, meaning that every Masterpage is connected to a specific Stylesheet. If you want to be able to dynamically attach different stylesheets from code (for example based on the user or based on the time of the day, the weather, etc), this is not the way for you.

So you really want to control the Stylesheet on run-time? Fine, but I'll warn you – it's not going to be nice.

First thing, you're not able to access the SharePoint:CssLink  control programmatically to set the DefaultUrl. This is because it has no Id and is not registered in the code-behind. Looping to all controls does not help as well; I can't get a hook to the control…

Luckily the Render() method of the CssLink control also has a 4th source from which it retrieves CSS links;

4th: It writes a link which is defined in one of the current SPWeb properties: m_alternateCssUrl. This property is also accessible from the public property SPWeb.AlternateCssUrl.

So if we set this property it will automatically be rendered by the SharePoint:CssLink control :-)

The downsize to this is that you are actually changing SPWeb properties. If you don't save the SPWeb these changes are discarded after the request, but if the SPWeb is saved somewhere along the page execution path, this setting will be saved as well. Not very nice.  

Custom CssLink Control?

So this SharePoint:CssLink control is not very easy to manipulate at run-time. For the moment I am fine setting the DefaultUrl property in the control itself. This leaves me with a bit less flexibility, but it works and it is actually default functionality. Due to the limited time I have available it's a nice trade-off.  

If you are ready to take more serious matters, there are 2 options I want to consider:

  • 1. Overriding the SharePoint:CssLink control and add our own logic on top of it
  • 2. Creating our own SharePoint:CssLink control
  • 3. Adding the CSS Link directly from the HttpModule.

The code if have so far look like (simplified):

public class MasterPageModule : IHttpModule
{
    public void Init(HttpApplication context)
    {
        context.PreRequestHandlerExecute += context_PreRequestHandlerExecute;
    }

    static void context_PreRequestHandlerExecute(object sender, EventArgs e)
    {
        Page page = HttpContext.Current.CurrentHandler as Page;
        if (page != null)
        {
            page.PreInit += page_PreInit;
        }
    }

    static void page_PreInit(object sender, EventArgs e)
    {
        Page page = sender as Page;

        if (page.MasterPageFile.ToLower().Contains("application.master"))
        {
            page.MasterPageFile = "~/_layouts/customApplication.master";
        }
        else if (page.MasterPageFile.ToLower().Contains("default.master"))
        {
            page.MasterPageFile = "~/_layouts/custom.master";
        }

    }

    public void Dispose()
    {
    }
}
 
[update] The solution to set the masterpage as shown above does not work for publishing pages. I'm still investigating this issue.

Leave a Reply