My Feature Stapler doesn’t work, what to do?


Problem 1

You’ve been asked to add a new navigation link, remove a column from a list or maybe modify a webpart on a page to every team site within your farm, but there are over 200 of them, and you’ve got to ensure that any future team sites created will also incorporate these new changes. A Stapler won’t work for existing sites, therefore a PowerShell script / Console App will need to be run to fix existing sites, then create a stapler feature for all new sites, then you might encounter the next problem.

Problem 2

You are creating a new site and added your code as a feature stapler, but it throws an error when your site is being created. Why? It turns out that your feature is trying to access something like SharePoint Navigation or a list, before it has been created in the site. With feature staplers you cannot always guarantee the order they will be stapled to a site and activated. Therefore you get the error.

Solution

We need a way to ensure our feature code is run once the site is fully created. So the solution is to use a Control Template and a delegate control, with a self-deactivating feature so that it isn’t run multiple time.

Simply what will happen is there will be a delegate control sitting on the page, when the first user hits the page, it will run the code within the Page_Load method. This will add the Navigation Link, List, change content type of all Items etc, and then once run it will deactivate the feature containing the delegate control to prevent it running again. Everything is now in place, the first user might get 1 redirect and slower loading time, while processing, but if they hit the page again or any following user hits the page everything will be in place and load up at normal SharePoint speed.

Basic layout of project shown above

The following steps will show you the basic setup.

In your SharePoint project, create a Control Template, I’ve called my Initialiser.ascx. This control template on load performs the main guts of what you want your Stapler to do.

We have a FeatureGuid as a public property. This is to pass in the FeatureID of our delegate control that holds this Control Template in. We will build that bit later.

using System;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using Microsoft.SharePoint;
namespace CFSP.FeatureStaplerWithControlTemplate.CONTROLTEMPLATES
{
    public partial class Initialiser : UserControl
    {
        public string FeatureGuid { get; set; }
        protected void Page_Load(object sender, EventArgs e)
        {
            bool success = false;
            var currentWebUrl = SPContext.Current.Web.Url;
            SPSecurity.RunWithElevatedPrivileges(delegate()
            {
                using (SPSite site = new SPSite(currentWebUrl))
                {
                    using (SPWeb web = site.OpenWeb())
                    {
                        try
                        {
                            web.AllowUnsafeUpdates = true;
                            if (SomeCodeToPerform())
                            {
                             //Deactivate the feature that this control is linked to so that this code will not run again.
                            bool hasFeature = false;</span>
                             foreach (SPFeature feature in web.Features)
                             {
                              if (feature.DefinitionId.ToString().Equals(FeatureGuid, StringComparison.CurrentCulture))
                              {
                                hasFeature = true;
                                success = true;
                                break;
                              }
                             }

                             if (hasFeature)
                             {
                                 web.Features.Remove(new Guid(FeatureGuid));
                             }
                            }
                        }
                        catch (Exception ex)
                        {
                            //Log message in ULS.
                        }
                        finally
                        {
                            web.AllowUnsafeUpdates = false;
                        }
                    }
                }
            });

 //If code is affecting landing page, if success refresh the page. This only happens for the first user who hits this page.
 //Don't refresh if the success is false otherwise you will encounter endless loop!</span>
 if(success)
     Page.Response.Redirect(currentWebUrl, false);
}

private bool SomeCodeToPerform()
{
 /*A function that will perform something that might not work or cannot work in a feature stapler due to timing.
  * For Example:
  * Adding Navigation Item to List
  * Remove a Field from a content type
  * Add a list
  * Change a content type for all items in a list
  * etc
  */

 return true;
}
}
}

If you read through the code above, you’ll see that I’m running with elevated Privileges this is because we don’t know what permissions the first user who hits the page will have. They might be a site owner, but they most likely will be a site visitor or contributor. They wouldn’t have access to create, change or deactivate the feature. Once the changes have been made to the site, it looks for the Feature GUID and deactivates the feature. This feature GUID is itself, and by removing it, it will prevent any following users from running it on this site.

Next we will need to create a DelegateControl, add an empty element and in the Elements.xml file. We also need to give a FeatureGuid property. We will fill in this information in the next step.

<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <Control Id="AdditionalPageHead"
           Sequence="100"
           ControlSrc="~/_controltemplates/CFSP/Initialiser.ascx">
    <Property Name="FeatureGuid">  TODO  </Property>
  </Control>
</Elements>

Add a web feature to the project. Make the feature hidden (to ensure no user can enable or disable it in manage features), and add the DelegateControl from the last step to it. In the property window of this feature find the Feature Id and copy it to the FeatureGuid of the DelegateControl property,

    <Property Name="FeatureGuid">ac5887fe-926f-426f-9c77-007d83f0b521</Property>
 

If you are stapling to an existing template of SharePoint such as mysite, blogs, teamsite etc follow the next step few steps to ensure you staple the delegate control to your template. If you have a custom WebTemplate then add the delegate control feature to your WebTemplate onet.xml file and you ready to skip to the powershell section.

Add another empty element to the project and in the Elements.xml file add a FeatureSiteTemplateAssociation. In my example I’m adding this to the blog template. Remember the ID is the Feature ID of the delegate control.

<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <FeatureSiteTemplateAssociation Id="ac5887fe-926f-426f-9c77-007d83f0b521" TemplateName="BLOG#0" />
</Elements>

Add a WebApplication feature to the project, and add the stapler from the last step to it.

If you deploy this solution, you will find (in my example) that every new blog site will run my code, because of the stapler. However all current existing sites don’t. This is because the stapler only runs on newly created sites, and the delegate control feature hasn’t been activated on any existing site. To complete this solution we need to create a powershell script that will activate the delegate control feature for all existing blog sites. (Remember when the feature is activated, all it does is adds the delegate control. No code to change the site is made until a person hits the page).

PowerShell

$rootSite = New-Object Microsoft.Sharepoint.SPSite "http://cfsp/"
foreach ($site in $rootSite.WebApplication.Sites) {
foreach ($web in $site.AllWebs) {
if ($web.WebTemplate -ne "BLOG"){continue}
          EnableFeature "FeatureStaplerWithControlTemplate_Web - Initialiser" $web.Url
}
}

The full solution code can be found on my skydrive

Advertisements