Custom Site Definition '3rd way'.
Do you want a custom site definition, but can't face copying and modifying an ONet? Need to use an OOTB Site Definiton, but also change it? Like features, but hate the issues stapling gives you? Then read on…

The debate about this rages on-and-on (see Microsofts guidelines here, and other developers perspective on upgrade issues here, here and here). Some like features, some like stapling, some like to use the .onet file. Each has its pros and cons. But all agree it would be nice to end up with a deployable sharepoint solution, use features within it, build on the OOTB site definitions (templates), provide a custom site definition the user can easily choose for their new collection, all without hacking a big Onet file. So how to square the circle for us devs?
Raymond Mitchell deserves the credit for this one using web provisioning, as I based my approach on his suggestions and added my preference for Features and WSPBuilder on top of it.
Note: I abandoned my original preference of stapling a master feature within this approach, as I couldn't get the stapled features to activate automatically (stapling happens at farm level, but provisioning is at web level), in the right order or to wait for provisioning to complete. I do think this aspect warrants revisiting as I may have made some mistakes and it would be nice to use more of SharePoint's built-in abilities.
In my implementation I have the following web temp to give us our new site definition:
<?xml version="1.0" encoding="utf-8"?>
<Templates xmlns:ows="Microsoft SharePoint">
<!– OOTB IDs are listed here: /mossms/archive/2009/02/09/configure-this.aspx –>
<Template Name="Your.Project.Template" ID="9999999">
<!– Choose a unique number for this Customisation's ID that is above 10000. –>
<Configuration
ID="0"
Title="Your.Project.Template#0"
Hidden="FALSE"
ImageUrl="/_layouts/images/Your.Project/templatePrev.png"
Description="Custom Your.Project.Template#0 used to provision the OOTB Document Center (BRD#0) before automatic customisation with features."
DisplayCategory="Custom"
AllowGlobalFeatureAssociations="TRUE"
Version="12.0.0.0"
ProvisionAssembly="Your.Project, Version=1.0.0.0, Culture=neutral, PublicKeyToken=Your.Project.snk"
ProvisionClass="Your.Project.l033.XML.WebProvisioningProvider">
</Configuration>
</Template>
</Templates>
It should be placed in the 12 hive location here:

Have a look at 'Configure this' for a list of existing template IDs, but ensure you use a new unique ID above 10000. This file will have named the custom site definition (template) and points to my provisioning provider shown below:
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.SharePoint;
namespace Your.Project.l033.XML
{
public class WebProvisioningProvider : SPWebProvisioningProvider
{
// Set your existing template and #configuration number here
public static string WebTemplateConfiguration = "BDR#0";
// OOTB Configurations are listed here: /mossms/archive/2009/02/09/configure-this.aspx –>
// Set your 'master' site (collection) scope feature here (master feature will activate sub features – can't use feature associations as these don't wait until site is provisioned).
public static Guid SiteCollectionMasterFeatureId = new Guid("Your-Projects-Site-Master-Feature-Guid");
// Set your 'master' web scope feature here
public static Guid WebMasterFeatureId = new Guid("Your-Projects-Web-Master-Feature-Guid");
public override void Provision(SPWebProvisioningProperties properties)
{
// Provision a site based on the OOTB Document Center site definition – do not dispose the spweb as it it part of the context.
SPWeb web = properties.Web;
if (WebTemplateConfiguration != null && WebTemplateConfiguration.Trim().Length > 0)
{
web.ApplyWebTemplate(WebTemplateConfiguration);
web.Update();
}
//Activate the master Site collection scope feature
if (SiteCollectionMasterFeatureId != null && SiteCollectionMasterFeatureId != Guid.Empty)
{
SPSite site = null; // Must be disposed
try
{
site = web.Site;
site.Features.Add(SiteCollectionMasterFeatureId);
}
finally
{
if (site != null)
{
site.Dispose();
}
}
}
//Activate the master Web scope feature
if (WebMasterFeatureId != null && WebMasterFeatureId != Guid.Empty)
{
web.Features.Add(WebMasterFeatureId);
}
}
}
}
I resisted the desire to reference the Stair Master feature, and placed my provisioning provider code here:

