Wednesday, September 22, 2010

WebProvisioned event receiver – a practical example

The WebProvisioned web event receiver is a new receiver type introduced in SharePoint 2010. It is one very useful addition to the already extensive suite of event receiver types in SharePoint, as it allows you to add some additional logic when a sub-site in your site collection gets created. In this sense it resembles to some extent the feature stapling functionality, but it differs from it because its scope is the site collection level whereas the feature stapling affects the whole farm (I personally dislike farm wide impact like this since you generally/potentially break all other solutions that may have been installed in the same farm). [updated 2010-12-29]

Creating and deploying a WebProvisioned event receiver is also a relatively simple task as you have a dedicated project item in Visual Studio 2010 – it basically supports all available receiver types, including the WebProvisioned one. With a couple of clicks you will be able to see an elements file for your WebProvisioned receiver that will look something like this:

<?xml version="1.0" encoding="utf-8"?>

<Elements xmlns="http://schemas.microsoft.com/sharepoint/">

  <Receivers >

    <Receiver>

      <Name>EventReceiver1WebProvisioned</Name>

      <Type>WebProvisioned</Type>

      <Assembly>$SharePoint.Project.AssemblyFullName$</Assembly>

      <Class>Stefan.SharePoint.EventReceiver1.EventReceiver1</Class>

      <SequenceNumber>10000</SequenceNumber>

    </Receiver>

  </Receivers>

</Elements>

the event receiver element definition will come with a new feature definition if you don’t have already one in your SharePoint project. Basically this is enough to have your new event receiver provisioned, but you may further consider two more important options: the first one is the Synchronization of the event receiver – you can specify either “synchronous” or “asynchronous” here (the “asynchronous” is the default one):

<Synchronization>Synchronous</Synchronization>

The Synchronization element should be placed below the “Receiver” element. Normally, I will opt for the synchronous type – this will be justified especially in cases where you will create your sites with the SharePoint UI and will expect to see immediately the results of the receiver when the home page of the site loads. Another thing is that with asynchronous receivers you have the risk of having save conflicts and concurrency issues especially when you create many sites simultaneously which is the case when you use a portal site definition.

The second option is the Scope option – you can specify it with the “Scope” attribute of the “Receivers” element – you can set it to either “Site” or “Web”:

<Receivers Scope="Site">

The scope determines whether the event receiver will be added to the SPSite.EventReceivers or SPWeb.EventReceivers collections. And there is a substantial difference in the behavior and application of the receiver depending on whether it is added to the SPSite or SPWeb level: in the first case it will be called for every sub-site created in the site collection, while in the second case, the receiver will be called only for the sub-sites that are immediate children of the site in whose EventReceivers collection the receiver is added. One other interesting “feature” (a rather peculiar one) is that when I tested my sample “Site” scoped WebProvisioned receiver it got called twice for one and the same site (no idea yet if this is an issue with my dev environment only or it is something by design). This is not the case for “Web” scoped WebProvisioned receivers.

Another important thing with the receiver’s scope is that the “Site” scope will be applied only if the activating feature also has “Site” scope (site collection scoped). If the feature has “Web” scope, the “Scope” attribute of the “Receivers” element will be ignored and the receiver will be added to the SPWeb.EventReceivers collection.

General purpose WebProvisioned event receiver

And now let’s have a look at one practical example - the general purpose WebProvisioned event receiver that I created (download source code from here). So, first let me say several words about the real life issues that I thought could be addressed with a WebProvisioned receiver – one of these is the ability of sub-sites to inherit certain settings from their parent site – for example the site default master page or the alternate CSS URL. This functionality is available for sub-sites based on the standard publishing site templates but that’s not the case for the non-publishing site definitions (like team site, blog site, wiki site, etc). This issue can be addressed with custom feature/features but it is not handy to create new site definitions that extend the standard ones only to add this extra functionality. Feature stapling is also an alternative in this case but it affects the whole farm which is definitely not a thing that I want to have in my SP installation [updated 2010-12-29]. And apart from the web settings we also have the settings managing the site navigation that are also suitable for inheriting – apart from the option to inherit the top navigation provided in the standard SharePoint “create site” page (and you can create your sites or site hierarchies with custom tools or scripting where this option is obviously not available).

