SharePoint 2010 Sandbox Feature Deactivation – Removing Module Assets

This post is one post in a series of posts I penned in hopes it would help those looking to move a SharePoint 2010 Visual Studio 2010 farm based solution to a sandbox. There are many gotcha’s and pitfalls that you may run into or you may want to be aware of. You can learn more about this series here as well as download the CodePlex Project, From the Farm to the Sandbox, which includes the sample farm and sandbox solutions references through this series.

The related posts include:
Automatically Check In Files in a SharePoint 2010 Sandbox Feature Event Receiver

The Proper Web Part Control Class for a SharePoint 2010 Sandbox Solution

Custom Properties in a SharePoint 2010 Sandbox Web Part

Why CSSRegistration Will Not Work in a SharePoint 2010 Sandbox Solution

Link to JavaScript Files in a SharePoint 2010 Sandbox Solution

Housekeeping on Deactivation

If your feature is going to install assets to your site, i.e. web parts (.webpart or .dwp files), master pages, page layouts, images, javascript files, custom stylesheet, even possibly custom content type, list definitions and list instances, it is normally considered best practices to remove what you added when the feature is deactivated. I say normally because sometime you may not be able to remove custom Content Types or List Instances if they are being used. Or possibly your assets may have been modified, so removing them even if the feature is deactived could be a problem. Features in of themselves do not offer a pre-built way to remove what was added, nor would uninstalling and deleting the solution remove assets a feature installed, but features do offer the function:

public override void FeatureDeactivating(SPFeatureReceiverProperties properties)

By overriding this function we can add our own code that can use a similar method as we used when checking in files during the feature activation, that being we can take the Element Definition Collection from the feature itself, loop through it and look for what we know we added, possibly modules (don’t forget that .webpart files are added to _catalogs/wp via a module), or a List Instance, etc. Once we grab a list of assets, lists etc that were added we can remove them.

Let us look at feature deactivation code that could be used in a farm based solution’s feature event receiver.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
{
     using (SPSite spSite = properties.Feature.Parent as SPSite)
     {
          using (SPWeb web = spSite.RootWeb)
          {
               List<String> lModuleFiles = new List<String>();
               List<String> lListInstances = new List<String>();
               SPElementDefinitionCollection spFeatureElements = properties.Definition.GetElementDefinitions(CultureInfo.CurrentCulture);

               foreach (SPElementDefinition spElementDefinition in spFeatureElements)
               {
                    if (spElementDefinition.ElementType == "Module")
                    {
                         XmlElement xmlElementNode = (XmlElement)spElementDefinition.XmlDefinition;
                         String sModName = xmlElementNode.GetAttribute("Name");
                         String sModUrl = SPUtility.ConcatUrls(spSite.Url, xmlElementNode.GetAttribute("Url"));

                         foreach (XmlElement xmlChildElementNode in xmlElementNode.ChildNodes)
                         {
                              if (xmlChildElementNode.Name == "File")
                              {
                                   String sFile = SPUtility.ConcatUrls(sModUrl, xmlChildElementNode.GetAttribute("Url"));
                                   lModuleFiles.Add(sFile);
                              }
                         }
                    }
                    else if (spElementDefinition.ElementType == "ListInstance")
                    {
                         XmlElement xmlElementNode = (XmlElement)spElementDefinition.XmlDefinition;
                         String sModTitle = xmlElementNode.GetAttribute("Title");
                         String sModUrl = xmlElementNode.GetAttribute("Url");
                         lListInstances.Add(sModTitle);
                    }
               }
               #region Delete list instances added by this feature
               try
               {
                    if (lListInstances.Count > 0)
                    {
                         foreach (String listName in lListInstances)
                         {
                              try
                              {
                                   SPList workList = spSite.RootWeb.Lists.TryGetList(listName);
                                   if (workList != null)
                                   {
                                        Guid gd = workList.ID;
                                        spSite.RootWeb.Lists.Delete(gd);
                                   }
                              }
                              catch { }
                         }
                    }
               }
               catch { }
               try
               {
                    if (lModuleFiles.Count > 0)
                    {
                         foreach (String fileUrl in lModuleFiles)
                         {
                              try
                              {
                                   SPFile file = web.GetFile(fileUrl);
                                   if (file != null) {
                                        SPFolder folder = file.ParentFolder;
                                        file.Delete();
                                        web.Update();

                                        //attempt to delete the folder if it is now empty
                                        if (folder.Files.Count < 1)
                                             folder.Delete();
                                   }
                              }
                              catch { }
                         }
                    }
               }
               catch { }
          }
     }
}

And in a Sandbox Feature?

