LINQ to XML and Deleting Files on Feature Deactivation
Posted
Monday, February 16, 2009 5:15 PM
by
CoreyRoth
Using the Module element of a Feature has always been a great way to deploy a file. Unfortunately, the main issue with this method is that it doesn’t clean up after itself. How many times have you seen a site like this when working with features and the Module element?
This is because Feature Activation doesn’t remove anything. The easiest way to take care of this is write code to either remove the web parts from the zones or even easier just delete the page and assume activating the feature again is going to recreate it. Before, when I wrote code like this, it was always hard coded to delete a specific file or whatever, but now I have a better approach. I am going to use LINQ to XML to iterate through the Elements.xml file of my feature, find any files that were created and then delete them. Of course, you can do this without LINQ to XML, but it just makes it easier.
Before, we begin, consider a typical elements.xml file that looks like the one below. It deploys two files named default.aspx. One in the root and one in a folder called Test.
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
<Module Name="TestModule" Path="" Url="">
<File Name="default.aspx" Url="default.aspx" IgnoreIfAlreadyExists="FALSE" NavBarHome="True">
<AllUsersWebPart WebPartZoneID="Right" WebPartOrder="1">
<![CDATA[
<WebPart xmlns="http://schemas.microsoft.com/WebPart/v2" xmlns:iwp="http://schemas.microsoft.com/WebPart/v2/Image">
<Assembly>Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c</Assembly>
<TypeName>Microsoft.SharePoint.WebPartPages.ImageWebPart</TypeName>
<FrameType>None</FrameType>
<Title>$Resources:wp_SiteImage;</Title>
<iwp:ImageLink>/_layouts/images/homepage.gif</iwp:ImageLink>
<iwp:AlternativeText>$Resources:core,sitelogo_wss;</iwp:AlternativeText>
</WebPart>
]]>
</AllUsersWebPart>
</File>
</Module>
<Module Name="TestModule" Path="" Url="Test">
<File Name="default.aspx" Url="default.aspx" IgnoreIfAlreadyExists="FALSE">
<AllUsersWebPart WebPartZoneID="Right" WebPartOrder="1">
<![CDATA[
<WebPart xmlns="http://schemas.microsoft.com/WebPart/v2" xmlns:iwp="http://schemas.microsoft.com/WebPart/v2/Image">
<Assembly>Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c</Assembly>
<TypeName>Microsoft.SharePoint.WebPartPages.ImageWebPart</TypeName>
<FrameType>None</FrameType>
<Title>$Resources:wp_SiteImage;</Title>
<iwp:ImageLink>/_layouts/images/homepage.gif</iwp:ImageLink>
<iwp:AlternativeText>$Resources:core,sitelogo_wss;</iwp:AlternativeText>
</WebPart>
]]>
</AllUsersWebPart>
</File>
</Module>
</Elements>
I created a Feature Receiver as usual by inheriting from SPFeatureReceiver. On the FeatureDeactivting event, I have the following code. The code simply gets an instance of the web object and gets the path to the elements.xml file as described in my past post.
public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
{
using (SPWeb currentSite = (SPWeb)properties.Feature.Parent)
{
string elementsPath = string.Format(@"{0}\FEATURES\{1}\Elements.xml",
SPUtility.GetGenericSetupPath("Template"), properties.Definition.DisplayName);
DeleteFeatureFiles(currentSite, elementsPath);
}
}
The DeleteFeatureFiles method takes an instance of the web object and the path to the elements.xml. I then use LINQ to XML to load the document and call another method to delete the files. In this case, I am specifically interested in the Url attribute of any Module element and any child File element. The Url attribute contains the subfolder that the file is in. The result is a list of all modules and the Files element of each one. I had to specify the SharePoint namespace on the root element (as expected), but also on the Files child element. Nothing else required it strangely enough.
private void DeleteFeatureFiles(SPWeb currentSite, string elementsPath)
{
XDocument elementsXml = XDocument.Load(elementsPath);
XNamespace sharePointNamespace = "http://schemas.microsoft.com/sharepoint/";
// get each module name and the files in it
var moduleList = from module in elementsXml.Root.Elements(sharePointNamespace + "Module")
select new
{
ModuleUrl = (module.Attributes("Url").Any()) ? module.Attribute("Url").Value : null,
Files = module.Elements(sharePointNamespace + "File")
};
// iterate through each module and delete the child files
foreach (var module in moduleList)
{
DeleteModuleFiles(module.ModuleUrl, module.Files, currentSite);
}
}
The URL of the module and a collection of XElement representing each file is then passed to a method for deletion. I then just iterate through that list of files for deletion. Note, I did have to handle files in the root of the site differently. I think I can probably clean that code up, but for now this works.
private void DeleteModuleFiles(string moduleUrl, IEnumerable<XElement> fileList, SPWeb currentSite)
{
// delete each file in the module
foreach (var fileElement in fileList)
{
// use the name attribute if specified otherwise use Url attribute (since it is required)
string filename = (fileElement.Attributes("Name").Any()) ? fileElement.Attribute("Name").Value
: fileElement.Attribute("Url").Value;
// pass the moduleUrl if it has a value
if (!string.IsNullOrEmpty(moduleUrl))
currentSite.GetFile(string.Format("{0}/{1}", moduleUrl, filename)).Delete();
else
currentSite.Files.Delete(filename);
}
}
This works great for me and I can now just use one FeatureReceiver to handle this on any feature that I deploy. This can obviously be applied to other concepts. I’ll finally write my post on how to remove items from the web part gallery next.
UPDATE: I updated the DeleteModuleFiles method to look for the file in the Name attribute first and then the Url attribute for the name of the file.