Removing Web Parts from the ‘My Site’ Web Part Gallery

For my first post I am going to illustrate how someone can limit the web parts available to users on their ‘My Site’. Several useful web parts are available out of the box in SharePoint, but some of those Web Parts may not work particularly well in some companies. The example I will use here are the OWA web parts. These are great if your organization uses Exchange (and of course I hope they do), but for those that don’t, or those that haven’t extended Exchange to support OWA, these web parts can confuse the user community and increase the call volume to a support center. The goal is to remove those web parts when a My Site is provisioned so that a user doesn’t even see them as an option.

The easy way to do this would be to remove them from the site definition. However, this isn’t a supported scenario. As Steve Peschka provided in his article on the subject, the ‘My Site’ site definition is hard-coded to SPSPERS and modifying the site definition puts you in an unsupported state. Now what? Steve also provided the mechanism to accomplish this task, use a feature stapler to staple our desired functionality to the original site definition. When the site is provisioned, our feature comes along with it. If we implement the feature activated receiver it will be called as the ‘My Site’ is created. Steve’s article gives a great explanation on customizing the presentation of our default ‘My Site’, but some of those web parts removed from the home page can still be added back by the user because they continue to exist in the web part catalog. We want to eliminate that option.

My Sites are created using a site collection. A site collection provides a site collection Web Part gallery that lists the web parts exposed within the site collection. It is this Web Part gallery that we want to target, and remove specific web parts that are provided out of the box. In order to do this we need to create a Feature Activated event receiver that is stapled to the site definition which will run when the feature is activated on a My Site. We’ll start by opening Visual Studio and adding a class library project to it. We also need to add a reference to the WSS assembly so that we can access the WSS APIs.

Next we’ll create a class that inherits from the FeatureReceiver base class and overrides the FeatureActivated method. Within this method we’ll place the code to remove our web parts from the gallery. The WSS API provides a GetCatalog() method on the SPSite object, used for referencing the site collections galleries. This method takes an SPListTemplateType enumeration as a parameter which will return an SPList reference to the catalog specified. After all, everything in SharePoint is a list. One of those enumerations is SpListTemplateType.WebPartGallery which will return the web part gallery as the list reference. With the SPList reference it is fairly easy to iterate the available web parts and remove those that would be available to users. The code below shows the guts of the implementation.

 /// <summary>
/// remove matching web parts from the web part gallery
/// </summary>
/// <param name="removeFromGallery">web parts to remove</param>
/// <param name="rootWeb">SPWeb site to remove</param>
/// <param name="readOnly">mode of operation</param>
private void RemoveFromWebPartGallery( string[] removeFromGallery, SPFeatureReceiverProperties properties ) {
    string traceCategoryName = "RemoveFromWebPartGallery";

    WriteLog( TraceProvider.TraceSeverity.InformationEvent, traceCategoryName, "Entered Method" );

    try {
        using ( SPWeb rootWeb = (SPWeb)properties.Feature.Parent ) {

            //remove web parts from the web part gallery
            using ( SPSite site = rootWeb.Site ) {
                SPList webPartCatalog = site.GetCatalog( SPListTemplateType.WebPartCatalog );

                if ( webPartCatalog != null ) {
                    List<Guid> matchingWebParts = new List<Guid>();

                    foreach ( SPListItem item in webPartCatalog.Items ) {

                        Predicate<string> exists = delegate( string p ) { return p.Trim().ToLower().Equals( item.Name.ToLower() ); };

                        if ( Array.Exists<string>( removeFromGallery, exists ) ) {
                            WriteLog(TraceProvider.TraceSeverity.Verbose, traceCategoryName, 
                                "***!!!Web Part in list...marked for removal" );
                            //add web part to collection of items to be removed
                            matchingWebParts.Add( item.UniqueId );
                        }
                    }

                    //now loop and remove
                    for ( int i = 0; i < matchingWebParts.Count; i++ ) {
                        WriteLog(TraceProvider.TraceSeverity.InformationEvent, traceCategoryName, 
                            string.Format( "Removing: {0} {1}", webPartCatalog.Items[matchingWebParts[i]].Name, matchingWebParts[i].ToString() ) );
                        //remove the web part from the web part gallery
                        webPartCatalog.Items[matchingWebParts[i]].Delete();
                        
                        WriteLog(TraceProvider.TraceSeverity.Verbose, traceCategoryName, 
                            "Removed" );
                    }
                } else {
                    WriteLog(TraceProvider.TraceSeverity.Exception, traceCategoryName, 
                        "Web Part Gallery returned a null reference" );
                }
            }
        }
    } catch ( Exception ex ) {
        WriteLog( TraceProvider.TraceSeverity.Exception, traceCategoryName, ex.Source );
        WriteLog( TraceProvider.TraceSeverity.Exception, traceCategoryName, ex.Message );
        WriteLog( TraceProvider.TraceSeverity.Exception, traceCategoryName, ex.StackTrace );
    }
}

