Fixing Uninstalled Activated Features


[Updated October 11 2008]

I uploaded a precompiled project with suitable visual studio project to google code (I’m fed up with wordpress file type restrictions by now), you can download the complete zip (includes wsp) here. Many thanks to Anthony Sumner for taking the time to create the package.

(Mine is sort of souped into a number of other stuff that I’m not at liberty of sharing freely)

[/Update]

In my former post on content deployment I had to cleanup old removed features before the content deployment would work. This post presents the tool that I build to fix those features.

The problem in a nutshell is that when a feature is activated (at some scope) and later uninstalled it still lingers in the content database. The feature doesn’t do much; it’s uninstalled, it doesn’t work, but it is still registered as being activated at that particular site, site collection, web application or farm. As far as I can see an uninstalled but activated feature only affects the affects the content deployment.

The procedure for finding where uninstalled features are activated is fairly straightforward:

  1. Go through every site collection and site recursively
  2. For each SPWebApp, SPSite and SPWeb
    1. Iterate through the *.Features collection
    2. For each feature, if the definition is null it is uninstalled but still activated

The way I’ve done it is to add a new webpage to the central administration site that will run through a given web application and list all the features activated at each level. It presents a “Remove” and “Remove (force)” next to each feature. Specifically all problematic features are listed at the very top of the page, so they can easily be removed (only forced removal work for these features) without looking through the entire list (it can be quite long).

I should probably stress that “remove” in this context simply means to deactivate the feature at the particular level.

It looks like this (with a couple of “bad” features):

Sorry for the beeps… 😉

Adding the Link to Central Admin

Add a feature with the following feature.xml file (shortened a bit as I have a lot more elements in it):

<?xml version="1.0" encoding="utf-8" ?>
<Feature xmlns="http://schemas.microsoft.com/sharepoint/"
Id="47240D13-6F84-4F43-9FCA-0FF456D36B95"
Scope="Farm"
Title="Carlsberg Administration Utilities"
Description="Adds a number of pages to the Utilities section on the Operations tab."
AutoActivateInCentralAdmin="TRUE"
Hidden="TRUE">
<ElementManifests>
<ElementManifest Location="WebConfigModifications.xml" />
<!-- additional element manifest files for other features... -->
</ElementManifests>
</Feature>

And “WebAppFeatureReporter.xml” file that will add a link to the webpage under the “Web Application Configuration” section on the Administration tab:

<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
<CustomAction
Id="WebAppFeaturesList"
GroupId="WebApplicationConfiguration"
Location="Microsoft.SharePoint.Administration.ApplicationManagement"
Sequence="100"
Title="Web Application Features List" >
<UrlAction Url="/_admin/WebAppFeatureReporter.aspx"/>
</CustomAction>
</Elements>

Adding the Code

Finally we just need to add the actual code. There is only one file as I added the code directly to the aspx file, name it “WebAppFeatureReporter.aspx” and make sure that it’ll be copied to the /_admin folder under the template folder when the feature is deployed (your manifest.xml of the containing solution).

I’m satisfied with the functionality and layout of this page, but not the coding style within it. It was a rather quick hack that proved to work perfectly and stayed unchanged after that.

(note: If wordpress formatting messed something up – very likely – you can download the file here. Just rename file back to .aspx)

<%@ Assembly Name=”Microsoft.SharePoint.ApplicationPages, Version=12.0.0.0,

<%@ Assembly Name="Microsoft.SharePoint.ApplicationPages, Version=12.0.0.0,
Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>

<%@ Page Language="C#" Inherits="Microsoft.SharePoint.WebControls.LayoutsPageBase"
MasterPageFile="admin.master" %>
<%@ Import namespace="System.Reflection"%>
<%@ Import namespace="Microsoft.SharePoint.Administration"%>

<%@ Import Namespace="Microsoft.SharePoint.ApplicationPages" %>

<%@ Register TagPrefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls"
Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral,
PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="Utilities" Namespace="Microsoft.SharePoint.Utilities"
Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral,
PublicKeyToken=71e9bce111e9429c" %>
<%@ Import Namespace="Microsoft.SharePoint" %>

