Removing Web Parts when your feature gets deactivated

See below for code adapted for usage in SP2013.

Today I took some time reading Inside MicroSoft SharePoint 2010. As I am already playing with SharePoint 2013 Preview, I thought it would be time to finally finish reading this book. As I was reading, I came across a section about deleting .webpart files from the Web Part Gallery, when deactivating a feature. Just a small piece of text with some sample code. I thought, yes I know the .webpart file gets left behind, but I don't always think about removing them when a feature gets deactivated. I also know that this gets skiped more often than we think, and it often doesn't get noticed.

I do get a bit bongus about this, why the heck do we need to write code to do this. We don't have to put these files there when we activate the feature, so why should we add some lame code to remove these files when the feature gets deactivated. Have they gone completely mad? But than, thinking about it again, it seems to make more sense, in the way that content is preserved even when deactivating a feature. Power users could have edited the .webpart files so they contain other default values, sadly they forget to mention this in the book. This is especialy true when deploying pages, but for me, with .webpart files it seems that the best option is to remove these files on deactivation. But you should be aware of the concequences.

So to remove the .webpart files from the Web Part Gallery, I created the following helper method in my FeatureHelper class, based on the code in the book, but a bit more reusable:

public static void RemoveFeatureWebParts(SPFeatureReceiverProperties properties)
{
    SPSite site = properties.Feature.Parent as SPSite;
    if (site == null)
    {
        SPWeb web = properties.Feature.Parent as SPWeb;
        if (web == null)
            return;

        site = web.Site;
    }

    string wpListName = SPUtility.GetLocalizedString("$Resources:webpartgalleryList", 
                            "core", site.RootWeb.Language);
    SPList wpList = site.RootWeb.Lists.TryGetList(wpListName);
    if (wpList == null)
        return;

    XmlNode featureXml = properties.Feature.Definition.GetXmlDefinition(CultureInfo.CurrentCulture);
    XDocument xDoc = XDocument.Load(new XmlNodeReader(featureXml));

    List<SPFile> filesToDelete = new List<SPFile>();
    foreach (SPListItem item in wpList.Items)
    {
        if (IsFileInFeature(item, xDoc))
            filesToDelete.Add(item.File);
    }
    foreach (SPFile file in filesToDelete)
        file.Delete();
}

private static bool IsFileInFeature(SPListItem item, XDocument xDoc)
{
    XNamespace ns = "http://schemas.microsoft.com/sharepoint/";
    return (from e in xDoc.Descendants(ns + "ElementFile")
            where e.Attribute("Location").Value == item.File.Name
                || e.Attribute("Location").Value.EndsWith("\\" + item.File.Name)
            select true).FirstOrDefault();
}

Now in your webparts feature SPFeatureReceiver, and yes you do need a feature receiver for every feature that deploys webparts to do this, you can just implement the FeatureDeactivating event, calling RemoveFeatureWebParts:

public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
{
    FeatureHelper.RemoveFeatureWebParts(properties);
}

I hope this helps you all, so if you think removing the .webpart files after feature deactivation is a good thing for your feature, you can easily implement this.

Revision of text from 7/22/2012

Removing Web Parts: get things working in SharePoint 2013

Well that was a bummer, my removing web parts code didn't work in SP2013. The code worked on the assumption that the file name of the .webpart file on the file system was the same as in the SharePoint web part gallery. By default this works fine with Visual Studio 2010, but when creating SP2013 web parts, the .webpart file gets prefixed:

<File Path="DummyPartOne\DummyPartOne.webpart" Url="RemovingWebParts_DummyPartOne.webpart" Type="GhostableInLibrary">

I do think that prefixing is a good idea, as there is less chance for a naming conflict, but sadly it makes the code redundant as no web parts are found and thus cannot be removed. So I started looking at the code to change things so it will work in SP2013 projects. At the start I already had an idea how I could fix the code, so it was just a question of coding this. As I was working on this I also changed the code that removed the .webpart files, as the code wasn't that efficient if you had many web parts.

Here is the code that can be used in SharePoint 2013:

public static void RemoveFeatureWebParts(SPFeatureReceiverProperties properties)
{
    SPSite site = properties.Feature.Parent as SPSite;
    if (site == null)
    {
        SPWeb web = properties.Feature.Parent as SPWeb;
        if (web == null)
            return;

        site = web.Site;
    }

    string wpListName = SPUtility.GetLocalizedString("$Resources:webpartgalleryList",
                            "core", site.RootWeb.Language);
    SPList wpList = site.RootWeb.Lists.TryGetList(wpListName);
    if (wpList == null)
        return;

    List<string> webpartsInFeature = GetWebPartsInFeature(properties);
    foreach (string wp in webpartsInFeature)
    {
        string fileRelativeUrl = SPUrlUtility.CombineUrl(wpList.RootFolder.Url, wp);
        SPFile file = site.RootWeb.GetFile(fileRelativeUrl);
        if (file.Exists)
            file.Delete();
    }
}

private static List<string> GetWebPartsInFeature(SPFeatureReceiverProperties properties)
{
    XmlNode featureXml = properties.Feature.Definition.GetXmlDefinition(CultureInfo.CurrentCulture);
    XDocument xDoc = XDocument.Load(new XmlNodeReader(featureXml));
    XNamespace ns = "http://schemas.microsoft.com/sharepoint/";
    List<string> webparts = (from e in xDoc.Descendants(ns + "ElementFile")
                                where e.Attribute("Location").Value.EndsWith(".webpart")
                                select e.Attribute("Location").Value).ToList();

    List<string> webpartsFromUrl = new List<string>();
    foreach (string wp in webparts)
    {
        webpartsFromUrl.Add(GetWebPartFromUrl(properties, wp));
    }
    return webpartsFromUrl;
}

private static string GetWebPartFromUrl(SPFeatureReceiverProperties properties, string wp)
{
    string[] pathSplit = wp.Split('\\');
    string path = Path.Combine(properties.Definition.RootDirectory, pathSplit[0]);
    using (FileStream file = File.OpenRead(Path.Combine(path, "elements.xml")))
    {
        XNamespace ns = "http://schemas.microsoft.com/sharepoint/";
        XDocument xDoc = XDocument.Load(file);
        return (from e in xDoc.Descendants(ns + "File")
                where string.Compare(e.Attribute("Path").Value, wp, true) == 0
                select e.Attribute("Url").Value).FirstOrDefault();
    }
}

So now you can still use flexible code to remove web parts in SharePoint 2013 projects.

For your convenience I have now added my RemovingWebParts sample solution for SP2013.

By the way, this code can also be used in SP2010 projects, but you will need a little change in 'GetWebPartFromUrl', as XDocument.Load in .Net 3.5 doesn't support a stream directly. Below you will find what the change for SP2010 will look like:

using (FileStream file = File.OpenRead(Path.Combine(path, "elements.xml")))
using (TextReader fileReader = new StreamReader(file))
{
    XNamespace ns = "http://schemas.microsoft.com/sharepoint/";
    XDocument xDoc = XDocument.Load(fileReader);