One other important thing that I wanted to have in my WebProvisioned event receiver was that it should be fully configurable. And that this configurability is achieved via feature properties – because the event receiver will be naturally provisioned by a feature which will support a set of feature properties that will determine the behavior of the event receiver.

So, let me directly start with an example of the custom configuration feature properties so that you can get an idea of the type of functionality that this custom WebProvisioned receiver provides:

<Properties>

  <Property Key="ReceiverScope" Value="Site" />

  <Property Key="Web.MasterUrl" Value="~SiteCollection/_catalogs/masterpage/v4.master" />

  <Property Key="Web.AlternateCssUrl" Value="/_layouts/styles/mydefault.css" />

  <Property Key="CustomProperty.Test" Value="Some value" />

  <Property Key="PublishingWeb.IncludeInCurrentNavigation" Value="true" />

  <Property Key="Navigation.GlobalIncludePages" Value="true" />

  <Property Key="Navigation.GlobalIncludeSubSites" Value="true" />

</Properties>

The first feature property – “ReceiverScope” – as its name suggests, determines whether the event receiver should be added to the SPSite or SPWeb EventReceivers collection respectively (you can use either “Site” or “Web” in its value, exactly the same as in the “Scope” attribute from the event receiver sample element above). And as you can probably figure this out already – the configurability of the receiver’s scope with a feature property means that you can’t use declarative CAML in a “Receiver” feature element but rather use code in a feature receiver that should create the WebProvisioned event receiver. You will see that in the sample code I simply commented out the CAML in the event receiver’s element file and added a feature receiver to the original receiver’s feature (as both were created by Visual Studio 2010) that creates everything with code. The code of the feature receiver works for both “Site” and “Web” scoped feature so you don’t have to couple the “Site” web event receiver’s scope with a site collection scoped feature as it is the case with the “Receiver” feature element.

The rest of the feature properties determine the concrete behavior of the WebProvisioned event receiver – as you see, their keys follow a specific naming convention – they consist of two parts separated by a dot. The first part can contain the following predefined values: Web, CustomProperty, PublishingWeb and Navigation. They correspond to the target instance that the receiver will modify – SPWeb, SPWeb.AllProperties, PublishingWeb and PublishingWeb.Navigation respectively (SPWeb instance in this case is the SPWeb of the newly created (provisioned) web that the WebProvisioned receiver is invoked for and the PublishingWeb is the object retrieved from this SPWeb instance using the PublishingWeb.GetPublishingWeb static method). The second part of the key specifies the name of a public instance property of the target class in the case of the SPWeb, PublishingWeb and PortalNavigation (PublishingWeb.Navigation) target objects and a key in the Hashtable instance in the case of the SPWeb.AllProperties target object. The properties of the SPWeb, PublishingWeb and PortalNavigation classes can be only of the following .NET types – System.String, primitive .NET types (boolean, integer, double, etc) or .NET enumerations, properties of other .NET types are not supported (meaning that you can modify only properties from the above mentioned types in the target instances with the general purpose WebProvisioned event receiver).

So, the feature properties from this first sample set fixed values to some of the properties of the new web that the web event receiver was invoked for, but what about inheriting these properties from the parent web. Have a look at this second sample:

  <Property Key="Web.MasterUrl" Value="${~Parent}" />

  <Property Key="Web.CustomMasterUrl" Value="${~ParentNoRoot}" />

  <Property Key="Navigation.GlobalIncludePages" Value="${~SiteCollection}" />

As you see, you can specify special tokens in the “Value” attribute of the feature property elements too, these three options are available:

  • ${~Parent} – with this token you specify that the property of the “Key” attribute will be copied from the parent web of the current web
  • ${~ParentNoRoot} – this is almost the same as the first option, the only difference being that if the parent site is the root site of the current site collection the property won’t be copied to the current web (meaning that if the current web is not a child of the root web, the property will get copied).
  • ${~SiteCollection} – this token specifies that the property will be copied from the root web of the current site collection (no matter whether it is the parent of the current web or not)

