For developers, the Feature Stapling facility of SharePoint 2007 adds a lot of flexibility to how we can customize a site template that is already in production. For example, you can use Feature Stapling to add extra features to the out-of-the-box site templates without modifying them. There are lots of examples of how this is done, for example:

The problem

There is one challenge I ran into a couple of times and I couldn't find a very satisfying solution. The problem is that if you like to run some code from a Feature Receiver against a list or a page provisioned by a site definition, and you like this code to be run duing provisioning of the site by stapling your feature receiver code via Feature Stapling, the Feature Receiver code would fail because the list or the page has not yet been provisioned when the stapled feature actually runs. I have read different workarounds like configuring a timer job to run the code, or introduce a delay within the code and hope that your delay gives the server enough time to finish provisioning of the pages or lists that the code would run against. But none of these make me feel too confident that the solution would execute properly in a client environment.

The problem is about the order of execution. If we can ensure the pages or lists our code has dependencies on have been provisioned, then we won't have this problem. With Feature Stapling, this dependency cannot be ensured because we cannot control when the page or list is provisioned and when the stapled feature that contains the feature receiver code is run.

The solution

One possible solution is to run the code when we are sure the site finished provisioning. How about running the code the first time the site is rendered, then that achieves the goal.

So how do we achieve that? The trick is to hook the code that you would normally run in a feature receiver to a server control that would be executed when the site is displayed for the first time, but not subsequent times. I choose to use a well known SharePoint technique that can be activated and devactivated by features: Delegate Control.

With a Delegate Control, when you activate it, it is linked to the Delegate Control container defined on a page. And when deactivated, the control will not be used. This gives me the flexibility such that I can staple the Delegate Control feature to the site definition, deactivate the Delegate Control feature at the end of the code block, essentially ensuring that this code only executes once.

Application

One application of this technique is to add an additional custom content type to the Personal Documents document library in My Site when the site collection is provisioned. If you enable users to have self site provisioning permission, their My Site is provisioned when they click on the My Site link in your portal. Since adding a content type to a specific list needs the list to exist, this technique fits the bill.

Sample Code

The solution would compose of the following bits and pieces:

  1. A feature that handles stapling the delegate control feature to the My Site site definition,
  2. A feature that defines the delegate control,
  3. A custom user control that does not have any UI, but code behind which adds the content type during On_PageLoad event.

1: Feature Stapling

This feature contains only the feature.xml and an Element.xml. The Element.xml tells SharePoint to staple a feature with ID="AF8D7B22-2238-4F20-9A1C-1DEC58195361" to the My Site site definition. Notice that this feature is a Farm feature. But the stapling targets the My Site site definition only.

Feature.xml
<?xml version="1.0" encoding="utf-8" ?>
<Feature
	Id="491CCEA0-5B5C-46A9-A2A6-6619FA9B688B"
	ActivateOnDefault="TRUE"
	AlwaysForceInstall="TRUE"
	DefaultResourceFile="core"
	Hidden="FALSE"
	Title="DeleteControlFeatureStapler"
	Description="A feature that staples other features to the My Site site definition"
	Scope="Farm"
	Version="1.0.0.0"
	xmlns="http://schemas.microsoft.com/sharepoint/">
	<ElementManifests>
		<ElementManifest Location="Elements.xml" />
	</ElementManifests>
</Feature>
Elements.xml
<?xml version="1.0" encoding="utf-8" ?>
<Elements Id="40A27020-EC75-4D6E-B593-0A0407853A42" xmlnshttp://schemas.microsoft.com/sharepoint/">
	<FeatureSiteTemplateAssociation Id="AF8D7B22-2238-4F20-9A1C-1DEC58195361" TemplateName="SPSPERS#0" />
</Elements>

2: Delegate Control

This feature, when activated, specifies a custom user control to be added to the page. By inspecting the My Site, it is using the default.master master page. In this master page, there is one delegate control with ControlID="AdditionalPageHead" that allows multiple controls to be instantiated within it. This is the best case scenario as I know that I would not have to replace any controls from the OOTB My Site experience, I am simply adding to it. Also this control is in the <HEAD> server tag, which means that there is no UI to be rendered. This again makes perfect sense to use this delegate control as I only need server side code to be run.

This feature contains Feature.xml and Elements.xml.

