This posting covers a subset of a more general subject; Using Custom Application Pages to Extend the Capabilities of the Central Administration Site. Perhaps I will elaborate on the subject another time. For now, I will cover how to create a custom application page to activate or deactivate site-scoped (web) features globally.
Not much of an explanation is needed in regards to the purpose or practicality of this posting, as I'm sure most of you have been there, but for the sake of being thorough here is a brief description of a problem it helps address:
With just about every SharePoint project I've worked on, there has been some level of custom branding, usually involving new themes, master pages, and page layouts. Ultimately, and preferably, these get deployed via site-scoped (web) features. While these features can be automatically activated during the site provisioning process for new sites via Feature Stapling, there are often existing sites that we need to activate the feature on. Activation of these features can generally be narrowed down to specific types of sites, that is sites created from specific templates or site definitions, and generally target specific web applications. Out of the box, SharePoint only allows activation of site scoped features per site via the Site Settings page or stsadmin.exe. Global activation of such features targeting sites of a specific type/definition requires the creation of custom scripts or batch files. The solution; create a custom application page for the Central Administration site that allows farm administrators to activate site features across all sites in a given web application and of a specific site definition.
WARNING: Great care should be taken when applying Features globally. A poorly developed feature, or one that is targeted towards one type of site and applied to another, may render the site inoperable. Make sure you test your features thoroughly with each applicable site definition/template before activating features globally using this or any other technique.
The first step is to create a page that inherits from the GlobalAdminPageBase class, which is in the Microsoft.SharePoint.ApplicationPages.Administration assembly. This is the class that most of your Central Administration Pages should inherit from as it implements security and other related logic with the Central Administration site.
<%@ Assembly Name="Microsoft.SharePoint.ApplicationPages.Administration, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"%>
<%@ Page Language="C#" AutoEventWireup="true" Inherits="Microsoft.SharePoint.ApplicationPages.GlobalAdminPageBase" MasterPageFile="~/_admin/admin.master" %>
Additionally, you'll need to include references to the following Namespaces and SharePoint Controls as they will be used throughout the page:
<%@ Import Namespace="System.Net" %>
<%@ Import Namespace="Microsoft.SharePoint" %>
<%@ Import Namespace="Microsoft.SharePoint.Administration" %>
<%@ Import Namespace="Microsoft.SharePoint.Utilities" %>
<%@ 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" %>
<%@ Register TagPrefix="wssawc"
Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"%>
<%@ Register TagPrefix="wssuc" TagName="ToolBar" src="~/_controltemplates/ToolBar.ascx" %>
<%@ Register TagPrefix="wssuc" TagName="InputFormSection" src="~/_controltemplates/InputFormSection.ascx" %>
<%@ Register TagPrefix="wssuc" TagName="InputFormControl" src="~/_controltemplates/InputFormControl.ascx" %>
<%@ Register TagPrefix="wssuc" TagName="ButtonSection" src="~/_controltemplates/ButtonSection.ascx" %>
The "Administration" namespace will be used, amongst other things, to obtain a reference to the SharePoint farm. The "Utilities" namespace will be used for security reasons when validating the page request. The "WebControls" namespace and additional "ascx" controls are used in rendering certain controls on the page.
Add the following Content Placeholders for the title, description, and main content area.
<asp:Content ID="Content1" contentplaceholderid="PlaceHolderPageTitle" runat="server">
Global Site Feature Management
</asp:Content>
<asp:Content ID="Content2" contentplaceholderid="PlaceHolderPageTitleInTitleArea" runat="server">
Global Site Feature Management
</asp:Content>
<asp:content ID="Content3" contentplaceholderid="PlaceHolderPageDescription" runat="server">
Use this page to activate or deactivate site-scoped Features across all sites based on the selected site definition for the selected Web Application.
</asp:content>
<asp:content ID="Content4" contentplaceholderid="PlaceHolderMain" runat="server">
</asp:content>
Next we will add an InputForm, ButtonSection, and a WebApplicationSelector controls to the page. This will allow the user to select the Web Application on wish to activate the feature. We use the same controls SharePoint uses on its Central Administration pages to provide a consistent look and feel. We will also add; a RadioButtonList control to display the features, a CheckBox to indicate if the action should be forced, a DropDownList to specify the action (activate or deactivate), and a CheckBoxList to display the Site Definitions. We will encapsulate all of this in an ASP Panel control to toggle the visibility of the form when the user clicks OK.
<!-- Use a ASP.NET Panel Control to show or hide the form from code -->
<asp:Panel ID="inputForm" runat="server">
<table border="0" cellspacing="0" cellpadding="0" width="100%">
<tr>
<td>
<!-- *****************************************
USE SHAREPOINT THE ButtonSection CONTROL
TO DISPLAY THE "OK" AND "CANCEL" BUTTONS -->
<wssuc:ButtonSection runat="server" TopButtons="true"
BottomSpacing="5" ShowSectionLine="false">
<Template_Buttons>
<asp:Button UseSubmitBehavior="false" runat="server"
class="ms-ButtonHeightWidth" OnClick="BtnSubmit_Click"
Text="<%$Resources:wss,multipages_okbutton_text%>"
id="btnSubmitTop"
accesskey="<%$Resources:wss,okbutton_accesskey%>"
Enabled="true"/>
</Template_Buttons>
</wssuc:ButtonSection>
<!-- **************************************** -->
<!-- **************************************
DISPLAY THE WEB APPLICATION SELECTOR
USING THE InputFormSecton AND WebApplicationSelector CONTROLS.
THE TITLE AND DESCRIPTION ARE SPECIFIED IN THE CORRESPONDING
ATTRIBUTES OF THE InputFormSection CONTROL, WHILE THE CONTROLS
THEMSELVES ARE PLACED INSIDE THE InputFormControl SECTION -->
<wssuc:InputFormSection runat="server"
Title="Web Application"
Description="Select a Web Application" >
<Template_InputFormControls>
<tr>
<td>
<SharePoint:WebApplicationSelector id="Selector" runat="server"
TypeLabelCssClass="ms-listheaderlabel"
AllowAdministrationWebApplication="true" />
</td>
</tr>
</Template_InputFormControls>
</wssuc:InputFormSection>
<!-- ****************************************** -->
<!-- ****************************************
DISPLAY THE AVAILABLE FEATURES
USING THE InputFormSecton AND ASP:RadioButtonList CONTROLS -->
<wssuc:InputFormSection runat="server"
Title="Site Feature"
Description="Select a site feature from the list" >
<Template_InputFormControls>
<tr>
<td>
<!-- Use a SharePoint Required Field Validator to ensure
that a web application is selected -->
<wssawc:InputFormRequiredFieldValidator
ID="ReqValAppPoolPassword"
ControlToValidate="rdSiteFeatures"
ErrorMessage="You must select a Feature to activate"
Runat="server" />
<asp:RadioButtonList ID="rdSiteFeatures" runat="server"
CssClass="ms-listheaderlabel" EnableViewState="true" />
</td>
</tr>
</Template_InputFormControls>
</wssuc:InputFormSection>
<!-- ******************************************* -->
<!-- *******************************************
DISPLAY A SECTION TO SPECIFY THE ACTION TO
PERFORM WITH THE FEATURE-->
<wssuc:InputFormSection runat="server"
Title="Action"
Description="Specify if activating or deactivating the selected Feature." >
<Template_InputFormControls>
<tr>
<td Class="ms-listheaderlabel">
<asp:DropDownList ID="lstAction" runat="server"
EnableViewState="true">
<asp:ListItem Text="Activate Feature" Value="Activate" />
<asp:ListItem Text="Deactivate Feature" Value="Deactivate" />
</asp:DropDownList>
</td>
</tr>
</Template_InputFormControls>
</wssuc:InputFormSection>
<!-- ******************************************* -->
<!-- *******************************************
DISPLAY A SECTION TO SPECIFY IF THE FEATURE ACTIVATION
SHOULD BE FORCED -->
<wssuc:InputFormSection runat="server"
Title="Force Feature Activation / Deactivation"
Description="Check here to force feature activation or deactivation." >
<Template_InputFormControls>
<tr>
<td>
<asp:CheckBox ID="chkForce" runat="server"
EnableViewState="true" Text="<b>Force</b>"
CssClass="ms-listheaderlabel" />
</td>
</tr>
</Template_InputFormControls>
</wssuc:InputFormSection>
<!-- ******************************************* -->
<!-- *******************************************
DISPLAY THE AVAILABLE SITE DEFINITIONS
USING THE InputFormSecton AND ASP:CheckBoxList CONTROLS -->
<wssuc:InputFormSection runat="server"
Title="Site Definitions"
Description="Select the site definitions to which the
feature will be applied" >
<Template_InputFormControls>
<tr>
<td>
<asp:CheckBox ID="chkSelectAll" runat="server"
CssClass="ms-listheaderlabel"
Text="<b>Select All</b><br>Click here to select all site
definitions."
OnCheckedChanged="chkSelectAll_Changed"
AutoPostBack="true" />
<asp:CheckBoxList ID="chkListSiteDefinitions" runat="server"
CssClass="ms-listheaderlabel" AutoPostBack="false" />
</td>
</tr>
</Template_InputFormControls>
</wssuc:InputFormSection>
<!-- ******************************************** -->
<!-- ****************************
OK AND CANCEL BUTTON SECTION -->
<wssuc:ButtonSection runat="server" TopButtons="false"
BottomSpacing="5" ShowSectionLine="true">
<Template_Buttons>
<asp:Button UseSubmitBehavior="false" runat="server"
class="ms-ButtonHeightWidth" OnClick="BtnSubmit_Click"
Text="<%$Resources:wss,multipages_okbutton_text%>"
id="BtnSubmitBottom"
accesskey="<%$Resources:wss,okbutton_accesskey%>"
Enabled="true"/>
</Template_Buttons>
</wssuc:ButtonSection>
<!-- **************************** -->
</td>
</tr>
</table>
</asp:Panel>
<asp:Literal ID="litMessages" runat="server" />
Next we'll need to populate the list of features. Check the PostBack state of the page, if false, get the available features from the FeatureDefinitions property of the SPFarm object. Iterate through each of the features and add a list item the the feature radio button list for each one that is scoped at the site(web) level.
//Get the regional localeID and Culture Information (Regional Settings)
//these will be used to retrieve the list of features and
//site definitions
int localeID = (int)SPContext.Current.RegionalSettings.LocaleId;
System.Globalization.CultureInfo cInfo = new System.Globalization.CultureInfo(localeID);
//Get a reference to the farm object we will use it
//to retrieve the feature definitions
SPFarm thisFarm = SPFarm.Local;
foreach (SPFeatureDefinition featureDefinition in thisFarm.FeatureDefinitions)
{
//Check that each feature is scoped at the web level and
//that it is not hidden before adding it to the list
if ((featureDefinition.Scope == SPFeatureScope.Web) && !(featureDefinition.Hidden))
{
ListItem siteFeatureItem = new ListItem("<b>" + featureDefinition.GetTitle(cInfo) + "</b><br> "
+ featureDefinition.GetDescription(cInfo), featureDefinition.Id.ToString());
rdSiteFeatures.Items.Add(siteFeatureItem);
}
}
To populate the list of site definitions obtain a reference to the SPWebTemplateCollection of the current site definition via the GetWebTemplates method.
//Get the list of available Site Definitions / WebTemplates
foreach (SPWebTemplate webTemplate in SPContext.Current.Site.GetWebTemplates(SPContext.Current.RegionalSettings.LocaleId))
{
//Skip "GLOBAL", it has an ID of 0
if (webTemplate.ID != 0)
{
ListItem webTemplateItem = new ListItem("<b>" + webTemplate.Title + "</b> - ID: " + webTemplate.ID + " Name: " + webTemplate.Name + "<br/>" + webTemplate.Description,webTemplate.Name);
chkListSiteDefinitions.Items.Add(webTemplateItem);
}
}
Finally, you'll need to write the code that adds or removes the feature from each site. Use the CurrentItem of WebApplicationSelector to get a reference to the selected web application and the underlying site collections. Iterate through each site(web) checking the WebTemplate and Configuration properties of each against the list of selected site definitions. If the web template and configuration of the site are in the list of selected site definitions, call the Add method of SPWeb.Features to activate the feature and the Remove method to deactivate it based on the "action" the user selected.
//*************************************************************
// This method is called when the user clicks the "OK" button
// to activate the site feature.
protected void BtnSubmit_Click(object sender, EventArgs e)
{
//Prepare a string object to display the result
//of the feature activation for each site
string strMessages = "";
//Hide the form
inputForm.Visible = false;
//Get the GUID of the selected Feature to activate
Guid featureID = new Guid(rdSiteFeatures.SelectedValue);
try
{
//Iterate through each of the site collections
//in the selected web applications
foreach (SPSite site in Selector.CurrentItem.Sites)
{
//Disable the CatchAccessDeniedException
//of the site collection to avoid being redirected
//to the "Access Denied" page if access is denied.
site.CatchAccessDeniedException = false;
//Iterate through each site in the site collection
//and activate the selected feature
foreach (SPWeb web in site.AllWebs)
{
//Use a try statement to trap any errors that
//may occur during activation. Errors may occur
//due to feature dependencies, features that
//may already be active, permissions, etc.
//Consider Refactoring this code for better error
//handling
try
{
//Check if the site definition/configuration of the
//current site is included in the list of site definitions
//that the feature will be applied to.
ListItem item = chkListSiteDefinitions.Items.FindByValue(web.WebTemplate + "#" + web.Configuration.ToString());
if ((item != null) & (item.Selected))
{
//Activate the feature
if (lstAction.SelectedValue == "Activate")
{
web.Features.Add(featureID, chkForce.Checked);
strMessages += "<br />Feature Successfully Activated: " + web.Url + "<br />";
}
else
{
web.Features.Remove(featureID, chkForce.Checked);
strMessages += "<br />Feature Successfully Deactivated: " + web.Url + "<br />";
}
}
}
catch (Exception featureActivationError)
{
//if an error occurs during activation;
//capture the message to display it to the user
//after iterating through all the sites
strMessages += "<br />Feature Activation Error: " + web.Url + " ERROR:" + featureActivationError.Message + "<br />";
}
}
//Allow the site collection to continue handling
//access denied exceptions
site.CatchAccessDeniedException = true;
}
}
catch (Exception featureActivationError)
{
//if an error occurs write the error message to
//an error element
strMessages += featureActivationError.Message + "hhhhhh<br>";
}
}
<?xml version="1.0" encoding="utf-8" ?>
<Feature xmlns="http://schemas.microsoft.com/sharepoint/"
Id="4D4F4516-DDEC-4ea8-82F4-124D1B743984"
Title="Site Scoped Feature Management"
Hidden="FALSE"
Scope="Farm"
ActivateOnDefault="TRUE"
Version="12.0.0.0">
<ElementManifests>
<ElementManifest Location="Elements.xml" />
</ElementManifests>
</Feature>
<?xml version="1.0" encoding="utf-8" ?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
<CustomActionGroup
Id="BDDBB947-CBC6-47f7-B57A-6C8BEB0E19D7"
Location="Microsoft.SharePoint.Administration.ApplicationManagement"
Title="Utilities"
Sequence="1000"/>
<CustomAction
Id="E57ECD33-91FD-4fc3-A4E8-E1B932178CB4"
GroupId="BDDBB947-CBC6-47f7-B57A-6C8BEB0E19D7"
Location="Microsoft.SharePoint.Administration.ApplicationManagement"
Sequence="20"
Title="Site Scoped Feature Management"
Description="">
<UrlAction Url="_admin/SiteFeatureManagement.aspx"/>
</CustomAction>
</Elements>
Follow me on Twitter