Well in a solution that works in a sandbox, we are out of luck with the above code. The reason being that Microsoft.SharePoint.Administration.SPElementDefinitionCollection spFeatureElements = properties.Definition.GetElementDefinitions(CultureInfo.CurrentCulture); is not available to us in sandboxed solution. That is actually a huge problem because that is how we are getting a list of assets to remove.

What is the fix?

There are a few different solutions to this problem, but the method I prefer is to have all modules add their assets to a “root” sub directory, unique to the feature in question. As an example, say a module adds files to the Style Library including images, js and css files. You might add the images to Style Library/Images and js files to Style Library/Scripts and so forth. Instead add all module assets to a predefined sub dir off Style Library, say YourFeature. This translates your files to folders such as:

Style Library/Your Feature/Images
Style Library/Your Feature/Scripts
Style Library/Your Feature/CSS

In your deactivating function you can look for the YourFeature folder within Style Library and that’s it, remove all of those assets.

For Lists, you could add which lists you would like to remove to a <list>, then loop through the list and remove each List Name. The same would work for Content Types and List definitions, but I am not going to review those here as often times you will need to leave custom Content Types even after a feature deactivation as they may be referenced and used.

Let’s looks at the code. Here what we are going to do is manually add which sub folders our feature added, so that we can quickly go and look for those and then remove them and their child items. Same for List Instances and the Web Parts.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
{
     SPSite spSite = properties.Feature.Parent as SPSite;
     if (spSite != null)
     {
          SPWeb spWeb = spSite.RootWeb;

          if (spWeb != null)
          {
               List<string> lListInstances = new List<string>();
               List<string> lStyleLibrarySubDirs = new List<string>();
               List<string> lWebParts = new List<string>();

               //add all list instance names created.
               lListInstances.Add("CustomListSandbox");

               //add all style library sub directories.
               lStyleLibrarySubDirs.Add("CustomSandbox");

               //add all list web parts added
               lWebParts.Add("CustomWebPartSandbox.webpart");

               //Delete list instances added by this feature
               try
               {
                    if (lListInstances.Count > 0)
                    {
                         foreach (String listName in lListInstances)
                         {
                              try
                              {
                                   SPList workList = spSite.RootWeb.Lists.TryGetList(listName);
                                   if (workList != null)
                                   {
                                        Guid gd = workList.ID;
                                        spSite.RootWeb.Lists.Delete(gd);
                                   }
                              }
                              catch { }
                         }
                    }
               }
               catch { }

               //Delete style library files
               try
               {
                    if (lStyleLibrarySubDirs.Count > 0)
                    {
                         //go through each list
                         for (int i = 0; i < spWeb.Lists.Count; i++)
                         {
                              if (spWeb.Lists[i].RootFolder.Name.Equals("Style Library"))
                              {
                                   SPDocumentLibrary styleLib = (SPDocumentLibrary)spWeb.Lists[i];
                                   foreach (String sSubDir in lStyleLibrarySubDirs)
                                   {
                                        foreach (SPListItem subFolder in styleLib.Folders)
                                        {
                                             //if themable dir
                                             if (subFolder.Name.Equals(sSubDir))
                                             {
                                                  if (subFolder.Folder.ParentFolder.Name.Equals("Style Library"))
                                                  {
                                                       DeleteSubFoldersAndFiles(subFolder.Folder);
                                                  }
                                             } //end if feature folder dir
                                        } //end going through each top level folder
                                   } //end going through each primary sub folder added by feature
                              } //end if this is the style library
                         } //end going through each top level site list (looking for style library)
                    }
               }
               catch { }

               //Delete Web part
               if (lWebParts.Count > 0)
               {
                    try
                    {
                         List<spfile> FilesToDelete = new List<spfile>();
                         SPList WebPartGallery = spWeb.Lists["Web Part Gallery"];
                         foreach (SPListItem WebPartTemplateFile in WebPartGallery.Items)
                         {
                              foreach (String sWebPart in lWebParts)
                              {
                                   if (WebPartTemplateFile.File.Name.Contains(sWebPart))
                                   {
                                        FilesToDelete.Add(WebPartTemplateFile.File);
                                   }
                              }
                         }
                         // delete Web Part template files
                         foreach (SPFile file in FilesToDelete)
                         {
                              file.Delete();
                         }
                    }
                    catch { }
               }
               spWeb.Close();
          }
          spSite.Close();
     }
}

For a complete review of the Farm and Sandbox solutions’ Feature Event Receiver, please reference From the Farm to the Sandbox for the full code.

To review a recent post where I got code for the web part gallery clean up, please reference this great article.

If you have another solution to the problem of feature deactivation housekeeping in a SharePoint 2010 Sandbox, please share them with us. Also, please do not forget to check out the other posts in this series, all related to converting a farm solution to a sandbox solution.

Speak Your Mind

*


6 + seven =