The provisioning provider will trigger when a new web (when a site collection is created you get the collections root web by default, so it will be triggered then) is created. My example above shows that it optionally provisions an OOTB template (see appendix my previous post for a list of OOTB definitions), activates a master site collection scope feature and a master web scope feature by referencing their Guids. Other scoped features could be added, but there may be some governance/assurance implications in applying features scoped beyond your specific ‘scope’ requirement.
My elements file is empty for now, but the site master feature xml is shown below:
<?xml version="1.0" encoding="utf-8"?>
<Feature Id="Your-Projects-Site-Master-Feature-Guid"
Title="Your.Project.Features.Site.Master"
Description="Description for Your.Project.Features.Site.Master MASTER feature"
Version="12.0.0.0"
Hidden="FALSE"
Scope="Site"
ImageUrl="Your.Project/your.Project.png"
DefaultResourceFile="core"
ReceiverAssembly="Your.Project, Version=1.0.0.0, Culture=neutral, PublicKeyToken=Your.Project.snk"
ReceiverClass="Your.Project.Features.Site.Master.Receiver"
xmlns="http://schemas.microsoft.com/sharepoint/">
<ElementManifests>
<ElementManifest Location="elements.xml"/>
</ElementManifests>
</Feature>
Remember to replace the Guid and Assembly public key with your own values – this feature lives here in my world:

This is the master site (collection) scope feature receiver code – don' worry about my utils 'using' for a dll reference, it's only used for the logging so comment out that code if you copy from here:
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.SharePoint;
using System.Threading;
//using Your.Project.Features.Site.Utils.Logging; // Comment line out if you copy this code
namespace Your.Project.Features.Site.Master
{
// Single 'master' feature for a site definition. Use this to activate other custom features as stapling does not activate features in order
public class Receiver : SPFeatureReceiver
{
// *** Add your unstapled Site Collection Scope Features here (could be driven from an xml file as this approach matures) ***
public static Guid[] FeatureGuids = new Guid[] {
//new Guid("F6924D36-2FA8-4f0b-B16D-06B7250180FA"), // This is the OOTB publishing feature required for style library creation
new Guid("Your-Projects-Site-Utils-Feature-Guid") // Your.Project.Features.Site.Utils – this is just an example feature
}; // You can make these features' xml property 'Hidden="TRUE"' once testing is complete
public override void FeatureActivated(SPFeatureReceiverProperties properties)
{ // NB: Do not dispose properties.Feature.Parent, as that is the feature context, just like the Current web context, but other spweb or spsites should be disposed.
//Activate Unstapled Site Collection Scope Features (do not wait for root web to be provisioned here, as we will wait forever…)
SPSite site = properties.Feature.Parent as SPSite;
if (FeatureGuids != null)
{
foreach (Guid guid in FeatureGuids)
{
if (guid != null && guid != Guid.Empty)
{
try
{
site.Features.Add(guid, true);
}
catch (Exception ex)
{
//Log.Write(ex); // comment out and replace with own procedures if you copy this code
}
}
}
}
}
public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
{ // NB: Do not dispose properties.Feature.Parent, as that is the feature context, just like the Current web context, but other spweb or spsites should be disposed.
SPSite site = properties.Feature.Parent as SPSite;
//Deactivate the master web scope feature on all subwebs in reverse subweb order
if (Your.Project.l033.XML.WebProvisioningProvider.WebMasterFeatureId != null && Your.Project.l033.XML.WebProvisioningProvider.WebMasterFeatureId != Guid.Empty)
{
for (int i = site.AllWebs.Count -1; i >= 0; –i)
{
SPWeb web = null;
try
{
web = site.AllWebs
;
web.Features.Remove(Your.Project.l033.XML.WebProvisioningProvider.WebMasterFeatureId, true);
}
catch (Exception ex)
{
//Log.Write(ex); // comment out and replace with own procedures if you copy this code
}
finally
{
if (web != null)
{
web.Dispose();
}
}
}
}
//Deactivate unstapled Site collection scope features in reverse order
if (FeatureGuids != null)
{
for (int i = FeatureGuids.Length -1; i >= 0; –i)
{
try
{
Guid guid = FeatureGuids
;
SPFeature feature = site.Features[guid];
site.Features.Remove(guid, true);
}
catch (Exception ex)
{
//Log.Write(ex); // comment out and replace with own procedures if you copy this code
}
}
}
}
public override void FeatureInstalled(SPFeatureReceiverProperties properties) {}
public override void FeatureUninstalling(SPFeatureReceiverProperties properties) {}
}
}
..and you can find it here – after this I think you'll have the idea of where I locate my code and the xml 12 hive locations, so I'll stop posting my project structure:

This activates all site collection features in order (otherwise dependencies may cause activation issues) and deactives them in reverse. It also deactivates and web features when you pull the site master by call in the web master feature I'll cover next. That probably isn't necessary, but it seems to prevent issues for me.
So below is the master web scope feature xml and code, activating and deactivating web features in a similar way to the site master, but first waiting for web provisioning to complete. This ensures all OOTB items are present for us to find/modify with our features. NB. If timing issue occur between site collection and web scoped feature activation, it could be done in the single web master feature below, with wider scopes activated first BEFORE we wait for OOTB web provisioning to complete.
<?xml version="1.0" encoding="utf-8"?>
<Feature Id="Your-Projects-Web-Master-Feature-Guid"
Title="Your.Project.Features.Web.Master"
Description="Description for Your.Project.Features.Web.Master MASTER feature"
Version="12.0.0.0"
Hidden="FALSE"
Scope="Web"
ImageUrl="Your.Project/your.Project.png"
DefaultResourceFile="core"
ReceiverAssembly="Your.Project, Version=1.0.0.0, Culture=neutral, PublicKeyToken=Your.Project.snk"
ReceiverClass="Your.Project.Features.Web.Master.Receiver"
xmlns="http://schemas.microsoft.com/sharepoint/">
<ElementManifests>
<ElementManifest Location="elements.xml"/>
</ElementManifests>
</Feature>
…and the receiver code this feature points to:
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.SharePoint;
using System.Threading;
//using Your.Project.Features.Site.Utils.Logging; // Comment line out if you copy this code
namespace Your.Project.Features.Web.Master
{
// Single 'master' feature for a site definition. Use this to activate other custom features as stapling does not activate features in order and doesn't wait for
rovisioning to complete
public class Receiver : SPFeatureReceiver
{
//Add your unstapled Web Scope Features here (could be driven from an xml file as this approach matures) – these are examples of the sorts of features you can add
public static Guid[] FeatureGuids = new Guid[] {
new Guid("Your-Projects-Web-Customisation-Feature-Guid"), // Your.Project.Features.Web.Customisation
new Guid("Your-Projects-Web-ListInstances-Feature-Guid"), // Your.Project.Features.Web.ListInstances
new Guid("Your-Projects-Web-Branding-Feature-Guid") // Your.Project.Features.Web.Branding
//new Guid("fc5e2840-0b48-42eb-9ad7-076f5add58ad") // OOTB HIDE the view all site content link feature
}; // You can make these features' xml property 'Hidden="TRUE"' once testing is complete
public override void FeatureActivated(SPFeatureReceiverProperties properties)
{ // NB: Do not dispose properties.Feature.Parent, as that is the feature context, just like the Current web context, but other spweb or spsites should be disposed.
// Ensure Web is provisioned (otherwise provisioning may not complete before feature activation starts)
SPWeb web = properties.Feature.Parent as SPWeb;
while (!web.Provisioned)
Thread.Sleep(1000);
//Activate Unstapled Web Scope Features
if( FeatureGuids != null )
{
foreach (Guid guid in FeatureGuids)
{
if (guid != null && guid != Guid.Empty)
{
try
{
web.Features.Add(guid, true);
}
catch (Exception ex)
{
//Log.Write(ex); // comment out and replace with own procedures if you copy this code
}
}
}
}
}
public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
{ // NB: Do not dispose properties.Feature.Parent, as that is the feature context, just like the Current web context, but other spweb or spsites should be disposed.
//Deactivate Unstapled Web Scope Features in reverse order
SPWeb web = properties.Feature.Parent as SPWeb;
if (FeatureGuids != null)
{
for (int i = FeatureGuids.Length – 1; i >= 0; –i)
{
try
{
Guid guid = FeatureGuids
;
SPFeature feature = web.Features[guid];
web.Features.Remove(guid, true);
}
catch (Exception ex)
{
//Log.Write(ex); // comment out and replace with own procedures if you copy this code
}
}
}
}
public override void FeatureInstalled(SPFeatureReceiverProperties properties) {}
public override void FeatureUninstalling(SPFeatureReceiverProperties properties) {}
}
}
So once this has been done, each new feature simply requires adding into the correct master sope features code. I would rather maintain the flexibility of reproducing this simple process for each project, but this procedure could easily be extended to provide some automation, resources or configuration file driven facilities (eg. an xml file to choose the OOTB configuration and list each feature, grouped by scope, in their desired order).

The end of another MOSSuMS post