In the case of the ${~ParentNoRoot} token you saw that there will be cases when the specified web property won’t get copied to the current web (for first level children). In this case you will need to specify two feature property elements with the same “Key”:

  <Property Key="Web0.CustomMasterUrl" Value="~SiteCollection/_catalogs/masterpage/v4.master" />

  <Property Key="Web1.CustomMasterUrl" Value="${~ParentNoRoot}" />

… or almost the same “Key” – you see that there is an extra digit before the dot separator in the “Key” attribute – this is a special trick that the WebProvisioned event receiver supports because SharePoint doesn’t allow you to have feature property elements with the same “Key” attribute. Another important thing here is the order of evaluation of the feature property elements that specify one and the same web property – in this case the properties appearing later in the properties’ definition will have precedence – this means that the static “v4.master” value will be applied only for first level child webs and all other sub-webs will have their “CustomMasterUrl” property copied from their respective parent webs.

So much about web properties’ inheritance, but what about the much rarer case when you may want the opposite - to disallow the web properties’ inheritance (which is the default behavior for the SPWeb’s MasterUrl, CustomMasterUrl and AlternateCssUrl properties for publishing sites). This sample would do the trick:

  <Property Key="Web.MasterUrl" Value="~SiteCollection/_catalogs/masterpage/v4.master" />

  <Property Key="Web.CustomMasterUrl" Value="~SiteCollection/_catalogs/masterpage/v4.master" />

  <Property Key="Web.AlternateCssUrl" Value="" />

  <Property Key="CustomProperty.__InheritsMasterUrl" Value="false" />

  <Property Key="CustomProperty.__InheritsCustomMasterUrl" Value="false" />

  <Property Key="CustomProperty.__InheritsAlternateCssUrl" Value="false" />

As you see, when you create publishing sub-sites it is not enough to just set the SPWeb’s MasterUrl, CustomMasterUrl and AlternateCssUrl properties. To “break” the inheritance of the master page settings you will also need to set several custom properties in the SPWeb.AllProperties collection as well.

And one last option that the general purpose WebProvisioned event receiver supports:

<Property Key="[SPS;STS#1]Navigation.GlobalIncludeSubSites" Value="true" />

