Friday, March 28, 2008

Walkthrough: Creating a SharePoint Feature Receiver and Custom Link with WSS Extensions

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:

  1. Copy a local file into the WSS instance.
  2. Add a link to the Site Actions menu.
  3. 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.Anatomy of SharePoint Solution

Walkthrough

  1. After the extensions have been installed, open up VS and create a New Project. Choose an Empty project, name it and click OK.Create new empty project with Visual Studio WSS Extensions
  2. 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. Renaming Module.xml to elements.xml
  3. 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:Anatomy of WSP
  4. See the sample.txt? Right now that will get deployed to the root of our site as dictated by the elements.xml.
  5. 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.
  6. Now the feature receiver, in the Solution Explorer add a new class called FeatureReceiver.cs under the InstallContent folder.Adding a Feature Receiver
  7. 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!Setting the deployment URL for the WSS Extensions

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.InstallContent demo feature deployed and activated. Changed site title and link in Site Actions menu. Modification to web.config done through SPWebConfigModification 

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

19 comments:

Anonymous said...

Everyting thing works fine until I deactivate the feature, uninstall the solution, change something ilke the default Title for the FeatureDeactivating event, rebuild, hit F5. That's when I get error: The language-neutral solution package was not found.

Any ideas on how to fix this issue?

Tyler Holmes said...

Surprisingly enough the only fix I've found for the "The language-neutral solution package was not found." error was to close Visual Studio and then open it again.

..I know it's a terrible workaround but it's the only one I've ran into so far that worked.

HTH,
Tyler

Kimoz said...

I created a project. Added a couple of features... Everything worked fine.

Then I closed the solution i Visual Studio. Moved the project to a new location in TFS. Opened the project again. Now WSP View shows Error loading Solution.xml....

Any Ideas? Seems pretty fragile this stuff...

Tyler Holmes said...

While I definitely agree with you that it's fragile there's 2 things going for you.

1) Version 1.2 just got released last week which I hope will prove to be a little more robust.

2) When the generated solution or solution structure starts to misbehave you can simply regenerate it by deleting the old and revisiting the WSP view (it'll regen out the whole solution/feature/element structure).

Hope some of that helps.

My Best,
Tyler

Anonymous said...

How do you "configure" a feature receiver in VSeWSS 1.2?

the feature.xml is regenerated, and the ReceiverAssembly etc are lost if you enter them manually.

Dhams said...

hi,
I am having trouble to design the spwebconfig format for membership provider key.

Can any one please guide me how to add the membership provider key ?

Thanks

Anonymous said...

Deactivating, undeploying and uninstalling the feature does not remove the sample.txt.
I have expected another behaviour...

Any ideas?
Thank you

Anonymous said...

Try this,

Custom sharepoint Features

http://sarangasl.blogspot.com/2009/09/in-this-article-im-going-to-describe.html

datasmithadvtechs said...

Hey mate,

Is it easy for you to update the solution for VS 2008 & VSE 1.3 ?

datasmithadvtechs said...

can you please update for vs2008?

Anonymous said...

I am trying to use your example to add a web.config connection string in a VseWss 1.3 (Mar CTP)site definition project for a MOSS 2007 site. I placed the feature receiver class under the site provisioning handler and set the Receiver assembly/class attributes in the corresponding feature.xml. Site def deploys and works fine, except the web.config is not modified. Do I need to something in the site provisioning OnActivated method which appears to override the feature events?

Thanx
FStumpp

Stephen said...

Great article. The code for adding the connectionStrings section if it didn't exist was the piece I was missing. Your code is flexible and well documented. Great job!

Anonymous said...

Между прочим, лучший способ обезопасить кого-нибудь от слежки - купить Подавитель связи

Ryan said...

Nice article.

I've put together a blog post with some of the more esoteric details of how Feature Receivers work. Thinks like what triggers each event and which account it will run under - not quite as obvious as it first sounds.

http://blog.pentalogic.net/2010/06/sharepoint-feature-receivers-events-details/

Anonymous said...

Hi all.
I'm not sure if anyone else is running into this but when I try deploying the solution from Visual Studio it throws this error on Activate Feature event:

Error occurred in deployment step 'Activate Features': 'connectionString' is an unexpected token. Expecting white space. Line 1, position 322.

I checked my connection string and everything seems ok there.
Thoughts anyone?

Thanks in advance.

TartanBono said...

Hi, I'm trying to write a custom email eventhandler but was having trouble, no errors, but nothing being added to my list (calendar) so I thought I'd try your sample but I'm recieving the following error when I try to run the sample:

Object reference not set to an instance of an object.

...but there is no filename/line/column numbers (or even project).

I am using VS2008 & VseWSS 1.3, not sure if that is the reason as I'm new to SharePoint development.

Any Help would be appreciated..

Thanks,
TartanBono

Lucian Conley said...

When developing custom business solutions for WSS and MOSS, the first thing you need to do is gain a firm understanding of features.

Syahri Ramadhan said...

I learnt a lot from your code, big salutation from me for sharing this knowledge

digital signature software said...

Very helpful article ! I was always curious about all these complex algorithms that are being used in these ssl encryptions.