Managing and storing custom configuration values in SharePoint

One of the things I frequently run into when developing custom SharePoint solutions is the need to store some form of configuration data. The traditional (ASP.NET) place to put this is in the web.config. However, this has some downsides. It’s difficult to maintain once deployed (and really, hand-editing config files sounds a bit old-fashioned anyway). Another downside is that, for Farm-wide settings, you’d need to duplicate the settings in each web.config. Fortunately, SharePoint comes with the useful SPPersistedObject.

Storing configuration data in SPPersistedObject

From http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.administration.sppersistedobject(office.12).aspx

The SPPersistedObject class provides a base class for all administration objects. It serializes all fields marked with the Persisted attribute to XML and writes the XML blob to the configuration database. The SPPersistedObject class contains code to serialize all its members that are base types, other persisted objects, and collections of persisted objects. Configuration data that is stored in persisted objects is automatically made available to every process on every server in the farm.

There are a lot of SharePoint classes that (indirectly) inherit from SPPersistedObject. Among them are SPFarm and SPWebApplication. Unfortunately, SPSite and SPWeb do not inherit from SPPersistedObject, though SPWeb has it’s own property-bag which can be used in a similar way.

SPFarm and SPWebApplication are just what we’re looking for.

The following is a very basic configuration helper:


public static class ConfigurationHelper
{
	public static object GetSetting(SPPersistedObject storage, string key)
	{
		if (storage.Properties.ContainsKey(key))
		{
			return storage.Properties[key];
		}
		else
		{
			return null;
		}
	}

	public static void SetSetting(SPPersistedObject storage, string key, object value)
	{
		if (!storage.Properties.ContainsKey(key))
		{
			storage.Properties.Add(key, value);
		}
		else
		{
			storage.Properties[key] = value;
		}
		storage.Update();
	}
}

Now this can be used to, for instance, get the connection string for our custom database:


object o = ConfigurationHelper.GetSetting(SPFarm.Local, "CustomDatabaseConnectionString");
if (o == null)
{
	throw new Exception("Custom database connection has not been configured.");
}
string connectionString = o.ToString();
// Do stuff ...

This all pretty nice of course, but it would be even better is there was an easy way to manage these settings. Like in Central Administration.

Creating the configuration pages for Central Administration

Creating the ASPX page

The easiest way to start is to copy one of the existing Central Administration pages. Keep in mind that you want to copy an Operations or Web Application page, depending on the level of the setting you are implementing. All the admin pages can be found in 12/TEMPLATE/ADMIN.

To stay with the connection string example, I’ll take an Operations page.


<%@ Page Language="C#" Inherits="CustomSharePointProject.CustomDatabaseSettingsPage" MasterPageFile="~/_admin/admin.master" %>

<!-- snip -->

<asp:content contentplaceholderid="PlaceHolderPageTitle" runat="server">
	<SharePoint:EncodedLiteral runat="server" text="Custom Database Settings" EncodeMethod='HtmlEncode'/>
</asp:content>
<asp:content contentplaceholderid="PlaceHolderPageTitleInTitleArea" runat="server">
	<SharePoint:EncodedLiteral runat="server" text="Custom Database Settings" EncodeMethod='HtmlEncode'/>
</asp:content>
<asp:content contentplaceholderid="PlaceHolderPageDescription" runat="server">
	<SharePoint:EncodedLiteral runat="server" text="Edit the database connection settings for the entire SharePoint Farm." EncodeMethod='HtmlEncodeAllowSimpleTextFormatting'/>
</asp:content>
<asp:content contentplaceholderid="PlaceHolderMain" runat="server">
	<table width="100%" class="propertysheet" cellspacing="0" cellpadding="0" border="0">
		<tr>
			<td class="ms-descriptionText">
				<asp:Label ID="Label1" runat="server" EnableViewState="False" class="ms-descriptionText" />
			</td>
		</tr>
		<tr>
			<td class="ms-error">
				<asp:Label ID="Label2" runat="server" EnableViewState="False" />
			</td>
		</tr>
		<tr>
			<td class="ms-descriptionText">
				<asp:ValidationSummary ID="ValidationSummary1" HeaderText="<%$SPHtmlEncodedResources:spadmin, ValidationSummaryHeaderText%>"
					DisplayMode="BulletList" ShowSummary="True" runat="server"></asp:ValidationSummary>
				<asp:Label runat="server" ID="lblConnectionTestResult" />
			</td>
		</tr>
		<tr>
			<td>
				<img src="/_layouts/images/blank.gif" width="10" height="1" alt="" />
			</td>
		</tr>
	</table>

	<table border="0" cellspacing="0" cellpadding="0" class="ms-propertysheet" width="100%">
		<wssuc:inputformsection title="Database Connection Settings" runat="server">
			<Template_Description>
				<SharePoint:EncodedLiteral runat="server" text="Custom Database Connection String." EncodeMethod='HtmlEncodeAllowSimpleTextFormatting'/>
			</Template_Description>
			<Template_InputFormControls>
				<wssuc:InputFormControl runat="server" LabelText="Connection String">
					<Template_control>
						<wssawc:InputFormTextBox Title="<%$Resources:spadmin, defaultcontentdb_dbservertext%>" class="ms-input" ID="txtConnectionString" Columns="50" Runat="server" MaxLength="256" size="25" />
						<wssawc:InputFormRequiredFieldValidator
							ControlToValidate="txtConnectionString"
							ErrorMessage="<%$Resources:spadmin, defaultcontentdb_dbservererror%>"
							Runat="server"
						/>
						</Template_control>
				</wssuc:InputFormControl>
			</Template_InputFormControls>
		</wssuc:inputformsection>
		<wssuc:buttonsection runat="server">
			<Template_Buttons>
				<asp:Button UseSubmitBehavior="false" OnClick="btnSubmit_Click" runat="server" class="ms-ButtonHeightWidth" Text="<%$Resources:wss,multipages_okbutton_text%>" id="btnSubmit" accesskey="<%$Resources:wss,okbutton_accesskey%>"/>
			</Template_Buttons>
		</wssuc:buttonsection>
	</table>
