2. Feature Receivers
A feature receiver
is basically an event handler responsible for handling installation and
activation events for a feature. We can add feature receivers to any
type of feature by right-clicking a Feature node in the Solution
Explorer pane and selecting Add Event Receiver. When we perform this
action, Visual Studio adds a new code file to our project and sets the
Receiver Assembly and Receiver Class properties of the feature to
reference the new code file. Let’s do this now:
Right-click the Features node and select Add Feature. A new feature named Example 19 Feature 2 will be added to the project.
Right-click the Feature2 node in the Features folder and select Add Event Receiver.
Double-click
Feature2.feature in the Feature 2 folder. In the Properties pane, the
Receiver Assembly and Receiver Class have been automatically set.
In the
Feature2.EventReceiver.cs file are five commented methods representing
the events that can be handled by the feature receiver. By uncommenting
these methods, we can add custom code to do whatever we need to do. As
you embark on more complex SharePoint projects, you’ll find that
although you can perform much configuration using the SharePoint Project
Items available in Visual Studio, a lot of configuration still needs to
be done programmatically. In these situations, the feature receiver is
the tool of choice. Bearing that in mind, let’s look at how feature
receivers work and how we can best make use of them.
Add the following code to Feature2.EventReceiver.cs:
public override void FeatureActivated(SPFeatureReceiverProperties properties)
{
if (properties.Feature.Parent is SPWeb)
{
SPWeb web = properties.Feature.Parent as SPWeb;
Guid listId=web.Lists.Add("My New List",
"This is a demonstration list",
SPListTemplateType.Contacts);
SPList newList = web.Lists[listId];
newList.OnQuickLaunch = true;
newList.Update();
}
}
public override void FeatureDeactivating(
SPFeatureReceiverProperties properties)
{
if (properties.Feature.Parent is SPWeb)
{
SPWeb web = properties.Feature.Parent as SPWeb;
SPList myList = web.Lists.TryGetList("My New List");
if (myList != null)
{
myList.Delete();
}
}
}
Deploy the solution by selecting Deploy Example 19 from the Build menu.
If all is well, we’ll find that our blank demo site now contains two new lists: one named Example
- ListInstance1, which has been created by the ElementManifest in
Feature1, and another named My New List, which has been created
programmatically by our feature receiver in Feature 2.
Notice in this code snippet that we’re using the
properties.Feature.Parent property to obtain a reference to the SPWeb
object on which our feature is being installed. Some investigation of
the Parent property will reveal that it’s of type object, and for that
reason we’re
checking its type before casting it to a variable of the correct type.
To understand why this is the case, you can take a look at how features
are defined within the server object model, as shown here:
Features can have four possible scopes. When a
feature of a particular scope is installed, it’s added to the Features
collection of the appropriate object. For example, a site
collection–scoped feature would be added to the Features collection of
the appropriate SPSite object. The SPFeature object that is returned by
properties.Feature can therefore have one of four possible parents,
depending on the scope of the feature.
To confirm that our receiver is working as expected, we can take the following steps:
From the Site Actions menu, select Site Settings, and then select Manage Site Features from the Site Actions section.
Both Example 19 Feature 1 and Example 19 Feature 2 are active. Deactivate Example 19 Feature 2. Notice that My New List is removed from the site. This confirms that our feature receiver is working as expected.
Debugging Feature Receivers
Feature receivers can be difficult to debug because
they are often executed within a separate process. To see an example of
this problem, put a breakpoint on the first line of our FeatureActivated
method and try debugging using Visual Studio. The code will be deployed
and the feature will be activated, but execution will not stop at the
breakpoint. Visual Studio makes use of a separate process,
VSSPHost4.exe, to automate the deployment process. The Visual Studio
debugger, however, is set up to attach to a W3SVC.exe process only, and
therefore the breakpoint is never hit but the code still executes.
We can work around this issue in one of two ways: we
can either attach a debugger to the appropriate process, or we can
ensure that our feature receiver runs in the W3SVC process. To ensure
that a debugger is attached to the correct process, we can take the
following steps:
Add the following line of code to the method to be debugged:
Start the debugging process as normal. An error dialog will be displayed:
Click
Debug The Program, and then in the Visual Studio Just-In-Time Debugger
select the appropriate instance of Visual Studio. Click Yes to begin
debugging.
This technique will work regardless of the host
process. For example, if PowerShell is used to install a package, the
same error dialog will be displayed.
Our second option is to ensure that the feature
receiver code runs in the W3SVC process. This is relatively easy to do.
Earlier when we looked at feature properties, we saw that the Activate
On Default value is used to determine whether a feature should be
automatically installed. We can use this setting as follows:
Remove the line of code that we added in the preceding example.
Double-click the Feature 2 node and set the Activate On Default property to False.
Debug the solution as normal. This time, when the solution is deployed, our feature will not be automatically activated.
When
the web site being debugged is shown in the browser, select Site
Settings from the Site Actions menu, and then select Manage Site
Features from the Site Actions section. Manually activate the feature
being debugged. The debugger will now stop on the breakpoints.
This method works because when features are activated
via the user interface, the feature receiver runs under the W3SVC
process, and Visual Studio has attached a debugger to this process as
part of the standard debugging mechanism.
Passing Parameters to Feature Receivers
You’ve seen how to create feature receivers and how
to pick up references to the object that you need in order to access the
server object model. We’ve looked at a few ways to enable debugging.
Let’s move on to look at more complex feature receivers.
As mentioned, practically every real-world SharePoint
project will require some custom feature receivers. This is especially
true when code being developed must be shared among multiple developers
or deployed to testing or staging environments. As a result, it is
sensible to create a library of feature receivers that perform specific
configuration tasks. For example, I have a collection of feature
receivers that perform actions such as configuring security for a site
or setting up search scopes. These are actions that are common to many
SharePoint projects but that can’t be performed declaratively.
One essential aspect of creating reusable feature
receivers is the ability to pass configuration into the receiver. Let’s
look at a few ways to solve this problem.
The first method is appropriate if a collection of name/value pairs is sufficient for our purposes.
Open the Feature Designer for Feature 2.
Add the FirstElement element that we created earlier to the feature, as shown:
In
the Solution Explorer pane, select the First Element node. Then in the
Properties pane, click the ellipsis next to Feature Properties.
Add two new properties, ListName and ListDescription. Set the values to Another New List and This is Another list, respectively.
Click OK to close the dialog.
Although
every element in a feature has a Feature Properties property, in
reality the properties are applied at the feature manifest level—that
is, the combination of all the properties that are added to each element
in Visual Studio are actually written within a single Properties
element in the feature manifest.
Update the code in Feature2.EventReceiver.cs as follows:
public override void FeatureActivated(SPFeatureReceiverProperties properties)
{
if (properties.Feature.Parent is SPWeb)
{
SPWeb web = properties.Feature.Parent as SPWeb;
string listName = properties.Definition.Properties["ListName"].Value;
string listDescription = properties.Definition.Properties["ListDescription"].Value;
Guid listId = web.Lists.Add(listName,
listDescription,
SPListTemplateType.Contacts);
SPList newList = web.Lists[listId];
newList.OnQuickLaunch = true;
newList.Update();
}
}
public override void FeatureDeactivating(
SPFeatureReceiverProperties properties)
{
if (properties.Feature.Parent is SPWeb)
{
SPWeb web = properties.Feature.Parent as SPWeb;
string listName = properties.Definition.Properties["ListName"].Value;
SPList myList = web.Lists.TryGetList(listName);
if (myList != null)
{
myList.Delete();
}
}
}
You can see that we’re able to address the properties
via the properties.Definition object. The Definition object is of type
SPFeatureDefinition and is an object representation of the various XML
elements that make up the feature.
The next method for solving the problem is
appropriate if more complex configuration is required. For example, when
configuring security settings for a site using a feature receiver, I
use this approach to load an XML file containing the security
configuration (see http://spsecurity.codeplex.com/ for more details).
Add an XML file named MyConfig.xml to the FirstElement folder. Add the following code:
<Lists>
<List name="1st List" description="1st list description"
type="Contacts"/>
<List name="2nd List" description="2nd list description"
type="Announcements"/>
<List name="3rd List" description="3rd list description"
type="Events"/>
</Lists>
To
specify that the MyConfig.xml should be included as an element file,
select the MyConfig.xml node in the Solution Explorer. Then, in the
Properties pane, change the Deployment Type to ElementFile, as shown
here:
Update the code in Feature2.EventReceiver.cs as follows:
public override void FeatureActivated(
SPFeatureReceiverProperties properties)
{
if (properties.Feature.Parent is SPWeb)
{
SPWeb web = properties.Feature.Parent as SPWeb;
using (Stream s = properties.Definition.GetFile(
"FirstElement\\MyConfig.xml"))
{
using (XmlReader rdr = XmlReader.Create(s))
{
rdr.ReadToDescendant("List");
do
{
string listName = rdr.GetAttribute("name").ToString();
string listDescription = rdr.GetAttribute(
"description").ToString();
string listType = rdr.GetAttribute("type").ToString();
SPListTemplateType typeEnum = (SPListTemplateType)Enum.Parse(
typeof(SPListTemplateType), listType);
Guid listId = web.Lists.Add(listName,
listDescription, typeEnum);
SPList newList = web.Lists[listId];
newList.OnQuickLaunch = true;
newList.Update();
} while (rdr.ReadToNextSibling("List"));
}
}
}
}
public override void FeatureDeactivating(
SPFeatureReceiverProperties properties)
{
if (properties.Feature.Parent is SPWeb)
{
SPWeb web = properties.Feature.Parent as SPWeb;
using (Stream s = properties.Definition.GetFile(
"FirstElement\\MyConfig.xml"))
{
using (XmlReader rdr = XmlReader.Create(s))
{
rdr.ReadToDescendant("List");
do
{
string listName = rdr.GetAttribute("name").ToString();
SPList myList = web.Lists.TryGetList(listName);
if (myList != null)
{
myList.Delete();
}
} while (rdr.ReadToNextSibling("List"));
}
}
}
}
When
we deploy the solution and activate the feature, three new lists will
be added to the site as specified in the MyConfig.xml file. In this
example, we’ve used an XmlReader to parse the configuration file for the
sake of keeping the example simple. In a real-world solution, using an
XmlSerializer to deserialize the configuration file into an appropriate
collection of objects would be more robust.