<%@ Register TagPrefix="wssuc" TagName="LinksTable" src="/_controltemplates/LinksTable.ascx" %>
<%@ Register TagPrefix="wssuc" TagName="InputFormSection"
src="/_controltemplates/InputFormSection.ascx" %>
<%@ Register TagPrefix="wssuc" TagName="InputFormControl"
src="/_controltemplates/InputFormControl.ascx" %>
<%@ Register TagPrefix="wssuc" TagName="LinkSection"
src="/_controltemplates/LinkSection.ascx" %>
<%@ Register TagPrefix="wssuc" TagName="ButtonSection"
src="/_controltemplates/ButtonSection.ascx" %>
<%@ Register TagPrefix="wssuc" TagName="ActionBar"
src="/_controltemplates/ActionBar.ascx" %>
<%@ Register TagPrefix="wssuc" TagName="ToolBar"
src="/_controltemplates/ToolBar.ascx" %>
<%@ Register TagPrefix="wssuc" TagName="ToolBarButton"
src="/_controltemplates/ToolBarButton.ascx" %>
<%@ Register TagPrefix="wssuc" TagName="Welcome" src="/_controltemplates/Welcome.ascx" %>

<asp:Content ID="Content1" ContentPlaceHolderID="PlaceHolderPageTitle" runat="server">
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="PlaceHolderPageTitleInTitleArea"
runat="server">
Web application feature reporter
</asp:Content>
<asp:Content ID="Content3" ContentPlaceHolderID="PlaceHolderPageImage" runat="server">
</asp:Content>
<asp:Content ID="Content4" ContentPlaceHolderID="PlaceHolderAdditionalPageHead"
runat="server">
</asp:Content>
<asp:Content ID="Content5" ContentPlaceHolderID="PlaceHolderMain" runat="server">

<table width="100%">
<tr><td align="right">
<SharePoint:WebApplicationSelector id="selector" runat="server"
OnContextChange="OnContextChange"
AllowAdministrationWebApplication="false"/>
</td></tr>
</table>

<asp:Literal ID="litMessage" runat="server" />
<asp:PlaceHolder ID="phMain" runat="server" />

</asp:Content>

<script runat="server" language="c#">

Hashtable parentObjects = new Hashtable();
string message = "";

protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
EnsureChildControls();
}

protected override void OnPreRender(EventArgs e)
{
base.OnPreRender(e);

if (selector.CurrentItem == null)
{
message = "Please select a web application to modify";
}
litMessage.Text = "<span style='text-color:red;'>" + message + "</span>";
}

protected void OnContextChange(object sender, EventArgs e)
{
//Recreate the controls
CreateControls();
}

protected override void CreateChildControls()
{
base.CreateChildControls();
CreateControls();
}

private void CreateControls()
{
//Rebuild controls
phMain.Controls.Clear();

PlaceHolder pnlWebApp = new PlaceHolder();
PlaceHolder pnlSite = new PlaceHolder();
PlaceHolder pnlWeb = new PlaceHolder();
PlaceHolder pnlInvalid = new PlaceHolder();

SPWebApplication oApp = selector.CurrentItem;

if( oApp != null )
{
SPSecurity.RunWithElevatedPrivileges(delegate { ListFeatures(pnlWebApp,
pnlSite, pnlWeb, pnlInvalid, oApp); });
}

if (pnlInvalid.Controls.Count > 0)
{
AddHtml(phMain, GetHeaderByTitle("Invalid scope or installed \"missing\""
+ " feature definitions"));
AddHtml(phMain, "<TABLE cellpadding='2' cellspacing='2' width='100%'>");
phMain.Controls.Add(pnlInvalid);
AddHtml(phMain, "</TABLE>");
}

phMain.Controls.Add(pnlWebApp);
phMain.Controls.Add(pnlSite);
phMain.Controls.Add(pnlWeb);
}

static void AddHtml(Control parent, string html)
{
LiteralControl lit = new LiteralControl();
lit.Text = html;
parent.Controls.Add(lit);
}