Feature.xml
<?xml version="1.0" encoding="utf-8" ?>
<Feature Id="AF8D7B22-2238-4F20-9A1C-1DEC58195361"
	Title="Delegate Control feature"
	Description="By activating this feature, a user control will be added to the delegate control with ControlId=AdditionalPageHead"
	Hidden="TRUE"
	Scope="Site"
	ActivateOnDefault="FALSE"
	AlwaysForceInstall="TRUE"
	DefaultResourceFile="core"
	Version="1.0.0.0"
	xmlns="http://schemas.microsoft.com/sharepoint/">
	<ElementManifests>
		<ElementManifest Location="Elements.xml"/>
	</ElementManifests>
</Feature>
Elements.xml
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
	<Control Id="AdditionalPageHead" ControlSrc="~/_controltemplates/CustomizationHook.ascx" />
</Elements>

3: Custom User Control

As mentioned, the user control does not have any UI, there is only server side code to be run.

CustomizationHook.ascx
<%@ Control
	Language="C#"
	compilationMode="Always"
	AutoEventWireup="true"
	Inherits="CustomizationLibrary.CustomizationHook, CustomizationLibrary, Version=1.0.0.0, Culture=neutral, PublicKeyToken=0dcddfee03fdddd9" %>
CustomizationHook.ascx.cs
using System;
using System.Collections;
using System.Collections.Generic;
using Microsoft.SharePoint;

namespace CustomizationLibrary
{
	public partial class CustomizationHook : System.Web.UI.UserControl
	{
		protected void Page_Load(object sender, EventArgs e)
		{
			SPWeb web = SPContext.Current.Web;
			SPSite site = web.Site;
			string customizationFeatureGuid = "AF8D7B22-2238-4F20-9A1C-1DEC58195361";
			string personalDocLibName = "Personal Documents";  // this is the OOTB library defined in My Site site definition I like to modify
			try
			{
				// this is essential
				web.AllowUnsafeUpdates = true;
				
				// you may need to see if this way of getting an SPList is appropriate for your situation.
				SPList list = web.Lists[personalDocLibName];  
				
				// I am assuming that the content type is already added to the SPWeb, you might have to do more work here depending on your situation.
				SPContentType customContentType = web.ContentTypes["customContentTypeName"]; 

				AddAsDefaultContentType(list, customContentType);
				web.Update();
				
				// trying to deactivate the feature that this control is linked to so that this code will not run again.
				bool hasFeature = false;
				foreach (SPFeature f in site.Features)
				{
					if (f.DefinitionId.ToString().Equals(customizationFeatureGuid, StringComparison.InvariantCultureIgnoreCase))
					{
						hasFeature = true;
						break;
					}
				}
				if(hasFeature) site.Features.Remove(new Guid(customizationFeatureGuid));
			}
			finally
			{
				// make sure it is set to false and the end of processing
				web.AllowUnsafeUpdates = false;
			}
		}
		
		private static void AddAsDefaultContentType(SPList list, SPContentType customContentType)
		{
			// enabling content type management on this list
			list.ContentTypesEnabled = true;
			list.Update();

			// adding the custom content type
			list.ContentTypes.Add(customContentType);
			list.Update();

			// setting it as default content type
			if (list.RootFolder.ContentTypeOrder != null)
			{
				IList<SPContentType> currentOrder = list.RootFolder.ContentTypeOrder;

				SPContentType[] orderedCT = new SPContentType[list.RootFolder.ContentTypeOrder.Count];
				orderedCT[0] = list.ContentTypes[customContentType.Name];
				int j = 1;
				for (int i = 0; i < list.RootFolder.ContentTypeOrder.Count; i++)
				{
					if (!list.RootFolder.ContentTypeOrder[i].Id.ToString().Contains(customContentType.Id.ToString()))
					{
						orderedCT[j] = list.RootFolder.ContentTypeOrder[i];
						j++;
					}
				}
				list.RootFolder.UniqueContentTypeOrder = orderedCT;
				list.RootFolder.Update();
			}
		}
	}
}

As you can see, the user control will run the custom code and then deactivate the stapled feature in the current site collection so that the delegate control will not be instantiated again the next time this page is loaded. This way we achieved the result of modifying a list that is provisioned by the OOTB site definition by using Feature Stapling.

Share