Why You Should Start Putting Everything in Features
If you're just getting into .wsp development with SharePoint these days you're in luck, you've managed to skip a couple years of pain when it comes to getting retractable functionality into SharePoint. With the WSS Extensions for Visual Studio a lot this stuff has never been easier.
It's relatively "easy" to get content INTO SharePoint, it seems like anyone with a copy of SharePoint Designer can embed files, lists, .aspx's in a WSS instance.
The REAL problem is getting your stuff out cleanly, and to allow yourself a managed upgrade when it comes time to maintain this content. Hopefully some plan that DOESN'T rely on you moving files around in SPD or visiting a series of web.config's and bin folders on all the Web Front End machines in the farm. This is the beauty of features and solutions and this walkthrough will show you how to create a simple feature that does the following:
- Copy a local file into the WSS instance.
- Add a link to the Site Actions menu.
- Modify the web.config programmatically on all machines in the farm.
Before You Start
If you get lost you can download a copy of a working solution here. Before you can open it though you'll need a copy of Visual Studio .NET 2005 and a copy of WSS Extensions for VS 2005 v1.1. Unfortunately there isn't support for VS 2008 yet word on the street has it that it'll be out in June of this year (2008).
What You Should Know about Solutions
Solutions (.wsp) are .cab files. You can take any .wsp rename it to a cab and open it up in WinRar or unpack it with WinZip (useful for debugging). Solutions pretty much all look the same. They all:
- Have a manifest.xml in the root. This lists all the Features, Web Parts and assemblies in the solution. The schema of valid tags for a solution can be found here.
- Underneath the root of the cab is usually a directory for each feature that is in the solution.
- Inside each feature folder is usually a file named feature.xml which describes a the title, description, a feature receiver if one exists, the scope of the feature (Web, Site, Web Application or Farm), the location of dependent files, and the a series of element manifests (elements.xml) which usually sit in their own folder underneath the feature folder.
- Element manifests (elements.xml) describes pretty much all the functionality that this feature will have. It's in these elements.xml that we can specify custom links, new list instances, content types, workflows etc... What can be described in an elements.xml (which is quite a bit) is listed here.
- Below is a sample solution which contains a Solution (FeatureDemo) which has a manifest.xml, one feature that has one elements.xml. The sample.txt is an example of a dependent file (like a .aspx) that we plan on deploying as part of this feature.
Walkthrough
- After the extensions have been installed, open up VS and create a New Project. Choose an Empty project, name it and click OK.
- Right click on your new project->Add->New Item, choose a Module and name it Install Content. Rename the Module.xml file that gets created to elements.xml (this isn't a requirement, but it's more in line with the typical anatomy of a solution). It's of note that whenever we add items to our project this way (using Right Click->Add) we're really setting up a new feature. Every folder that gets added by you adding WSS items to the project will for the most part end up equating to a Feature in our .wsp.
- The WSS Extensions v1.1 add a new window that you can view WSP anatomy with called the WSP View. You can get to it via View->Other Windows->WSP View. When you do this you can see the anatomy of your solution. It should now look something like:
- See the sample.txt? Right now that will get deployed to the root of our site as dictated by the elements.xml.
- Now we're going to add a link to Google.com on the site actions menu. In the Solution Explorer, open up elements.xml and add the following xml just below the <Elements> tag.
<CustomAction
GroupId="SiteActions"
Location="Microsoft.SharePoint.StandardMenu"
Sequence="1000"
Title="Google Search"
ImageUrl="/_layouts/images/ActionsSettings.gif"
Description="A public search engine."
>
<UrlAction Url="
http://google.com"/>
</CustomAction>
If you want to dig more into what you can do with custom link actions there's more details on custom action attributes here. Essentially you can add custom links almost anywhere in SharePoint, from Lists, to Items to Context Menus...it's quite extensive. - Now the feature receiver, in the Solution Explorer add a new class called FeatureReceiver.cs under the InstallContent folder.
- Add a reference to Windows SharePoint Services (or Microsoft.SharePoint.dll) and then copy/paste in the following code:
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
namespace FeatureDemo
{
public class FeatureReceiver : SPFeatureReceiver
{
//All the modifications we will be doing to the Web.Config when we activate/deactivate this feature.
private ModificationEntry[] entries =
{
//Ensure there's a connectionStrings section.
new ModificationEntry(
"connectionStrings"
,"configuration"
,"<connectionStrings/>"
,SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode
,true)
//Create the connectionstring.
,new ModificationEntry(
"add[@name='ConnectionString'][@connectionString='Data Source=serverName;Initial Catalog=DBName;User Id=UserId;Password=Pass']"
,"configuration/connectionStrings"
,"<add name='ConnectionString' connectionString='Data Source=serverName;Initial Catalog=DBName;User Id=UserId;Password=Pass'/>"
,SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode
,false)
};
public override void FeatureActivated(SPFeatureReceiverProperties properties)
{
//Get a reference to the web application and then remove entries for the blorum.
SPSite site = properties.Feature.Parent as SPSite;
SPWebApplication webApplication = site.WebApplication;
site.RootWeb.Title = "Set from activating code at " + DateTime.Now.ToString();
site.RootWeb.Update();
foreach (ModificationEntry entry in entries)
webApplication.WebConfigModifications.Add(CreateModification(entry));
webApplication.WebService.ApplyWebConfigModifications();
}
public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
{
//Get a reference to the web application and then remove entries for the blorum.
SPSite site = properties.Feature.Parent as SPSite;
SPWebApplication webApplication = site.WebApplication;
site.RootWeb.Title = "Set from deactivating code at " + DateTime.Now.ToString();
site.RootWeb.Update();
foreach (ModificationEntry entry in entries)
{
//Some entries we create but do not remove
if (!entry.CreateOnly)
webApplication.WebConfigModifications.Remove(CreateModification(entry));
}
webApplication.WebService.ApplyWebConfigModifications();
}
/// <summary>
/// Event that fires after this feature is installed.
/// </summary>
public override void FeatureInstalled(SPFeatureReceiverProperties properties)
{
}
/// <summary>
/// Event taht fires before this feature is installing.
/// </summary>
public override void FeatureUninstalling(SPFeatureReceiverProperties properties)
{
}
/// <summary>
/// Creates a SPWebModificaion object based on our parameters.
/// </summary>
private SPWebConfigModification CreateModification(ModificationEntry entry)
{
SPWebConfigModification modification = new SPWebConfigModification(entry.Name, entry.XPath);
modification.Owner = "OurApplicationName";
modification.Sequence = 0;
modification.Type = entry.ModificationType;
modification.Value = entry.Value;
return modification;
}
/// <summary>
/// Container to hold info about our modifications to the web.config.
/// </summary>
private struct ModificationEntry
{
public string Name; //Name of the node
public string XPath;//Where the node is located
public string Value;//The value of the attribute/node
public SPWebConfigModification.SPWebConfigModificationType ModificationType;
public bool CreateOnly;//Whether we should create and not remove the node when deactivating the feature.
public ModificationEntry(string name, string xPath, string value,
SPWebConfigModification.SPWebConfigModificationType modificationType, bool createOnly)
{
Name = name;
XPath = xPath;
Value = value;
ModificationType = modificationType;
CreateOnly = createOnly;
}
}
}
}
It's worth mentioning that this is an adaptation of a post by Ted Pattison, a SharePoint consultant and training. Lets talk about this code for a second. We essentially create a list of modifications that we're going to do to the web.config. We do these on FeatureActivated and clean up after ourselves on FeatureDeactivating. This forms the basis of cleaner SharePoint development. For demo purposes we also rename the site title to the current time stamp so that we know the code ran.
8. Now open up the feature.xml in the WSP View and change the Scope to "Site" and register our event receiver. Depending on what you named your project your feature.xml file should should look something like this:
<?xml version="1.0" encoding="utf-8"?>
<Feature
Id="beb48f0a-9c42-40b9-a4b5-9d01d23106a8"
Title="InstallContent"
Scope="Site" Version="1.0.0.0"
Hidden="FALSE"
DefaultResourceFile="core"
xmlns="
http://schemas.microsoft.com/sharepoint/" ReceiverAssembly="FeatureDemo, Version=1.0.0.0, Culture=neutral, PublicKeyToken=9f4da00116c38ec5"
ReceiverClass="FeatureDemo.FeatureReceiver"
>
<ElementManifests>
<ElementManifest Location="InstallContent\elements.xml" />
<ElementFile Location="InstallContent\sample.txt" />
</ElementManifests>
</Feature>
9. We're almost there! Finally open up your project properties and under the Debug section set the URL to the SharePoint site you want to deploy to. Now press F5 and watch it go!
10. That's it. If you now visit the site you'll see that our feature was activated under Site Actions->Site Settings->Site Collection Features. You can deactivate if you want and all our modifications through this feature will be gracefully removed. You can see our link to Google in the Site Actions Menu, our changed website title telling us that the activation code ran, our sample.txt document has been deployed to the site root, AND if you dig up the web.config you'll see our added <ConnectionStrings/> section and a connection string that we set. These changes will also disappear if you deactivate the feature.
There's also now a FeatureDemo.wsp file in the \bin\Debug\ folder. We could take this and deploy it to another farm using the following commands.
stsadm -o addsolution -filename FeatureDemo.wsp
stsadm -o deploysolution -name FeatureDemo.wsp -immediate -allowgacdeployment -url http://w2k3-tyler-virt:83">http://w2k3-tyler-virt:83
stsadm -o activatefeature -id {featureGuid} -url http://w2k3-tyler-virt:83
In doing so we've now deployed content that is easily retractable AND upgradable. I dream of a day where all SharePoint content is delivered with this level of thoughtfulness!
There's a good chance that some of those steps were confusing, and because of that you can download the source project here.
There's a lot available when it comes to SharePoint development these days. I think the biggest paradigm shift most ASP.NET developers are going through is realizing that we are now participating in a large shared platform with SharePoint. We are no longer writing stand alone apps where the ASP.NET developer is boss. With WSS/MOSS development a lot more consideration to Farm Administrators and other WSS Developers is in order when rolling out your content.
Take Care,
Tyler