private void ListFeatures(
PlaceHolder pnlWebApplication,
PlaceHolder pnlSite,
PlaceHolder pnlWeb,
PlaceHolder pnlInvalid,
SPWebApplication webApp)
{

ListFeaturesAux("Web application", pnlWebApplication, pnlInvalid, webApp.Features);

foreach(SPSite site in webApp.Sites ){
ListFeaturesSite( pnlSite, pnlInvalid, site);
ListFeaturesWeb( pnlWeb, pnlInvalid, site.RootWeb );
}
}

private void ListFeaturesWeb(PlaceHolder pnlWeb, PlaceHolder pnlInvalid, SPWeb web)
{
ListFeaturesAux( "Web: " +  web.Url, pnlWeb, pnlInvalid, web.Features);

foreach( SPWeb subweb in web.Webs)
{
ListFeaturesWeb( pnlWeb, pnlInvalid, subweb);
}
}

private void ListFeaturesSite(PlaceHolder pnlSite, PlaceHolder pnlInvalid, SPSite site)
{
ListFeaturesAux( "Site collection: " +  site.Url, pnlSite, pnlInvalid, site.Features);
}

private void ListFeaturesAux(string header, PlaceHolder pnlContent,
PlaceHolder pnlInvalid, SPFeatureCollection features ){

AddHtml(pnlContent, GetHeader(header));
AddHtml(pnlContent, "<TABLE cellpadding='2' cellspacing='2' width='100%'>");

foreach (SPFeature feature in features)
{
if( feature.Definition != null )
{
//Feature Ok, add
GetRow(pnlContent, feature.Parent, feature.Definition);
}
else
{
AddHtml(pnlInvalid, "<tr><TD class='ms-vb2' style='font-weight: bold;'>");
AddHtml(pnlInvalid, "Missing feature definition (feature id:"
+ feature.DefinitionId + "), Scope: " + header );
AddHtml(pnlInvalid, "</td><td align='right'>");
AddUninstallButton(pnlInvalid, feature.Parent, feature.DefinitionId, false);
AddUninstallButton(pnlInvalid, feature.Parent, feature.DefinitionId, true);
AddHtml(pnlInvalid, "</td><tr>");
}
}

AddHtml(pnlContent, "</TABLE>");
}

protected static string GetHeader(string scope)
{
return GetHeaderByTitle("Features scoped at the level of the " + scope);
}

protected static string GetHeaderByTitle(string title)
{
StringBuilder sb = new StringBuilder();

sb.Append("<TABLE cellpadding='2' cellspacing='2' width='100%' class='ms-toolbar'>");
sb.Append("<TR><TD class='ms-toolbar' nowrap='true'>");
sb.Append("<TABLE cellpadding='1' cellspacing='0' border='0'><TR>");
sb.Append("<TD class='ms-toolbar' nowrap style='padding: 2px'>");
sb.Append( title + "</TD></TR></TABLE></TD></TR></TABLE>");

return sb.ToString();
}

protected void GetRow(PlaceHolder pnl, object parent, SPFeatureDefinition feature)
{

AddHtml( pnl, "<TR><TD class='ms-vb2' style='font-weight: bold;'>");
AddHtml( pnl, "<B>" + feature.DisplayName + "</B>  ("
+  feature.Id + ")</TD>");

AddHtml(pnl, "<TD align='right'>");
//Create uninstall buttons
AddUninstallButton(pnl, parent, feature.Id, false);
AddUninstallButton(pnl, parent, feature.Id, true);

AddHtml( pnl, "</TD></TR>");
AddHtml( pnl, "<TR><TD class='ms-vb2' colspan='2'><I>"
+ feature.GetDescription(new System.Globalization.CultureInfo("en-us"))
+ "</I></TD>");
AddHtml( pnl, "<TR><TD class='ms-vb2' colspan='2'>");

if( feature.ActivationDependencies.Count>0 ) {
AddHtml( pnl, "Activation dependencies:");
AddHtml(pnl, "<UL>");
foreach (SPFeatureDependency dep in feature.ActivationDependencies) {
SPFeatureDefinition depFet = GetFeature(dep.FeatureId);
AddHtml(pnl, "<LI>" + depFet.DisplayName + " (" + depFet.Id + ")</LI>");
}
AddHtml(pnl, "</UL>");
}
AddHtml(pnl, "</TD>");
AddHtml(pnl, "</TR>");
}

