It’s very common to need to apply a custom master page to your SharePoint site. Very frequently you need to apply your custom master page to all sites within your site collection. And perhaps you’ll even need to apply your master page to all the site collections contained within all your web applications operating within a farm. This is easy enough to accomplish when you are customizing a site for the first time, since most subwebs are set up to inherit from their parent site, so if you customize the parent site, it will apply to all the children. But what happens when a site collection has already been operational, and you want to create a new custom page that applies to all the children sites, even if someone has said they didn’t want to inherit from the parent site? Furthermore, what happens when you deactivate the feature? By deactivating the feature does that mean that you have to reset all the sites back to the “default.master” master page, thereby erasing all the customizations people had made? I have seen a number of articles out there about creating a custom master page and installing it as a feature, but I wanted to take it a step further and write code that will handle applying the master page to all children sites, as well as insuring that the feature could be successfully uninstalled and revert all the sites back to their original master page. (Only if the originally set master page can’t be found will it revert the site to the default.master page.)
Before we begin, the first thing is to note the use of SPWeb.MasterUrl and SPWeb.CustomMasterUrl. A fairly good explanation of the meaning of these two properties can be found on Jim Yang’s blog on SharePointBlogs.com: http://www.sharepointblogs.com/jimyang/archive/2006/07/09/moss-2007-and-wss-3-0-master-page.aspx. Secondly, there’s a good post about creating a feature that installs a custom master page on Paul Papanek’s blog, at http://mindsharpblogs.com/PaulS/archive/2007/06/18/1903.aspx.
OK… on to the code. The first step is to create your SharePoint Visual Studio project to build a feature solution package. To see how to do this, read my blog entry E-mailing a Document from a SharePoint Document Library. Go ahead and create the project, name it GlobalMasterPage, modify its XMl file to create the wsp and cab files, dd the .ddf and .Targets file, and create a key to digitally sign the project. Last but not least, create your feature folder called GlobalMasterPage. Wasn’t that easy? Moving on….
Create a new file inside your TEMPLATE\FEATURES\GlobalMasterTemplate folder and call it MyCustom.master. Go ahead and create the custom master page you want from within SharePoint Designer, then copy and paste the markup into your Visual Studio master page, or else just copy the contents of default.master in the C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\TEMPLATES\GLOBAL directory on your drive. Note: don’t open up the default.master page from the GLOBAL directory in SharePoint Designer; that file is very important to SharePoint and SPD might modify its contents.) Also, create a preview image (which the user will see when they try to select the master page within the SharePoint browser UI), and call it CutomizedMasterPreview.gif, and place it inside the GlobalMasterPage folder as well.
OK, now create a file called elements.xml inside your GlobalMasterTemplate folder. Add the following XML:
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
<Module Url="_catalogs/masterpage" RootWebOnly="TRUE">
<File Url="MyCustom.master"
Name="MyCustom.master"
Type="GhostableInLibrary"
IgnoreIfAlreadyExists="FALSE">
<Property Name="ContentType" Value="$Resources:cmscore,contenttype_masterpage_name;"/>
<Property Name="PublishingPreviewImage"
Value="~SiteCollection/_catalogs/masterpage/Preview Images/Custom/CustomizedMasterPreview.gif,
~SiteCollection/_catalogs/masterpage/Preview Images/Custom/CustomizedMasterPreview.gif" />
<Property Name="Title" Value="MyCustom.master" />
</File>
</Module>
<Module Url="_catalogs/masterpage/Preview Images/Custom" RootWebOnly="TRUE">
<File Url="CustomizedMasterPreview.gif" Name="CustomizedMasterPreview.gif" Type="GhostableInLibrary">
<Property Name="Title" Value="CustomizedMasterPreview.gif" />
</File>
</Module>
</Elements>
If we take a look at what it’s doing, it’s adding a page called MyCustom.master to the /_catalogs/masterpage folder. We’re assigning it three values: a Content Type, a preview image, and a Name. The next thing we’re doing is adding a file to a folder called “Custom”, within the “Preview Images” folder. (If the folder doesn’t already exist, it will be created.) We’re giving that file a Name property as well.
The next step is create a Feature Receiver. This is compiled code that will get fired when the feature gets activated and deactivated. Create a file called FeatureReceiver.cs in the root of your project. Add the following code:
using System;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
namespace GlobalMasterPage
{
public class FeatureReceiver : SPFeatureReceiver
{
public override void FeatureInstalled(SPFeatureReceiverProperties properties) {}
public override void FeatureUninstalling(SPFeatureReceiverProperties properties) {}
public override void FeatureActivated(SPFeatureReceiverProperties properties) {}
public override void FeatureDeactivating(SPFeatureReceiverProperties properties) {}
}
}
The first thing to do is to create constants that point to the files we’ll be working with. We’ll be installing the custom master page in the same directory as the other site collection master pages, which is the /_catalogs/masterpage folder within SharePoint. (This is a virtual folder within the top level site; it doesn’t exist on the file system anywhere.) Add the following constants to your class:
const string defaultMasterUrl = "/_catalogs/masterpage/default.master";
const string customizedMasterUrl = "/_catalogs/masterpage/MyCustom.master";
const string previewImageName = "CustomizedMasterPreview.gif";
Now replace the FeatureActivating method with the following code:
public override void FeatureActivated(SPFeatureReceiverProperties properties)
{
SPSite site = properties.Feature.Parent as SPSite;
if (site == null)
return;
SPWeb rootWeb = site.RootWeb;
rootWeb.AllProperties["OldMasterUrl"] = rootWeb.MasterUrl;
rootWeb.AllProperties["OldCustomMasterUrl"] = rootWeb.CustomMasterUrl;
rootWeb.MasterUrl = customizedMasterUrl;
rootWeb.CustomMasterUrl = customizedMasterUrl;
rootWeb.Update();
foreach (SPWeb subWeb in rootWeb.Webs)
{
ProcessSubWebs(subWeb, true);
}
}
private void ProcessSubWebs(SPWeb web, bool isActivation)
{
if (isActivation)
{
web.AllProperties["OldMasterUrl"] = web.MasterUrl;
web.AllProperties["OldCustomMasterUrl"] = web.CustomMasterUrl;
web.MasterUrl = web.Site.RootWeb.MasterUrl;
web.CustomMasterUrl = web.Site.RootWeb.MasterUrl;
}
else
{
DeactivateWeb(web);
}
web.Update();
foreach (SPWeb subWeb in web.Webs)
{
ProcessSubWebs(subWeb, isActivation);
}
}
First, we’re checkng to make sure the feature’s parent (in this case, site collection, exists. If not, exit and do nothing. The next step is record what the current master and custom master pages are for the top level site. We’ll add a property to the SPWeb to keep track of this information. Note: you do not want to use the Properties property of SPWeb, as this will only return a subset of properties. Instead, make sure you use AllProperties. By adding a property to the property bag, if it doesn’t exist, it will be created, and if it already exists, it will be overwritten. After we’ve done that, we can reset the root level site’s MasterUrl and CustomMasterUrl properties. You must call SPWeb.Update() or else the properties you added to the SPWeb will not be saved. Finally, after we’ve done that, we recursively iterate through all the child sites and point them to this top level site’s master page. (When you are finished, if you look at a child’s site’s Site Settings page for selecting a master page, you’ll see that the radio button is checked indicating it’s inheriting its master page from its parent.)
Easy enough. The next step is to write the code to deactivate the feature. This is slightly more complicated. Whereas we’re using the solution package to deploy the master page and preview image to the correct directory in the site collection, we’ll have to manually reset all the subsites back to their original master page (after we’ve determined if that master page still exists), then manually delete the custom master page and preview image.
Replace the FeatureDeactivating method with the following code:
public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
{
SPSite site = properties.Feature.Parent as SPSite;
if (site == null)
return;
SPWeb rootWeb = site.RootWeb;
DeactivateWeb(rootWeb);
rootWeb.Update();
foreach (SPWeb subWeb in rootWeb.Webs)
{
ProcessSubWebs(subWeb, false);
}
if (rootWeb.MasterUrl != customizedMasterUrl)
{
try
{
bool fileExists = rootWeb.GetFile(customizedMasterUrl).Exists;
SPFile file = rootWeb.GetFile(customizedMasterUrl);
SPFolder masterPageGallery = file.ParentFolder;
SPFolder temp = masterPageGallery.SubFolders.Add("Temp");
file.MoveTo(temp.Url + "/" + file.Name);
temp.Delete();
fileExists = masterPageGallery.SubFolders["Preview Images"].SubFolders["Custom"].Exists;
SPFolder customFolder = masterPageGallery.SubFolders["Preview Images"].SubFolders["Custom"];
if (customFolder.Files.Count == 1 && customFolder.Files[0].Name == previewImageName)
{
masterPageGallery.SubFolders["Preview Images"].SubFolders["Custom"].Delete();
}
else if (customFolder.Files.Count > 1 && customFolder.Files[previewImageName].Exists)
{
customFolder.Files[previewImageName].Delete();
}
}
catch (ArgumentException)
{
return;
}
}
}
private void DeactivateWeb(SPWeb web)
{
if (web.AllProperties.ContainsKey("OldMasterUrl"))
{
string oldMasterUrl = web.AllProperties["OldMasterUrl"].ToString();
try
{
bool fileExists = web.GetFile(oldMasterUrl).Exists;
web.MasterUrl = oldMasterUrl;
}
catch (ArgumentException)
{
web.MasterUrl = defaultMasterUrl;
}
string oldCustomUrl = web.AllProperties["OldCustomMasterUrl"].ToString();
try
{
bool fileExists = web.GetFile(oldCustomUrl).Exists;
web.CustomMasterUrl = web.AllProperties["OldCustomMasterUrl"].ToString();
}
catch (ArgumentException)
{
web.CustomMasterUrl = defaultMasterUrl;
}
web.AllProperties.Remove("OldMasterUrl");
web.AllProperties.Remove("OldCustomMasterUrl");
}
else
{
web.MasterUrl = defaultMasterUrl;
web.CustomMasterUrl = defaultMasterUrl;
}
}
The first thing to do, as before, is make sure the parent site collection exist. Next, we’ll deactivate the top level site. Inside DeactivateWeb, what we’ll do is see if the “OldMasterUrl” property still exists in the web’s properties collection. If it doesn’t we have no alternative than to revert back to default.master. If we do find the OldMasterUrl property, check to make sure that the file it pointed to before still exists. (Note: although it would seem intuitive that accessing the Exists property on an SPFile object would return True or False, in fact, if a file doesn’t exist, it throws an ArgumentException error. Also, you can’t just call SPFile.Exists becuase it thinks you’re trying to set it’s value. That’s why I created a dummy boolean, simply as an excuse to trigger the error if the file (or SPFolder) doesn’t exist.) We follow the same exercise for the OldCustomMasterUrl property, (although we’re not explictly checking for existence. It seems safe to me if one of our custom properties are there, the other will be as well.) If one or both original master pages don’t exist in SharePoint any more at their old location, we revert back to default.master. Finally, we delete our custom properties because they are no longer needed. We then iterate through this same process for every child site.
Going back to our DeactivateFeature method, the next step is to delete our custom master page and icon from the /_catalogs/masterpage directory. Unfortunately, there seems to be an issue in SharePoint where you can not delete a master page that was installed by a feature, even if no one is referencing that master page. It will throw an error saying, “This item cannot be deleted because it is still referenced by other pages.” Luckily, I found a quick, although hackish, work around on “C-Dog’s .NET Tip of the Day” blog at http://www.dotnettipoftheday.com/Blog.aspx?Id=376. You have to move the page to another folder, then delete that folder. Also, we installed the preview image to a new folder called “Custom”. Although the foler is meant to house our custom image, it’s possible someone else could have added more content to it since it was created. If there’s nothing other than our custom image in it, delete the folder (along with the image); otherwise, just delete our image out of it. And that’s it… the feature receiver class is done.
Now it’s time to create the feature manifest. Create a file called feature.xml inside your GlobalMasterPage folder. Add the following XML:
<Feature xmlns="http://schemas.microsoft.com/sharepoint/"
Id="400D8B01-1F64-46b4-A1DD-A1DA6A0E8E94"
Title="Custom Master Page"
Hidden="FALSE"
Scope="Site"
Version="1.0.0.0"
ReceiverAssembly="GlobalMasterPage, Version=1.0.0.0, Culture=neutral, PublicKeyToken=a56f42ad8ea54585"
ReceiverClass="GlobalMasterPage.FeatureReceiver">
<ElementManifests>
<ElementManifest Location="elements.xml" />
<ElementFile Location="MyCustom.master" />
<ElementFile Location="CustomizedMasterPreview.gif"/>
</ElementManifests>
</Feature>
Several things to note: the Scope is “Site”, which means it’s applied at the Site Collection level. Secondly, notice that we are referencing our FeatureReceiver class, so that it’s fired when the feature is installed or uninstalled. You can also see that the custom master page and preview image files are being included in the ElementManifests section, along with the elements.xml manifest file itself.
Now it’s time to add the manifest file. It should look like this:
<Solution xmlns="http://schemas.microsoft.com/sharepoint/"
SolutionId="AAB93B39-E127-4b90-B966-E6DC60D06C76"
DeploymentServerType="WebFrontEnd"
ResetWebServer="TRUE">
<Assemblies>
<Assembly DeploymentTarget="GlobalAssemblyCache" Location="GlobalMasterPage.dll">
<SafeControls>
<SafeControl Namespace="GlobalMasterPage" TypeName="*" Safe="True" />
</SafeControls>
</Assembly>
</Assemblies>
<FeatureManifests>
<FeatureManifest Location="GlobalMasterPage\feature.xml"/>
</FeatureManifests>
</Solution>
Looking at this, we can see that our DLL is being added to the GAC as well as the SafeControls node in the web.config, so it can be called and executed safely from within SharePoint. The manifest also points to the feature manifest.
Last but not least, modify your .ddf file so it looks like this:
.OPTION Explicit
.Set DiskDirectoryTemplate=CDROM
.Set CompressionType=MSZIP
.Set UniqueFiles=Off
.Set Cabinet=On
manifest.xml
bin\debug\GlobalMasterPage.dll GlobalMasterPage.dll
.Set DestinationDir=GlobalMasterPage
TEMPLATE\FEATURES\GlobalMasterPage\feature.xml
TEMPLATE\FEATURES\GlobalMasterPage\elements.xml
TEMPLATE\FEATURES\GlobalMasterPage\MyCustom.master
TEMPLATE\FEATURES\GlobalMasterPage\CustomizedMasterPreview.gif
Once you’ve finished compiling your code, you can load the .wsp file into SharePoint using the stsadm tool. When it’s time to deploy the solution, you can either deploy it to every web application in the farm, or to a particular site collection. Once it has been deployed to a site collection, you can browse to that particular site collection and activate the feature. You should see all the pages now skinned with your new master page. (Note: you can’t scope this feature for an entire farm, or even a web application, because the feature installs a master page into a particular top level site collection. If you try to scope it as “Farm”, SharePoint will throw an error. That means, the way this code works, you can deploy the code to every web application, but you have to activate the feature for each site collection. You could probably write another feature that activates this feature for each site collection, but that’s outside the scope of this blog post.)
You can download the source code here. Have fun and good luck.