We also want to make this configurable so that we can configure the web parts that should be removed by default. We can do that by specifying a property in our feature definition that we can reference in our code to figure out the web parts we want to remove. I chose a semi-colon delimited list to specify the web parts that should be removed from our organizations 'My Site' implementation. Since our Web Parts are listed in the gallery by web part file name we will use that as our target for identifying which ones we care about. The feature definition for our receiver looks like the picture below.

 <?xml version="1.0" encoding="utf-8" ?>
<Feature Id="5418E37D-4452-4742-AB74-072475668A70" Title="My Site Creation Feature" Scope="Web"
    ReceiverAssembly="MySiteGalleryManager, Version=1.0.0.0, Culture=neutral, PublicKeyToken=4573118414f00a99"
    ReceiverClass="Microsoft.Samples.Receivers.MySiteGalleryReceiver"
    Hidden="TRUE"
  xmlns="https://schemas.microsoft.com/sharepoint/">
    <Properties>
        <Property Key="REMOVE_FROM_GALLERY" Value="owa.dwp;owainbox.dwp;owacalendar.dwp;owatasks.dwp;owacontacts.dwp"/>
    </Properties>
</Feature>

How do we get to the property we have defined in the file? The SPFeatureReceiverProperties class that is passed to our FeatureActivated event has a property for Feature and that exposes a Properties property on it which returns an SPFeaturePropertyCollection to get information from the feature definition. How convenient!

So the code that initiates our searching of the web part gallery will get this property, parse it and send the resulting array of web part names to our RemoveWebPartsFromGallery() method we showed earlier. The FeatureActivated event is shown below.

 /// <summary>
/// handler for the feature activated receiver
/// </summary>
/// <param name="properties">SPFeatureReceiverProperties</param>
public override void FeatureActivated( SPFeatureReceiverProperties properties ) {
    TraceProvider.RegisterTraceProvider();

    //SPFeatureProperty removeFromPage = properties.Feature.Properties["REMOVE_FROM_PAGE"];
    //WriteLog(TraceProvider.TraceSeverity.InformationEvent, "Remove From Page Property", removeFromPage.Value );
    //string[] pageParts = removeFromPage.Value.Split( ";".ToCharArray() );
    
    SPFeatureProperty removeFromGallery = properties.Feature.Properties["REMOVE_FROM_GALLERY"];
    WriteLog(TraceProvider.TraceSeverity.InformationEvent, "Remove From Gallery Property", removeFromGallery.Value );
    string[] galleryParts = removeFromGallery.Value.Split( ";".ToCharArray() );

    try {

        //RemoveFromHomePage( pageParts, properties );

        RemoveFromWebPartGallery( galleryParts, properties );

    } catch (Exception ex) {
        WriteLog( TraceProvider.TraceSeverity.Exception, "FeatureActivated", ex.Source );
        WriteLog( TraceProvider.TraceSeverity.Exception, "FeatureActivated", ex.Message );
        WriteLog( TraceProvider.TraceSeverity.Exception, "FeatureActivated", ex.StackTrace );
    }
    TraceProvider.UnregisterTraceProvider();
}

Of course we want our work to be logged to the ULS log for our administrators in the event that something goes wrong, so we also include the ability to write to the Trace logs within our code. The class and implementation of a SharePoint trace provider can be found here.