private void AddUninstallButton(PlaceHolder pnl, object parent, Guid featureId, bool force)
{
//Add the link to the parent object for this feature
string buttonId = GetFeatureContextId( parent, featureId );
parentObjects[ buttonId] = parent;

Button b = new Button();
b.ID = "remove_" + buttonId + "_" + force;
b.Text = "Remove" + (force ? " (force)" : "" );
b.CommandArgument = buttonId; //featureId.ToString();
b.CommandName = force ? "forceremove" : "remove";
b.Command += new CommandEventHandler(UninstallCommand);

pnl.Controls.Add( b );
}

/// <summary>
/// remove a given feature with or without force
/// </summary>
/// <param name="sender"></param>
/// <param name="e">CommandArgyment is the id of the feature, syntax: "parent name|feature guid"</param>
void UninstallCommand(object sender, CommandEventArgs e)
{
try
{
string operation = e.CommandName;
string globalFeatureId = e.CommandArgument.ToString();
Guid featureId = new Guid(globalFeatureId.Split('|')[1]);

//Should NEVER be null
object parent = parentObjects[globalFeatureId];

SPSecurity.RunWithElevatedPrivileges(delegate
{
//Could have used reflection to invoke member, but this is more
// readable and less error prone.
SPFeatureCollection features;
if (parent is SPWebApplication)
{
features = ((SPWebApplication) parent).Features;
}
else if (parent is SPSite)
{
features = ((SPSite) parent).Features;
}
else if (parent is SPWeb)
{
features = ((SPWeb) parent).Features;
}
else
{
throw new ApplicationException("Unsupported parent:" + parent);
}

switch (operation)
{
case "remove":
features.Remove(featureId);
message = "Removed feature:" + featureId;
break;
case "forceremove":
message = "Removed feature:" + featureId + " (forced)";
features.Remove(featureId, true);
break;
default:
throw new ApplicationException(
"Failed to remove feature, unknown operation");
}
});

//Do a refresh (ok to do at this time
//  since only one command event will be fired per request)
CreateControls();
}
catch( Exception exc )
{
message = exc.ToString();
}
}

///<summary>
/// Get a feature definition from the feature id
///</summary>
protected static SPFeatureDefinition GetFeature(Guid featureId)
{
SPFeatureDefinition retval = null;
foreach (SPFeatureDefinition feature in SPFarm.Local.FeatureDefinitions)
{
if (feature.Id == featureId)
{
retval = feature;
break;
}
}
return retval;
}

///<summary>
/// Get a feature reference that includes the parent context.
/// Result is "parent id|feature id".
///</summary>
private static string GetFeatureContextId(object parent, Guid featureId)
{
if( parent is SPSite )
{
return ((SPSite)parent).ID + "|" + featureId.ToString("N");
}
else if( parent is SPWeb )
{
return ((SPWeb)parent).ID + "|" + featureId.ToString("N");
}
else if( parent is SPWebApplication)
{
return ((SPWebApplication)parent).Id + "|" + featureId.ToString("N");
}
throw new ApplicationException("Unknown feature parent:" + parent);
}
</script>

Final Comments

I will accept no critic of the actual code in the web page as it was compiled rather quickly by cut/paste from a few other custom admin pages, to solve a specific task as quickly as possible. I do like the way it’s added to the central administration site (thanks to Joel Oleson for that), it works, it looks fairly good, the code style is crap, let’s leave it at that 😉

I actually don’t remember where the original layout for this page came about but it was either something from Joels Oleson blog or a feature from GotDotNet or a mixture thereof.

The breadcrumb on the page is specified in another xml file, not shown here. See Joel’s blog for that subject, as it is not without problems and worth a proper discussion.

Hope it helps all of you guys to get an overview of where your features are used whether or not you use content deployment.

Advertisements