A collegue of mine prepared a list in her SharePoint 2007 MySite. Her question was how she could make this list available as a template in everybody’s MySite. The issue with this is that every MySite is a separate Site Collection, and therefore every MySite has its own site columns, content types and template galleries. This makes it difficult to deploy to list template to 1 location from where it can be used.
There are a number of ways how you can get around this. I choose to create a feature that uploads STP files to the list template gallery when the feature gets activated. The feature gets activated in the site definition, so when a user creates his/her MySite for the first time, the STP files are uploaded to the list template gallery. In our case, this was only part of the solution, because the feature is not activated on existing MySites. I created a STSADM command that activates the feature if it is not yet activated. This is a generic command that you can use to (de)activate any site feature on all sites based on a specific template.
Step 1 – Create a FeatureReceiver
The first step is to create a FeatureReceiver that uploads the STP file if it does not exist. If the list template is already available in the list template gallery, the feature deletes the file and uploads the latests version. The feature checks the feature folder for a subfolder called “ListTemplates”. All STP files in this folder are uploaded to the List template gallery. First we get a reference to the site the feature gets activated for and the STP files in the folder:
public override void FeatureActivated(SPFeatureReceiverProperties properties)
{ SPSite site = properties.Feature.Parent as SPSite;
if (site != null)
{ try
{ string directory = properties.Definition.RootDirectory;
if (!directory.EndsWith(@"\"))
directory += @"\";
directory += "ListTemplates";
if (System.IO.Directory.Exists(directory))
{ string[] templates = System.IO.Directory.GetFiles(
directory, "*.stp", System.IO.SearchOption.TopDirectoryOnly);
SPDocumentLibrary listTemplates =
site.GetCatalog(SPListTemplateType.ListTemplateCatalog)
as SPDocumentLibrary;
UploadTemplates(listTemplates, templates);
}
}
finally
{ site.Dispose();
}
}
}
The code snippet also gets reference to the List template gallery. This gallery is a SharePoint document library and can be found using the GetCatalog function of our SPSite object. The method ‘UploadTemplates’ actually takes care of uploading the STP file:
private void UploadTemplates(SPDocumentLibrary templateGallery, string[] templateFiles)
{ if (templateGallery != null)
{ foreach (string template in templateFiles)
{ System.IO.FileInfo fileInfo = new System.IO.FileInfo(template);
SPQuery query = new SPQuery();
query.Query = string.Format(
"<Where><Eq><FieldRef Name='FileLeafRef'/>" +
"<Value Type='Text'>{0}</Value></Eq></Where>", fileInfo.Name); SPListItemCollection existingTemplates = templateGallery.GetItems(query);
int[] Ids = new int[existingTemplates.Count];
for (int i = 0; i < existingTemplates.Count; i++)
{ Ids[ i ] = existingTemplates[ i ].ID;
}
for (int j = 0; j < Ids.Length; j++)
{ templateGallery.Items.DeleteItemById(Ids[j]);
}
byte[] stp = System.IO.File.ReadAllBytes(template);
templateGallery.RootFolder.Files.Add(fileInfo.Name, stp);
}
}
}
This method first checks the template gallery if a template with the same filename exists. If it exists, it is deleted first. Then the STP is uploaded to the rootfolder of the gallery. This turned out very handy, because just after I installed this to our production environment and uploaded the STP to all MySites, there was a change in the schema of the list….. It was very easy to redeploy the new list template:
- Deactivate the feature on all MySites using my custom STSADM command (see step 4)
- Copy the new STP file to the ListTemplates folder in the feature folder
- Activate the feature on all existing MySites
The class you create should inherit from Microsoft.SharePoint.SPFeatureReceiver. You will have to compile this into an assembly and install that to your SharePoint server(s).
Step 2 – Create the feature XML
The next step is to create the XML for the feature and install the feature using STSADM. The xml for my feature looks like this:
<?xml version="1.0" encoding="utf-8" ?>
<Feature Id="3DB2BE7A-E5DD-400d-B853-EB1F59E57E85"
Title="Template deployment for list templates"
Description="This feature uploads list templates to the Template Gallery of a site collection."
Version="1.0.0.0"
Scope="Site"
ReceiverAssembly="TST.FeatureReceivers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=503edd7b21a430b3"
ReceiverClass="TST.FeatureReceivers.TemplateDeployment"
xmlns="http://schemas.microsoft.com/sharepoint/">
</Feature>
This xml needs to be saved in a file called “feature.xml” that is copied into a subfolder of the TEMPLATE\FEATURES folder in the 12 hive. The feature can be installed using STSADM.
Step 3 – Create a Stapler for the feature
After installing the feature we need to activate it. Normally you create a new site definition and activate the feature from the site definition (in onet.xml). For the MySite this is difficult, because it is not a very good idea to modify the existing SharePoint site definitions. SharePoint always uses the SPSPERS site definition and that cannot be changed. This is where feature stapling can help. Read this blog by Chris O’Brian if you want to learn more about stapling, or read this item by Steve Peschka. You simply create a new feature folder with a feature.xml file like below.
<?xml version="1.0" encoding="utf-8" ?>
<Feature Id="ED23C370-9FC1-4d76-8495-F3BAEFE931CA"
Title="Stapler for TemplateDeployment feature"
Scope="Farm"
xmlns="http://schemas.microsoft.com/sharepoint/">
<ElementManifests>
<ElementManifest Location="elements.xml"/>
</ElementManifests>
</Feature>
This is a feature that is “Farm” scoped and adds our TemplateDeployment feature to the site collection features to be activated with the SPSPERS site definition. In the elements.xml that is referenced in this feature file, you add this xml. Please note that the Guid in the Id attribute of the FeatureSiteTemplateAssociation element is the same as the Id of the feature (see xml in step 2).
<?xml version="1.0" encoding="utf-8" ?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
<FeatureSiteTemplateAssociation
Id="3DB2BE7A-E5DD-400d-B853-EB1F59E57E85"
TemplateName="SPSPERS#0" />
</Elements>
Then you install the feature using STSADM.
Step 4 – Create a STSADM command
The next step is to create the STSADM command that activate/deactivates site scoped features. To do this, you create a new class that inherits from ISPStsadmCommand. This base class is available in the Microsoft.SharePoint.StsAdmin namespace. In your class you will need to implement GetHelpMessage and a Run method. In the first method, you return the help text for users that ask for help on the command prompt. The Run method has 3 parameters:
- command – the command that the STSADM user is using (that is entered after “-o”).
- keyValues – a StringCollection containing the options that the user has entered
- output – an output variable that you can set to give feedback to the user about the result of your command.
An example of my STSADM command:
stsadm" -o activatesitefeature -featurefolder MySiteListTemplates -template SPSPERS
In this case “activatesitefeature” is the command in the Run method and “featurefolder” and “template” are the settings that I need. They are part of the keyValues. Below you will find my implementation of the Run command. Just below the code you will find some comments on this piece of code.
public int Run(string command, System.Collections.Specialized.StringDictionary keyValues, out string output)
{ if (command != "activatesitefeature" && command != "activatesitefeature")
{ throw new ArgumentException("Invalid command"); }
string folder = keyValues["featurefolder"];
if (string.IsNullOrEmpty(folder))
throw new ArgumentException("Parameter 'featurefolder' must have a value.");
string template = keyValues["template"];
if (string.IsNullOrEmpty(template))
throw new ArgumentException("Parameter 'template' must have a value.");
SPFeatureDefinition activateFeature = null;
int errors = 0;
SPSecurity.RunWithElevatedPrivileges(delegate()
{ foreach (SPFeatureDefinition feature in SPFarm.Local.FeatureDefinitions)
{ if (feature.RootDirectory.ToLower().EndsWith(folder.ToLower()))
{ activateFeature = feature;
break;
}
}
if (activateFeature == null)
{ throw new ArgumentException(string.Format(
"Feature in folder {0} not found. Please install the feature first.", folder));
}
if (activateFeature.Scope != SPFeatureScope.Site)
{ throw new ArgumentException(string.Format(
"The scope for feature in folder {0} is not correct; should be Site.", folder));
}
SPWebService service = SPFarm.Local.Services.GetValue<SPWebService>(""); foreach (SPWebApplication webApplication in service.WebApplications)
{ for (int i = 0; i < webApplication.Sites.Count; i++)
{ using (SPSite site = webApplication.Sites
)
{ using (SPWeb web = site.RootWeb)
{ if (string.Compare(web.WebTemplate, template, true) == 0)
{ bool activate = true;
Guid remove = Guid.Empty;
foreach (SPFeature checkFeature in site.Features)
{ if (checkFeature.DefinitionId == activateFeature.Id)
{ switch (command.ToLower())
{ case "activatesitefeature":
activate = false;
break;
case "deactivatesitefeature":
remove = checkFeature.DefinitionId;
break;
}
}
}
switch (command.ToLower())
{ case "activatesitefeature":
if (activate)
{ try
{ site.Features.Add(activateFeature.Id);
}
catch (Exception ex)
{ errors++;
LogMessage(string.Format("Error: {0}", ex.Message)); }
}
break;
case "deactivatesitefeature":
if (remove != Guid.Empty)
{ try
{ site.Features.Remove(remove, true);
}
catch (Exception ex)
{ errors++;
LogMessage(string.Format("Error: {0}", ex.Message)); }
}
break;
}
}
}
}
}
}
});
output = string.Format("Deactivation completed with {0} errors.", errors); return 0;
}
First all input parameters (command and options) are validated. After that you will see that I used SPSecurity.RunWithElevatedPrivileges, this is because the STSADM command will run in the context of the user who is logged on to the SharePoint server to run the command. This user typically is a farm administrator, but this person probably does not have the permissions to activate a feature on a MySite. By running the code with elevated privileges, the code runs with Full Control. The next step is to search for the featuredefinition that the user wants to activate. If this feature cannot be found, or the scope of the feature is wrong, an error message is raised. In the next step I start to iterate through all web applications on the SPWebService service. For each SPSite in all web applications, I check if the WebTemplate of the RootWeb is equal to the value of the template parameter passed by the user. If these are equeal, the site feature gets activated (if it is not yet activated) or deactivated, depending on the command. Activating a feature is done by using the Add method of the Features collection of the site and passing the guid of the featuredefinition.
Step 5 – Register the STSADM command
After compiling and deploying the assembly, the STSADM command needs to be registered. This can be done by creating a xml file with the xml below. The file should be named “stsadmcommands[myname].xml” where [myname] is your unique name. In this file you register the commands and link them to the assembly:
<?xml version="1.0" encoding="utf-8" ?>
<commands>
<command
name="activatesitefeature"
class="TST.STSADM.FeatureActivation,
TST.STSADM, Version=1.0.0.0, Culture=Neutral,
PublicKeyToken=503edd7b21a430b3"
/>
&