That’s it. Now we need to package our receiver and staple it to the ‘My Site’ site definition. To do that we create a feature stapler that references our feature receiver, as shown below (the feature and element definitions are included together here for brevity.

 <?xml version="1.0" encoding="utf-8" ?>
<Feature Id="8A06A268-309A-44ce-B02C-8A9EB80DF16E" Title="My Site Manager Stapler" Scope="Farm"
  xmlns="https://schemas.microsoft.com/sharepoint/" >
    <ElementManifests>
        <ElementManifest Location="element.xml" />
    </ElementManifests>
</Feature>

<!-- element.xml file-->
<Elements xmlns="https://schemas.microsoft.com/sharepoint/" >
    <FeatureSiteTemplateAssociation Id="5418E37D-4452-4742-AB74-072475668A70" TemplateName="SPSPERS#0"/>
</Elements>

Once that is complete it is deployment time. Create a folder for the Feature stapler and another one for the feature receiver definition. Copy the respective feature.xml file in the corresponding folder. Now we need to build the WSP file to deploy this solution to the farm.  I have packaged mine as separate .wsp files in order to control the installation and activation of the associated features.  After our solution packages are ready we can use the install.bat script shown below (this again aggregates a couple of files to illustrate the concept) to install the receiver WSP, activate the feature, deploy the stapler WSP to the server and we are done. Now when a user creates a ‘My Site’ it will be without the web parts identified in the REMOVE_FROM_GALLERY property in the feature receiver definition file.

 :begin
@echo off

set webApplicationUrl=https://moss:32901

Call install_mysitegallerymanager.bat %webApplicationUrl%

Call install_mysitegallerystapler.bat

pause

rem begin install_mysitegallerymanager.bat file
rem //--------
:begin
@echo off

set url=%1
set solutionName=MySiteGalleryManagerReceiver
set featureName=MySiteGalleryManagerReceiver

@set PATH=C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\BIN;%PATH% 

echo --- Adding solution %solutionName% to solution store...
echo --- stsadm -o addsolution -filename %solutionName%.wsp

stsadm -o addsolution -filename %solutionName%.wsp

if errorlevel == 0 goto :deploySolution

echo ### Error adding solution %solutionName%
echo .
goto end

:deploySolution

echo --- Deploying solution %solutionName%...
echo --- stsadm -o deploysolution -name %solutionName%.wsp -immediate -allowGacDeployment -allowCasPolicies -force

stsadm -o deploysolution -name %solutionName%.wsp -immediate -allowGacDeployment -allowCasPolicies -force
stsadm -o execadmsvcjobs 

if errorlevel == 0 goto :activateFeature

echo ### Error deploying solution %solutionName%
echo .
goto end

:activateFeature

echo --- Activating features in solution %solutionName%...
echo --- stsadm -o activatefeature -filename %featureName%\feature.xml -url %url% -force 

stsadm -o activatefeature -filename %featureName%\feature.xml -url %url% -force 

if errorlevel == 0 goto :success

echo ### Error activating the features
echo .
goto end

:success

echo Successfully deployed solution and activated feature(s)..
echo .

goto end
:end
rem //--------
rem end of install_mysitegallerymanager.bat


rem begin install_mysitegallerystapler.bat
rem //--------
:begin
@echo off

set solutionName=MySiteGalleryManagerStapler
set featureName=MySiteGalleryManagerStapler

@set PATH=C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\BIN;%PATH% 

echo --- Adding solution %solutionName% to solution store...
echo --- stsadm -o addsolution -filename %solutionName%.wsp

stsadm -o addsolution -filename %solutionName%.wsp

if errorlevel == 0 goto :deploySolution

echo ### Error adding solution %solutionName%
echo .
goto end

:deploySolution

echo --- Deploying solution %solutionName%...
echo --- stsadm -o deploysolution -name %solutionName%.wsp -immediate -allowGacDeployment -allowCasPolicies -force

stsadm -o deploysolution -name %solutionName%.wsp -immediate -allowGacDeployment -allowCasPolicies -force
stsadm -o execadmsvcjobs 

if errorlevel == 0 goto :success

echo ### Error activating the features
echo .
goto end

:success

echo Successfully deployed solution ..
echo .

goto end
:end
rem //--------
rem end install_mysitegallerystapler.bat

Of course, there may be sites that already exist with these web parts already deployed to the gallery. It is easy enough to iterate the UserProfile store, find out which personal sites have been created and remove the web parts from their gallery if they exist. It is also important to remember that removing a web part from the gallery only prevents the web part from showing up in the web part catalog when they add a web part to the page. If they have the .dwp or .webpart file they can import it to their ‘My Site’ by simply uploading it (if it is already marked as a safe control). For the majority of our users removing it from the gallery is going to remove it from their mind and odds are the web part won’t be used, which was the initial goal.