</asp:content>

The first 2/3rd of the page is to make it fit right into Central Administration by having the same look and feel. The interesting part comes with wssuc:inputformsection, which is where the configuration form goes. It’s all pretty straightforward. Just look at some of the standard Central Admin pages to get an idea of what’s possible.

To get/set the actual configuration value, a code-behind page needs to be created. Based on, in this case,  Microsoft.SharePoint.ApplicationPages.OperationsPage. Configuration pages targeted at Web Applications should inherit from Microsoft.SharePoint.ApplicationPages.ApplicationsManagementPage.


public class CustomDatabaseSettingsPage : Microsoft.SharePoint.ApplicationPages.OperationsPage
{
	private TextBox ConnectionStringTextBox
	{
		get
		{
			// Return a reference to the TextBox on the ASPX page using FindControl and what not ...
		}
	}

	protected override void OnLoad(EventArgs e)
	{
		base.OnLoad(e);
		
		if (!IsPostBack)
		{
			ConnectionStringTextBox.Text = ConfigurationHelper.GetSetting(SPFarm.Local, "CustomDatabaseConnectionString");
		}
	}

	public void btnSubmit_Click(object sender, EventArgs e)
	{
		if (Page.IsValid)
		{
			ConfigurationHelper.SetSetting(SPFarm.Local, "CustomDatabaseConnectionString", ConnectionStringTextBox.Text);
			RedirectToOperationsPage();
		}
	}
}

So that’s it as far as managing the settings goes. In order for all this to work you’ll need to add references to 12/CONFIG/BIN/Microsoft.SharePoint.ApplicationPages.dll and 12/CONFIG/ADMINBIN/Microsoft.SharePoint.ApplicationPages.Administration.dll. Note that these DLLs do not allow partially trusted callers.

Adding items to the Central Administration menu

To finish it all off we add some nice links to the Central Administration menu. This is done using the CustomAction nodes in the elements.xml file of your Feature.


<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
	<CustomActionGroup
		Id="{123C97F8-CCF8-4529-9999-31E9532FD910}"
		Location="Microsoft.SharePoint.Administration.Operations"
		Sequence="100"
		Title="Custom Project SharePoint Administration"
	/>
	<CustomAction
		Id="{56CC763E-7469-427b-B224-B56417566C82}"
		GroupId="{123C97F8-CCF8-4529-9999-31E9532FD910}"
		Location="Microsoft.SharePoint.Administration.Operations"
		Sequence="10"
		Title="Custom Database Settings"
		Description="Manage the connection string for the custom database."
	>
		<UrlAction Url="/_admin/CustomProject/CustomDatabaseSettings.aspx" />
	</CustomAction>
</Elements>

To also include the page in the breadcrumb, a sitemap XML file needs to be created and added it to the 12/TEMPLATE/ADMIN folder. This file must follow the following naming convention: admin.sitemap.[your-name-here].xml .


<?xml version="1.0" encoding="utf-8" ?>
<siteMap>
  <siteMapNode
	  title="Custom Database Settings"
	  url="/_admin/CustomProject/CustomDatabaseSettings.aspx"
	  parentUrl="/_admin/operations.aspx"
	/>
</siteMap>

In order for the menu to be properly updated, your feature must include a feature receiver which calls the following methods:

// This is needed to make sure the menus on the applications.aspx and operations.aspx pages contain our new custom actions.
SPFarm.Local.Services.GetValue<SPWebService>().ApplyApplicationContentToLocalServer();
// Force the sitemap to be regenerated so the central administration breadcrumb appears at the top of our admin pages.
SPWebService.AdministrationService.ApplyApplicationContentToLocalServer(); 

One final note: You’ll notice the “local” part in ApplyApplicationContentToLocalServer. This can cause some problems in Farms with multiple servers. Read more about it in Sean McDonough’s article The ApplyApplicationContentToLocalServer Method and Why It Comes Up Short

This entry was posted in .NET and tagged , , . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *

*