Showing posts with label feature receiver. Show all posts
Showing posts with label feature receiver. Show all posts

Sunday, 12 August 2007

Site definitions - custom code in the site creation process

This is the second article in a series of three, where I aim to show how to customize the site creation process (known as site provisioning) with your own API code. The full introduction and series contents can be found at http://sharepointnutsandbolts.blogspot.com/2007/07/article-series-custom-permissions-with.html. The example customization I'm using is as follows: any sites created with the definition should use a specific set of permissions, and not simply follow the default behavior of inheriting the parent site's permissions. Since this can't be done with a standard site definition (like many other things you might want to do), use of the API is required.

However, today the focus is less on the permission specifics of my example, and more on how generally to add your own code which runs in the site provisioning process. And the best thing is, it's actually very simple if you understand SharePoint Features.

There are many reasons why you might have cause to use the API in the site provisioning process. Essentially, if you can't find a way to do what you want using CAML schema in the onet.xml file, chances are you'll have to write code. Hence, it's almost easier to think of what you can do in the onet.xml file and reverse the list in order to work out scenarios which require code, but some examples which spring to mind anyhow are:

  • changing the custom master page of a site
  • creating a site column which gets it's data from a list (see my post on my Feature receiver which does this at Feature to create lookup fields on Codeplex)
  • adding custom unique permissions to a site (the example in this article series)
  • set a site property from any kind of dynamic lookup

In short, there are many scenarios.


Creating site definitions with VSeWSS

If you've ever created a site definition with Visual Studio Extensions for Windows SharePoint Services, you'll notice that the VS project it gives you contains a file called SiteProvisioning.cs. Inside is an event-handler method, where you can add your custom code which will execute when a site is created from the definition. The class looks like this:

namespace COB.Demos.SiteDefinition

{

    public partial class ProjectXSiteDefinition

    {

        /// <summary>

        ///  Define your own feature activation action code here

        /// </summary>

        public void OnActivated(SPFeatureReceiverProperties properties)

        {

            // my code here..

        }

    }

}

 

