How to store each SharePoint Site Collection in its own Database – Part 3

As requested in Part 2, here is the feature code to add eventreceivers to all existing and new site collections. In my experience, I like to create a Web scoped feature that does the work for one web, and then write another feature scoped at the site collection or web application level that activates my web scoped feature for existing and new webs.

So first off, here is the code that does the work for one web:

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.SharePoint;
using Microsoft.SharePoint.WebPartPages;
using System.Web.UI.WebControls.WebParts;
using System.Web;
using Microsoft.SharePoint.Administration;

namespace Features.SiteCollectionDeletedEventReceiverWeb
{
    class FeatureReceiver : SPFeatureReceiver
    {
        public override void FeatureInstalled(SPFeatureReceiverProperties properties) { }
        public override void FeatureUninstalling(SPFeatureReceiverProperties properties) { }
        public override void FeatureActivated(SPFeatureReceiverProperties properties)
        {
            using (SPWeb web = (SPWeb)properties.Feature.Parent)
            {
                if (web == null) throw new ApplicationException("Web could not be found");

                web.EventReceivers.Add(SPEventReceiverType.SiteDeleted,
                    "EventHandlers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=XXXXXXXXXXXXXXXX",
                    "EventHandlers.DeleteDatabaseOnSiteDeleted");
                web.EventReceivers.Add(SPEventReceiverType.SiteDeleting,
                    "EventHandlers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=XXXXXXXXXXXXXXXX",
                    "EventHandlers.DeleteDatabaseOnSiteDeleted");
                web.Update();
            }
        }
        public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
        {
            using (SPWeb web = (SPWeb)properties.Feature.Parent)
            {
                if (web == null) throw new ApplicationException("Web could not be found");

    List<SPEventReceiverDefinition> eventReceiversToDelete = new List<SPEventReceiverDefinition>();

    foreach (SPEventReceiverDefinition eventReceiver in web.EventReceivers)
    {
     if (eventReceiver.Class == "EventHandlers.DeleteDatabaseOnSiteDeleted")
     {
      eventReceiversToDelete.Add(eventReceiver);
     }
    }

    foreach (SPEventReceiverDefinition eventReceiver in eventReceiversToDelete)
    {
     eventReceiver.Delete();
    }

    web.Update();
            }
        }
    }
}

And here is the code that activates the previous feature on all existing webs for the whole farm:

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;

namespace Features.SiteCollectionDeletedEventReceiverWebApplication
{
    class FeatureReceiver : SPFeatureReceiver
    {
        public Guid FeatureId()
        {
            return new Guid("{0C522324-C774-4424-B73E-1BCDEC147D89}");
        }

  public override void FeatureActivated(SPFeatureReceiverProperties properties)
        {
            SPWebApplication webApplication = properties.Feature.Parent as SPWebApplication;
            if (webApplication == null) throw new ApplicationException("WebApplication could not be found");

            foreach (SPSite site in webApplication.Sites)
            {
                foreach (SPWeb web in site.AllWebs)
                {
                    if (false == IsFeatureActive(FeatureId(), web.Features) &&
                        ShouldActivateFeature(web))
                    {
                        web.Features.Add(FeatureId(), true);
                    }
                    web.Dispose();
                }
                site.Dispose();
            }
        }

        public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
        {
            SPWebApplication webApplication = properties.Feature.Parent as SPWebApplication;
            if (webApplication == null) throw new ApplicationException("WebApplication could not be found");

            foreach (SPSite site in webApplication.Sites)
            {
                foreach (SPWeb web in site.AllWebs)
                {
                    if (IsFeatureActive(FeatureId(), web.Features))
                    {
                        web.Features.Remove(FeatureId(), true);
                    }
                    web.Dispose();
                }
                site.Dispose();
            }
        }

  public override void FeatureInstalled(SPFeatureReceiverProperties properties)
        {

        }

        public override void FeatureUninstalling(SPFeatureReceiverProperties properties)
        {

        }

        private bool ShouldActivateFeature(SPWeb web)
        {
   //Implement any custom logic whether to activate the feature on the web
            return true;
        }

  private bool IsFeatureActive(Guid featureId, SPFeatureCollection features)
        {
            try
            {
                SPFeature feature = features[featureId];
                if (feature != null) return true;
            }
            catch { }
            return false;
        }
    }
}

Also, this is feature stapling code in the feature_elements.xml file that activates the feature on any new webs:

<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
 <FeatureSiteTemplateAssociation Id="0C522324-C774-4424-B73E-1BCDEC147D89" TemplateName="GLOBAL" />
 <FeatureSiteTemplateAssociation Id="0C522324-C774-4424-B73E-1BCDEC147D89" TemplateName="STS#1" />
</Elements>

The global means it will be activated no matter what template is selected. I found that in some cases, a template (such as the blank template, STS#1) will not allow global based feature stapling. That’s why I added the extra line for STS#1, which is the blank template.

The full source can be downloaded here.

If you are going to copy my source, please remember a few things:

  1. Make sure you sign your assemblies
  2. EventReceivers are REQUIRED to be in the GAC
  3. Make sure you replace the ReceiverAssembly and ReceiverClass with your assembly and class names including the publickeytoken in the Feature.xml files
  4. Do the same thing for the EventReceivers in the SiteCollectionDeletedEventReceiverWeb FeatureReceiver

Using Features to enable Drop-Down Menus in Team Sites

I recently was trying to enable the top nav bar to be a drop down menu of all of the sub sites. I found this article http://sharingpoint.blogspot.com/2007/02/wss-v3-drop-down-menus-in-team-sites.html which told me everything I needed to know. Instead of modifying the default.master and the TopNavBar.ascx (for the application.master) to change the SPNavigationProvider to SPSiteMapProvider, I decided I wanted each teamsite to have the option and I’m reluctant from changing SharePoint system files (see http://www.graphicalwonder.com/?p=532), so I put this part into a feature. Here’s my code below:

The first file is Feature.xml

<Feature  Id="22b94164-5348-11dc-8314-0800200c9a66"
          Title="All Sites in Site Collection Navigation"
          Description="Enable all sites in the site collection top navigation bar."
          Version="12.0.0.0"
          Scope="Web"
          Hidden="false"
    DefaultResourceFile="core"
    xmlns="http://schemas.microsoft.com/sharepoint/">
    <ElementManifests>
        <ElementManifest Location="NavigationSiteSettings.xml"/>
    </ElementManifests>
</Feature>

The second file is NavigationSiteSettings.xml

<Elements
    xmlns="http://schemas.microsoft.com/sharepoint/">
    <!-- Top Nav -->
 <Control 
        Sequence="25"
        Id="TopNavigationDataSource"
  ControlClass="System.Web.UI.WebControls.SiteMapDataSource"
  ControlAssembly="System.Web, version=2.0.3600.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
        <Property Name="ID">topSiteMap</Property>        
        <Property Name="SiteMapProvider">SPSiteMapProvider</Property>
        <Property Name="ShowStartingNode">true</Property>
    </Control>
    <HideCustomAction
        Id="TopNav"
        HideActionId="TopNav"
        GroupId="Customization"
        Location="Microsoft.SharePoint.SiteSettings" />
</Elements>

I still needed to modify the default.master and TopNavBar.ascx (you don’t need to edit the TopNavBar.ascx if you are implementing my Using your SharePoint site Master Page on all application pages) so that more than one level is shown. I haven’t figured out a way around that part yet. Does anyone else know a way to override the SharePoint.AspMenu control?

————————————————————————————————————
Update (09/21/07): I have since found out that apparently the SPSiteMapProvider relies on the user having permissions to view the top-level site in the site collection. If the user does not have access to this top-level site, then they will be prompted for authentication and won’t be able to access the site. Using Reflector I was able to find the code where they are finding the top-level site without checking for permissions first.

I ended up having to write my own SiteMapProvider that did the correct security checking and trimming while still allowing access to the site.

On a side note, while I was looking into this issue, I also noticed that the Global Breadcrumbs (which also uses the SPSiteMapProvider) doesn’t do security trimming. So I wrote a SiteMapProvider for the Global Breadcrumbs as well that did security trimming. There doesn’t seem to be a delegate control for the Global Breadcrumbs like there is for the top nav so I couldn’t write a feature to override the default. So I decided to just change my default.master and in combination with Using your SharePoint site Master Page on all application pages¬†will use my custom SiteMapProvider for the Global Breadcrumbs.

————————————————————————————————————
Update (05/19/08): Some people have asked for the source code of my custom site map providers. You can download my latest files below:

AllSitesInSiteCollectionNavTrimmed.cs

AllSitesBreadcrumbsTrimmed.cs

FYI, this may not be the most efficient code and I have noticed some caching issues with it sometimes. Other than that it seems to work great.