Wednesday, August 24, 2011

Using a custom master page for the 'Access Web Database' Templates in SharePoint 2010

I'm working on a project where our goal isn't to deliver a solution of SharePoint-based sites, pages, and components, but rather to deliver a generic branding solution that can be applied to any of the OOTB SharePoint templates.  To that end, I need to make sure our custom branding (master page + CSS + JS) applies to all the sites. Most sites weren't too big of a problem, we set MasterUrl and CustomMasterUrl to our master page (via a feature) and we were in business. A couple (the search centers) required a CustomMasterUrl that was based on minimal.master (instead of v4.master), but things were reasonably smooth.

That is, until we got to the 'Access Web Database' templates.

The first thing I saw when I created one is that it was completely ignoring our branding - nothing was applying. "Ok", I thought, "this must just be like the search templates - they set minimal.master as their custom master, I'll just need to add code to set my minimal.master as the CustomMasterUrl and I'll be done." No joy.  I confirmed my master pages were already being applied (using PowerShell - the settings page was blocked), but it still wasn’t working.

Add-PSSnapin Microsoft.SharePoint.Powershell;
$w = get-spweb http://test.local/sites/test1/AWD 
$w | format-list *masterurl*
MasterUrl : /sites/branding-STS-0-dynamic/_catalogs/masterpage/MyDefaultMaster.master
CustomMasterUrl : /sites/branding-STS-0-dynamic/_catalogs/masterpage/MyDefaultMaster.master

Hmm… well, why is my master page being ignored? Maybe there's something weird with the MasterPageFile reference in the page.  I guess SharePoint Designer does have a use after all - let's fire it up and see what the page tag looks like.

clip_image001

Ok, that didn’t work.  Maybe I forgot to enable SharePoint Designer for this web app. A quick check of the web application configuration in central administration confirms that it’s enabled, and I can hit it's parent site.  Apparently the template is so ashamed of what it did to SharePoint it feels it has to hide it’s shame from me. No matter, PowerShell to the rescue again:

Add-PSSnapin Microsoft.SharePoint.Powershell;
$w = get-spweb http://test.local/sites/test1/AWD
$f = $w.GetFile("default.aspx")
$data = $f.OpenBinary()
[System.io.file]::Writeallbytes("awd-default.aspx")

Now, let’s open up awd-default.aspx and see what this template is trying to hide, shall we?

<%@ Page language="C#" 
MasterPageFile="_catalogs/masterpage/minimal.master"

That would be our problem.  A hard-coded reference to a master page in embedded in the page.  At this point, I see a few options to fix this:

  1. Find the file on the file system (14\TEMPLATE\SiteTemplates\AccSrv\default.aspx) and edit it to reference a master page I can control.
  2. Add a HttpModule (in the style of the 2007-era application.master override) to dynamically change the master page to one I control at runtime
  3. Customize the default.aspx to reference a master page I can control.

Well, #1 is out due to the non-supportability of filesystem changes.  Option 2 is out due to my client not wanting to install a HttpModule, so option #3 it is.   I already have code that sets the CustomMasterUrl to my own version of minimal.master, so I just need to modify default.aspx to reference “~masterurl/custom.master” and I’ll be all set.

/// <summary>

/// The Access Web Database templates (ie. ACCSRV#*) have a hard-coded reference to minimal.master.

/// Let's change that to ~masterurl/custom.master so we can configure the master page.

/// </summary>

/// <param name="web">The <see cref="SPWeb"/> to fix up</param>

private static void FixBrandingForWebDatabases(SPWeb web)

{

    var file = web.GetFile("default.aspx");

    var data = file.OpenBinary();

    var strData = ByteArrayToString(data);

    var re = new Regex(@"MasterPageFile=['""][^'""]*['""]");

    var newStrData = re.Replace(strData, "MasterPageFile=\"~masterurl/custom.master\"");

    if (newStrData != strData)

    {

        data = Encoding.UTF8.GetBytes(newStrData);

        if (file.CheckOutType == SPFile.SPCheckOutType.None)

        {

            file.CheckOut();

        }

        file.SaveBinary(data);

        file.CheckIn("changed master page");

    }

}



/// <summary>

/// Converts the passed byte array to a string, using <see cref="StreamReader"/>'s automatic encoding logic.

/// </summary>

private static string ByteArrayToString(byte[] data)

{

    using (var ms = new MemoryStream(data))

    {

        using (var sr = new StreamReader(ms))

        {

            return sr.ReadToEnd();

        }

    }

}

Now, I just have to call this for sites created from these templates (I called it right after I set the master page):

ID Name Title
2764 ACCSRV#0 Access Services Site
2764 ACCSRV#1 Assets Web Database
2764 ACCSRV#3 Charitable Contributions Web Database
2764 ACCSRV#4 Contacts Web Database
2764 ACCSRV#6 Issues Web Database
2764 ACCSRV#5 Projects Web Database

And that’s one more site branding like I want.

 

The question remains: how many other templates are going to give me this problem?  A quick search of the ‘TEMPLATE’ directory in the 14 hive gives me these possibilities:

  • SiteTemplates\AccSrv\default.aspx (the one I just fixed)
  • SiteTemplates\SPSMSITE\public.aspx (part of SPSMSITE#0 aka “Personalization Site”)
  • SiteTemplates\SPSSITES\LISTS\SITESLST\summary.aspx (part of SPSSITES#0 aka “Site Directory”)

From what I can tell, SPSMSITE#0 was the 2007 my site host (SPSMSITEHOST#0 being the 2010 one and SPSPERS#0 is the actually user-specific mysite template), and SPSSITES#0 was the 2007 “Site Directory” site (not supported in 2010, retained for compatibility only).  Since neither of these sites are being supported for this project (only sites creatable via OOTB SP 2010 are supported), it looks like there shouldn’t be any other templates I need to mess with like I did for the ACCSRV series of templates. 

Further testing revealed that the only other thing I needed to change was to use my minimal.master as the CustomMasterUrl for SPSMSITEHOST#0 as well (like I had set it for all the search center templates), and all my sites were now looking nice and branded, just as they should.  A final tweak to ensure I had a placeholder named “PlaceHolderGlobalNavigation” (for _layouts/mysite.aspx) in my default.master, and everything was good to go.  At least until my content started leaking out of it’s zones and over my pretty rounded corners, but that’s a story of an epic fight between tables, divs, images, the layout bug in IE 8 that continues to annoy me, and the thing that makes a solution possible: jQuery.