The plumbing behind all this is interesting. At first glance, the method signature looks like a Feature receiver, but it's actually not. However, examining the VS project (you'll need to build the project with F5 at least once to generate the files) reveals that VSeWSS has in fact created some Features in the background. These files can be found under the bin\Debug\solution folder in your VS project (hidden by default - you'll need to do a 'Show All Files' in Visual Studio Solution Explorer). If you do some more delving around to see exactly what VSeWSS is doing, you'll find the following:

  • 2 hidden Features have been created - 1 deploys the 'default.aspx' file, the other has no 'elements' file but is hooked up to a Feature receiver - this is a class in an assembly named the same as your VS project. If you check the GAC, you will indeed find this assembly there.
  • a line similar to the following has been added to the onet.xml file under the 'WebFeatures' element:

    <Feature ID="67b2507c-8822-41dc-b939-3d8f34b5ad13" />


    Notably, this is the ID of the Feature which is hooked up to the Feature receiver.
  • Using Reflector on the assembly containing the Feature receiver shows that the main event-handler method performs some processing and then calls into the OnActivated method shown above, i.e. the place where VSeWSS provides for you to add your own code to execute when sites are created. This code is actually contained in the SiteProvisioning.Internal.cs file within the VS project. (If you're curious as to what on earth all the code in here is doing, the answer as far as I can tell is nothing when site definitions are created with the VSeWSS project template. However, this code is also found when Solution Generator is used to extract a site definition - in that case there are some fixups which need to be done, and this is the code which is used.)

So in summary, VSeWSS creates a hidden Feature is added to the 'WebFeatures' section of the onet.xml so that it is automatically activated when the definition is used to create a web*. The Feature is hooked up to a Feature receiver which calls the OnActivated method where your custom code lives.

*(Note that if the definition is used to create a site definition, the root web is also created automatically so the Feature would also be activated then. Also note the feature needs to be already installed in the farm for it to be activated in this way).

What we can derive from this is that there's no 'special place' in the site provisioning process to inject custom code, but it can be accomplished by use of a Feature receiver. So if you don't want to use VSeWSS to create site definitions, this is the technique to use to add your custom code to the site creation process.

In terms of what that code might look like, a 'Hello World' example could be:

public void OnActivated(SPFeatureReceiverProperties properties)

{

     SPWeb currentWeb = null;

     SPSite currentSite = null;

     object oParent = properties.Feature.Parent;

 

     if (properties.Feature.Parent is SPWeb)

     {

         currentWeb = (SPWeb)oParent;

         currentSite = currentWeb.Site;

     }

     else

     {

         currentSite = (SPSite)oParent;

         currentWeb = currentSite.RootWeb;

     }

 

     currentWeb.Title = "Set from provisioning code at " +  DateTime.Now.ToString();

     currentWeb.Update();

}


Hopefully this illustrates that it's quite simple to write code which sets properties on sites created from the definition. Generally the SPWeb object is the entry point, and any property which can be modified can be modified using the API. So, this is a pretty powerful technique which can be used in many scenarios.

If you have this type of requirement, I'd definitely recommend using VSeWSS to simplify the process. It's certainly possible to hook everything up manually and package it into a Solution, but the tool does save a large amount of hassle. However as usual with VSeWSS, the price of this is some flexibility. As my sample code in the final article will show, it's sometimes useful to pass data into Features by using Feature properties, and this unfortunately is not supported by VSeWSS. So in case it's useful, the following link provides a zip file containing a Solution/Feature which uses the above technique, without using VSeWSS:

http://sharepointchris.googlepages.com/customcodewithsitedefinitions

In the next and final article, I'll cover the specifics of using the API to modify site permissions as sites are created. As is hopefully clear, this is in conjunction with the technique detailed here so the net result is that the specific permissions are set 'automatically', courtesy of the Feature which is automatically activated against a site when it is created.

Friday, 20 April 2007

Feature to create lookup fields on Codeplex

In a recent post I posted some sample code for a feature receiver which would create lookup fields (site columns which get their data from lists). A couple of people left comments asking for full set of files.

I've put these on Codeplex at http://www.codeplex.com/SP2007LookupFields.

Some notes:-

  • I've enhanced the solution to be more generic and deal with creating multiple lookup fields in one feature. Now the name of the list can be included in the CAML and the feature receiver will parse this, find the list and fix the reference via the list GUID using the API. Note currently the list must be in the root web, though it would be trivial to extend this.
  • All the hardcoded values have been removed, e.g. the path to the file containing the CAML definition is now passed as a feature property.
  • I mentioned in the earlier post that you could have a dependent feature so that the assembly containing the feature receiver also gets deployed automatically on feature activation. This doesn't quite make sense since assemblies can only be deployed using SharePoint solutions not features. Hence I've wrapped the feature in a SharePoint solution which deploys the assembly and feature. When the feature is activated the assembly is already in the GAC and the feature receiver runs happily. Note it would also be fairly simple to enhance the solution such that the assembly gets deployed to a site bin with appropriate CAS policy for highly-controlled environments.
  • I've also included an STSADM script to take care of deploying the solution - you need to edit the URL in this file to point to your SharePoint site.

Hope this is useful, let me know if you have feedback..

Sunday, 15 April 2007

Sample code - creating lookup columns as a feature

I promised in an earlier post to post the code I used to create lookup columns as a feature. The basic idea here to use CAML as far as possible to define the site column, but use code to inject the list ID which the column refers to (unknown at feature design-time since SharePoint decides the GUID for lists). Extracted below is the code for the feature receiver.

A couple of key points:-

  • this assumes a file exists at the location D:\COB.Demo.ListBasedSiteColumns.Fields.xml with the following contents (note the 'List' attribute with an empty value):-
    <?xml version="1.0" encoding="utf-8"?>
    <elements xmlns="http://schemas.microsoft.com/sharepoint/">
    <!-- _filecategory="ContentType" _filetype="File" _filename="fields.xml" _uniqueid="c0188da6-e320-44c0-b50c-cb6eaecec512" -->
    <Field
    Type="Lookup"
    Displayname="Locations"
    Required="FALSE"
    List=""
    ShowField="LinkTitleNoMenu"
    UnlimitedLengthInDocumentlibrary="FALSE"
    Group="COBDemo"
    ID="{ae8e6ab8-b151-4fa4-8694-3cd24ad3b1bc}"
    SourceId="{8c066b26-5a3e-4e1b-85ec-7e584cf178d7}"
    StaticName="Locations"
    Name="Locations">
    </elements>

  • Since most of the column definition is in CAML, this allows you to specify the ID of the column. This is useful later on when we deploy content types or lists which need to reference this site column by ID.
  • Site columns cannot be deleted when they are in use i.e. used in content types or lists. I prefer to try to delete the column on feature deactivation or reactivation, but omit error-handling so SharePoint throws an exception. This makes it clear to me why the feature cannot be deactivated. See comments in code for more information.
  • Note that the assembly which contains the code below must be available (in the GAC, or site bin directory with appropriate CAS policy) when the feature is activated. You must also specify the FeatureAssembly and FeatureReceiver attributes in your feature.xml file to register the feature receiver.
  • In terms of deploying the assembly, note that whilst you certainly can deploy assemblies to the GAC or site bin using the feature framework, you can't do it in this feature. Despite the 'FeatureActivated' name of the event handler, when your code executes the CAML won't yet have been processed. Hence your assembly won't yet have been copied and you'll get a FileNotFoundException when it tries to load the assembly. A good solution is to use a feature dependency. Here you can create a 2nd feature which deploys the assembly, and set the main feature to be dependent on this one. Additionally you can mark the assembly deployment feature to be hidden thus making the implementation a bit tidier. If it's useful to see the entire set of files I used leave me a comment and I'll put them up somewhere.
Apologies for the formatting, I will try to do something about this when I get time:-


public class FeatureReceiver : SPFeatureReceiver
{
private readonly string f_csSITE_COLS_DEFINITION_PATH = @"D:\COB.Demo.ListBasedSiteColumns.Fields.xml";

public override void FeatureActivated(SPFeatureReceiverProperties properties)
{
// feature is scoped at Site, so the parent is type SPSite rather than SPWeb..
using (SPSite site = properties.Feature.Parent as SPSite)
{
SPWeb currentWeb = null;
Guid gRootWebId = Guid.Empty;
if (site != null)
{
currentWeb = site.RootWeb;
gRootWebId = currentWeb.ID;
}
else
{
currentWeb = properties.Feature.Parent as SPWeb;
gRootWebId = currentWeb.Site.RootWeb.ID;
}





using (currentWeb)
{
// get reference to the list..
SPList referencedList = currentWeb.Site.RootWeb.Lists["LocationsList"];
string sFieldElement = null;
XmlTextReader xReader = new XmlTextReader(f_csSITE_COLS_DEFINITION_PATH);
while (xReader.Read())
{
if (xReader.LocalName == "Field")
{
sFieldElement = xReader.ReadOuterXml();
break;
}
}


string sFinalCaml = replaceListGuidString(sFieldElement, referencedList);
createLookupColumn(currentWeb, sFinalCaml, "Locations");
currentWeb.Update();
}
}
}


private string replaceListGuidString(string sFieldElement, SPList referencedList)
{
string sPopulatedGuid = string.Format("List=\"{0}\"", referencedList.ID);
return sFieldElement.Replace("List=\"\"", sPopulatedGuid);
}

/// <summary>
/// Attempt to delete the column. Note that this will fail if the column is inuse,
/// i.e. it is used in a content type or list. I prefer to not catch the exception
/// (though it may be useful to add extra logging), hence feature deactivation/re- /// activation will fail. This effectively means this feature cannot be deactivated whilst the column is in use.
/// </summary>
/// <param name="column">Column to delete.</param>

private void attemptColumnDelete(SPFieldLookup column)
{
try
{
column.Delete();
}
catch (SPException e)
{
// consider logging full explanation..
throw;
}
}






private void createLookupColumn(SPWeb web, string sColumnDefinitionXml, string sColumnName)
{
// delete the column if it exists already and is not yet in use..
SPFieldLookup lookupColumn = null;
lookupColumn = web.Fields[sColumnName] as SPFieldLookup;
if (lookupColumn != null)
{
attemptColumnDelete(lookupColumn);
}

// now create the column from the CAML definition..
string sCreatedColName = web.Fields.AddFieldAsXml(sColumnDefinitionXml);

// also set LookupWebId so column can be used in webs other than web which hosts list..
lookupColumn = web.Fields[sCreatedColName] as SPFieldLookup;
lookupColumn.LookupWebId = web.Site.RootWeb.ID;
lookupColumn.Update();
}


public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
{
// delete the column if it exists already and is not yet in use..



// feature is scoped at Site, so the parent is type SPSite rather than SPWeb..
using (SPSite site = properties.Feature.Parent as SPSite)
{
SPWeb currentWeb = null;
Guid gRootWebId = Guid.Empty;
if (site != null)
{
currentWeb = site.RootWeb;
gRootWebId = currentWeb.ID;
}
else
{
currentWeb = properties.Feature.Parent as SPWeb;
gRootWebId = currentWeb.Site.RootWeb.ID;
}

SPFieldLookup lookupColumn = null;
lookupColumn = currentWeb.Fields["LocationsList"] as SPFieldLookup;

if (lookupColumn != null)
{
attemptColumnDelete(lookupColumn);
}
}
}



public override void FeatureInstalled(SPFeatureReceiverProperties properties)
{
}


public override void FeatureUninstalling(SPFeatureReceiverProperties properties)
{
}
}


If it's useful to have the full set of files illustrating the 2 features(particularly since the above isn't very readable!), leave me a comment and I'll make them available somewhere..

[Update - these have now been uploaded to Codeplex, see http://sharepointnutsandbolts.blogspot.com/2007/04/feature-to-create-lookup-fields-on.html]

Tuesday, 20 March 2007

How to debug SharePoint feature receivers

Just a quick one to detail how this is done in case someone finds it useful. For those still getting into working with SharePoint features, a feature receiver is a class which contains some code you've written to execute when a feature gets activated. Or deactivated, installed or uninstalled.

The key thing to note is that it's just standard ASP.Net debugging. The process is:-

  • deploy the assembly to the runtime location, either the GAC or the site bin directory. Note that if it's the bin directory your feature will also need appropriate CAS policy to grant the code the permissions it requires.
  • deploy the .pdb file to the same location. If this is the GAC, you can do the following:-

    - map a drive to the GAC folder i.e. C:\WINDOWS\assembly but using a UNC path such as [MachineName]\C$\WINDOWS\assembly. This allows you to browse the GAC without the shell which the framework puts on the folder, thus allowing you to see the actual structure of the files on disk.
    - locate the GAC_MSIL subfolder. In here you will see a directory for each assembly currently stored in the GAC. Find the directory for your assembly, and add the .pdb file so it sits next to the dll.


  • In Visual Studio, attach the debugger to the w3wp.exe process. Note that occasionally there will be 2 of these processes (e.g. when the process is being recycled), and it's possible to attach to the wrong one. Either do an IISReset to stop them both so that only 1 spins up with the next web request, or type 'iisapp' at the command prompt to get the process IDs of the running w3wp.exe processes. You can then match the correct one to the list which appears in the 'Attach debugger' dialog.
  • Activate the feature through the web UI (Site settings > Site Collection features/Site features). The debugger will now stop on any breakpoints you set.

And remember that the assembly must be built in debug mode so that the symbols are created.