you can specify optionally the names of the target site definitions for which the property setting should be applied. You can specify one or more site definition names separated by semi-colon in the beginning of the “Key” attribute enclosed in square brackets. You can use names of site definitions with or without configuration number added (after the ‘#’ character) – when you use a site definition name without a configuration number the property will be applied for all available configurations in this site definition (actually for webs based on all configurations).

And lastly several words about the sample solution with the general purpose WebProvisioned receiver: it contains a feature named “WebProvisionedReceiver” – this is the feature which actually installs the custom WebProvisioned receiver. In order that you can use it you will need to provide it with the appropriate for your scenario feature properties (starting with the “ReceiverScope” one) – this you can do in the onet.xml file of a site definition of yours in which you will add a reference to the “WebProvisionedReceiver” feature. Optionally (handy for testing purposes) you can add the feature properties directly to the feature’s definition (the feature.xml template file).

Besides the “WebProvisionedReceiver” feature the sample solution contains one more feature which is called “WebSettings”. It can be configured with exactly the same properties as the “WebProvisionedReceiver” feature (except the “ReceiverScope” one). This feature uses the same internal logic as the general purpose WebProvisioned receiver but instead applies the specified web properties directly to the web in which it is activated.

10 comments:

  1. Nice article, especially the scope details was new to me.

    Is there any particular reson that you use Feature properties instead of ..

    ReplyDelete
  2. Hi TGITM,
    I didn't quite understand your question (the instead of ... part). Basically, I wanted to have just one "receiver installing" feature which is fully configurable using feature properties alone. This also nicely fits in the model of my site template configurator utility - http://stefan-stanev-sharepoint-blog.blogspot.com/2010/08/site-template-configurator-utility.html

    ReplyDelete
  3. Stefan,

    Is there a way to create a farm-level WebProvisioned event, so that we DON'T have to manually activate this for every site collection? So far I have only been able to get this to work at the site or web level.

    The only way I could think of accomplishing this would be to feature staple the event handler feature... which kind of defeats the purpose of having the event handler as opposed to just using the feature stapler.

    The reason why I want to do my actions in the event handler is that I've found that when using feature stapling, the default.aspx page has not been created before my feature that is stapled fires. So I can't do certain actions to the default.asp page, like add/remove webparts.

    With the WebProvisioned event, I have found that everything is fully created and I can modify/add/remove items to the web as I see fit.

    Thanks!

    ReplyDelete
  4. Hi Anonymous,
    I understand your problem. I assume that you are most probably trying to add some customizations to one of the standard site templates (the usual suspect here is the standard "Blog" site definition). As I see it, there are a couple of approaches that can be used but they all seem pretty ugly. One possibility is to go on with the feature stapling and use a feature receiver which will launch a separate thread that will keep checking the SPWeb.Provisioned property in a loop (with short thread sleeps) until its value becomes TRUE. This solution will work in most cases unless you use a command line tool like the standard STSADM to create your sites - in the case of a console application, the application may terminate before the thread gets the chance to apply its changes. Another possible approach is to create a custom site definition based on the standard one (starting with copy-pasting and then adding your modifications to the onet.xml and/or schema.xml files in the site definition).
    As for the WebProvisioned event receiver - unfortunately its scope is limited to the site collection level and the combination of a stapling feature that would install it on every site collection is indeed not better than the feature stapling itself.

    ReplyDelete
  5. Hi Stefan,

    I read your article and it helped me understand the WebProvision event handling :) Great Post!

    As I understand Feature Stapling, you can staple the feature at Web Application level. I tried that and it works perfectly fine at web application and it doesn't affect the farm. See
    below scenarion I tried:

    1. Created Sample feature (Scope=Site) and stapled it to Team Site i.e. STS#0 using feature stapler (Scope=WebApplication).
    2. Created two WebApplications: WebApplicationWithFeatureStaplerActivated and WebAppWithougFeatureStaplerActivated.
    3. Created Site Collection using TEam Site template under WebApplicationWithFeatureStaplerActivated. Automatically sample feature got activated.
    4. Created Site Collection using Team Site template under WebAppWithougFeatureStaplerActivated. I checked the features at site collection level and I don't see a sample feature available in the features collection :)

    I think your post provides wrong information regarding feature stapling approach. Let me know if you have any questions.

    Cheers
    DB

    ReplyDelete
  6. Hi d.b.
    you are right, I had completely overlooked that - feature stapling can be scoped not only on farm level, but also on web application level as you noted, and even on site collection level - (http://msdn.microsoft.com/en-us/library/aa544294%28v=office.14%29.aspx). In the latter case it will work almost the same as the WebProvisioned event receiver.
    Thanks for the correcting remark.

    ReplyDelete
  7. Thank you, this led me to see the light :) I had struggled one whole day on an event receiver and then it took one minute to fix this with this article :)

    And the problem was just that I had the event receiver on web-scoped feature

    hank

    ReplyDelete
  8. HI, nice article. Can you please tell me if it is possible to deploy web level event receiver at the scope of a web applciation? Thanks in advance.

    ReplyDelete
  9. Hi Vinod,
    Probably feature stapling with web application scope will do the trick. Although it will work only for a predefined set of site templates (the ones specified in the stapling feature)

    Greets
    Stefan

    ReplyDelete
  10. Hi Stefan,

    I read through the article and its really good. However while i am trying to create groups on creation of a subsite within a top level site in a site collection, I am only able to create groups on the immediate top level site within site collection using the event receiver and not for the sites within those subsites. I have set the scope to site. Kindly suggest. Thanks in advance.

    Best Regards,
    Abhinav

    ReplyDelete