Using the SPPropertyBag with Custom Admin Pages in SharePoint

A couple of months ago, I started a MOSS 2007 project which was intended as a collaborative environment. In some cases, we wanted to read data from other data sources, one of them being a DB2 UDB, and display it in SharePoint. In order to successfully connect to a database you need a connection string, and you need to store it securely since it usually contains the required credentials to access the DB.

In this post I will explain how we can use SharePoint’s SPPropertyBag to securely store strings of information and how we can use admin pages to interact with the data from within the SharePoint 2007 web interface.

This is what we are trying to achieve:

MOSS Admin Area: Custom Settings Page

What we see here is an application page in the site/web adminitration area allowing admins to store web/application-related information.

You can download the source code for the application page, the required Feature to enable this functionality and the complete code for the SPProperties type from here: Source Code: SPProperties Article
Note: This is a Visual Studio 2008 Solution, get Microsoft Visual Studio 2008 Express from here.

Prerequisites

So, where do you store your (connection) strings then? I usually like the web.config approach with a connectionString key, but that’s not very secure, it’s as secure as the server hosting the web.config file. Why don’t we put it in the same DB SharePoint writes its content to? Great idea, but how do I access the DB which I do not know the structure of? BTW, messing around SharePoint content DBs is a big no-no! This is where SPPropertyBag comes in very handy.

The SPPropertyBag Type

Every SharePoint site (represented by the SPWeb type in the object model) is associated a Properties property of type SPPropertyBag with the site’s settings such as the version of Windows SharePoint Services and the language locale.

We can use a site’s SPPropertyBag to store any site-related information as we’ll see shortly. To obtain the site’s Properties simply use the following syntax:

SPWeb spWeb = (new SPSite("http://dev.local")).OpenWeb();
SPPropertyBag spProperties = spWeb.Properties;
 
foreach (DictionaryEntry entry in spProperties)
  Console.WriteLine("   {0,-25} {1}", entry.Key, entry.Value);

As you can see from the code an SPProperyBag implements the StringDictionary interface which implements a hash table with the key and the value strongly typed to be strings rather than objects. That simply means, we can add new items by specifying the key and value as strings, illustrated in the following code snippet:

private void SetValue(String key, String value, String siteUrl) {
  SPWeb spWeb = (new SPSite("http://dev.local")).OpenWeb();
  SPPropertyBag spProperties = spWeb.Properties;
 
  if (spProperties.ContainsKey(key)) {
    spProperties[key] = Value;
  } else {
    spProperties.Add(Key, Value);
  }
 
  spProperties.Update();
}

This method adds or sets an item in the Properties property of the site specified by siteUrl. The call to Update causes SharePoint to persist the changes to the database. Normally, you can obtain a reference to sites (the site collection’s root web site, for instance) using a call similar to these:

// gets the site collection's root web site
SPWeb spWeb = SPContext.Current.Site.RootWeb
// gets the current web site from the request's context
Web = SPControl.GetContextWeb(Context);

.

The Helper Assembly

Storing settings is a requirement almost every application must fulfill and everytime again, developers and software architects must decide where to store application settings. We can choose from many options: databases, XML files, resource files and binary data, to name a few.

Getting and setting application data is a common operation, independent of the technology used to persist the data. It is therefore desirable to create a reusable and extensible approach to the problem: storage and retrieval of application data without taking into account the persisting technology.

Let’s create an abstract class Settings which will be the base class for all the different solutions to the problem statement. TheSettings class defines the two self-descriptive methods ExistsKey and GetValue. The indexer allows for simple access to the collection using string values as keys:

using System;
using System.Reflection;
 
namespace SG.Global {
  public abstract class Settings {
    protected Settings() { }
 
    public abstract bool ExistsKey(String key);
    protected abstract String GetValue(String key);
 
    public String this[String key] {
      get { return this.GetValue(key); }
    }
  }
}

Based on this class we can derive many types, depending on the different data stores. We could, for example, inherit from Settings to allow reading and writing config file settings:

public class ConfigFileSettings : SG.Global.Settings {
  public override bool Existskey(String key) {
    return (ConfigurationSettings.AppSettings[key] != null);
  }
 
  protected override String GetValue(String key) {
    return ConfigurationSettings.AppSettings[key];
  }
}

Next, we need to derive from Settings in order to implement the SharePoint version for reading and writing SPWeb properties:

using System;
using System.Collections.Generic;
using System.Text;
 
using Microsoft.SharePoint;
using Microsoft.SharePoint.Utilities;
 
namespace SG.Global.SharePoint {
  public class SPProperties : SG.Global.Settings {
    private SPPropertyBag spProperties;
    private SPWeb webCurrent;
 
    public SPProperties(SPWeb webCurrent) : base() {
      this.webCurrent = webCurrent;
      spProperties = this.webCurrent.Properties;
    }
 
    public override bool ExistsKey(String key) {
      return spProperties.ContainsKey(key);
    }
 
    protected override String GetValue(String key) {
      if (ExistsKey(key)) {
        return (String)spProperties[key];
      } else {
        return String.Empty;
      }
    }
 
    public void SetValue(String key, String value) {
      if (ExistsKey(key)) {
        spProperties[key] = value;
      } else {
        spProperties.Add(key, value);
      }
 
      spProperties.Update();
    }
 
    public void DeleteKey(String key) {
      spProperties.Remove(key);
      spProperties.Update();
    } 
 
    public SPWeb CurrentWeb {
      get { return webCurrent; }
      set { webCurrent = value; }
    }
  }
}

The constructor takes an SPWeb argument and the methods GetValue, SetValue, DeleteKey, ExistsKey to what you think they do: getting, setting, deleting items and checking for their existence.

So far, so good, but what does all this have to do with strings of information and admin pages? Glad you asked!

The Admin Pages

Whenever you click the Site Settings menu item in the Site Actions menu you’re entering the backend area. The following screenshot shows the admin area of a typical Web within a site collection.

MOSS Admin Area

Please note the last link My Custom Settings in the column Site Collection Administration. Klicking it will open a custom admin page similar to the one you’ve seen at the beginning of this article.

The Page Structure

Creating custom admin pages is not very difficult and I recommend starting with an existing page from the Layouts directory. All application pages have a similar structure and allow adding and removing controls as you wish. To avoid having to deal with the layout of these admin pages SharePoint encapsulates all the supported controls in assemblies and user controls you can reference and use in your code the same way you do in traditional ASP.NET development.

The following graphics illustrates the basic structure of such an administration page:

MOSS Admin Page: Structure

Let’s take a look at the source code and compare the graphics to the code (some elements removed for clarity):

<%@ [directives removed for brevity] %>
 
<script runat="server">
    /* your code */
</script>
 
<asp:Content ID="Content2" ContentPlaceHolderID="PlaceHolderPageTitle" runat="server">
    <SharePoint:EncodedLiteral runat="server" Text="My Portal Settings" EncodeMethod='HtmlEncode' />
</asp:Content>
 
<asp:Content ID="Content3" ContentPlaceHolderID="PlaceHolderPageTitleInTitleArea" runat="server">
    <SharePoint:EncodedLiteral runat="server" Text="My Portal Settings" EncodeMethod='HtmlEncode' />
</asp:Content>
 
<asp:Content ID="contentMain" ContentPlaceHolderID="PlaceHolderMain" runat="server">
    <table class="ms-settingsframe">
        <tr>
            <td width="100%" colspan="4" style="padding-top: 0px;">
                <table class="ms-pageinformation">
                    <tr>
                        <SharePoint:EncodedLiteral ID="EncodedLiteral1" />
                        <SharePoint:EncodedLiteral ID="EncodedLiteral2" />
                    </tr>
                </table>
            </td>
        </tr>
        <tr>
            <td>
                <table class="ms-propertysheet">
                    <wssuc:InputFormSection Title="Global Settings" id="ifmSQL" Description="...">
                        <template_inputformcontrols>                        
                            <wssuc:InputFormControl LabelText="Domain (NETBIOS)" runat="server">
                                <Template_Control />
                            </wssuc:InputFormControl>
                            <wssuc:InputFormControl
                                LabelText="Error Logging" runat="server" Description="...">
                                <Template_Control />
                            </wssuc:InputFormControl>   
                        </template_inputformcontrols>
                    </wssuc:InputFormSection>
                    <wssuc:InputFormSection Title="Exchange Server Settings"
                        id="ifmExchange" Description="..." runat="server">
                        <template_inputformcontrols>                        
                            <wssuc:InputFormControl
                                LabelText="Exchange Server OWA Address" runat="server">
                                <Template_Control />
                            </wssuc:InputFormControl>   
                        </template_inputformcontrols>
                    </wssuc:InputFormSection>
                    <SPSWC:InputFormButtonSection runat="server">
                        <SPSWC:InputFormButtonAtBottom
                            runat="server" ID="cmdOK" OnClick="OnClickOK" />
                        <SPSWC:InputFormButtonAtBottom
                            runat="server" ID="cmdCancel" OnClick="OnClickCancel" />
                    </SPSWC:InputFormButtonSection>
                </table>
            </td>
        </tr>
    </table>
</asp:Content>

As you can see, we do not need to specify additional styles or other elements to create pages following SharePoint’s default design. Everything is neatly hidden in user and server controls.

Page Layout by Dissection

Depending on your needs we’ll have to register several tag prefixes and import some namespaces:

wssuc
The wssuc tag prefix is used for all user controls being imported from the filesystem (12\TEMPLATE\ControlTemplates). In this example, the three controls InputFormSection.ascx, InputFormControl.ascx and ButtonSection.ascx are responsible for rendering the page layout and used in all admin pages, so you’ll most likely need them quite often.
wssawc
The wssawc tag prefix is not shown in the code example above but you’ll need it for all interactive controls like textboxes and buttons. In our custom admin page all textboxes, checkboxes and buttons are referenced through this prefix. We will see a fully functioning example shortly.
SPSWC
The SPSWC tag prefix identifies controls provided by MOSS via namespace Microsoft.SharePoint.Portal.WebControls. It is used for page layout and interactive components as well.
SharePoint
The SharePoint tag prefix is from Microsoft.SharePoint.WebControls and the most common one. You’ll find lots of controls from this namespace all over WSS 3.0 and MOSS 2007, in master pages, custom controls, etc.

The <script> block is the right place for our code. Following the script block we can identify two very common content place holders, namely PlaceHolderPageTitle and PlaceHolderPageTitleInTitleArea, the former sets the browser window’s title and the latter sets the title for the admin page as we can see in the figure above.

Various controls are responsible for the correct layout and assembly of appropriate HTML code. The first wssuc:InputFormSection renders the following HTML:

<tr id="ctl00_PlaceHolderMain_ifmSQL">
  <td valign="top" class="ms-descriptiontext">
    <table width="100%" cellspacing="0" cellpadding="1" border="0">
      <tbody>
        <tr>
          <td valign="top" height="22" style="padding-top: 4px;" class="ms-sectionheader">
            <h3 class="ms-standardheader">Global Settings</h3>
          </td>
        </tr>
        <tr>
          <td class="ms-descriptiontext ms-inputformdescription">
            Logging and permissions for i5 users
          </td>
          <td><img width="8" height="1" alt="" src="/_layouts/images/blank.gif"/></td>
        </tr>
        <tr>
          <td><img width="150" height="19" alt="" src="/_layouts/images/blank.gif"/></td>
        </tr>
      </tbody>
    </table>
  </td>
  <td valign="top" align="left" class="ms-authoringcontrols ms-inputformcontrols">
    <table width="100%" cellspacing="0" cellpadding="0" border="0">
      <tbody>
        <tr>
          <td width="9"><img width="9" height="7" alt="" src="/_layouts/images/blank.gif"/></td>
          <td><img width="150" height="7" alt="" src="/_layouts/images/blank.gif"/></td>
          <td width="10"><img width="10" height="1" alt="" src="/_layouts/images/blank.gif"/></td>
        </tr>
        <tr>
          <td></td>
          <td class="ms-authoringcontrols">
            <table width="100%" cellspacing="0" cellpadding="0" border="0" class="ms-authoringcontrols">
              <tbody>
                <tr id="ctl00_PlaceHolderMain_ifmSQL_ctl00_tablerow1">
                  <td nowrap="nowrap" colspan="2" class="ms-authoringcontrols">
                    <span id="ctl00_PlaceHolderMain_ifmSQL_ctl00_LiteralLabelText">
                      Domain (NETBIOS)
                    </span>
                  </td>
                </tr>
                <tr id="ctl00_PlaceHolderMain_ifmSQL_ctl00_tablerow2">
                  <td><img width="1" height="3" alt="" src="/_layouts/images/blank.gif"/></td>
                </tr>
                <!-- End Right_Text -->
                <tr id="ctl00_PlaceHolderMain_ifmSQL_ctl00_tablerow3">
                  <td width="11">
                    <img width="11" height="1" alt="" src="/_layouts/images/blank.gif"/>
                  </td>
                  <td width="99%" class="ms-authoringcontrols">
                    <input type="text" alwaysenablesilent="true" class="ms-input"
                        id="ctl00_PlaceHolderMain_ifmSQL_ctl00_tbDomain" size="60"
                        name="ctl00$PlaceHolderMain$ifmSQL$ctl00$tbDomain"/>
                    <span style="color: Red; display: none;"
                        id="ctl00_PlaceHolderMain_ifmSQL_ctl00_reqDomain">
                      <br/>Required
                    </span>                 
                  </td>
                </tr>
                <tr id="ctl00_PlaceHolderMain_ifmSQL_ctl00_tablerow5">
                  <td><img width="1" height="6" alt="" src="/_layouts/images/blank.gif"/></td>
                </tr>
                <tr id="ctl00_PlaceHolderMain_ifmSQL_ctl01_tablerow1">
                  <td nowrap="nowrap" colspan="2" class="ms-authoringcontrols">
                    <span id="ctl00_PlaceHolderMain_ifmSQL_ctl01_LiteralLabelText">Error Logging</span>
                  </td>
                </tr>
                <tr id="ctl00_PlaceHolderMain_ifmSQL_ctl01_tablerow2">
                  <td><img width="1" height="3" alt="" src="/_layouts/images/blank.gif"/></td>
                </tr>
                <!-- End Right_Text -->
                <tr id="ctl00_PlaceHolderMain_ifmSQL_ctl01_tablerow3">
                  <td width="11">
                    <img width="11" height="1" alt="" src="/_layouts/images/blank.gif"/>
                  </td>
                  <td width="99%" class="ms-authoringcontrols"></td>
                </tr>
                <tr><td class="ms-formspacer" /></tr>
                <tr>
                  <td class="ms-formspacer">
                    <img width="10" src="/_layouts/images/trans.gif" alt=""/>
                  </td>
                  <td width="100%" class="ms-authoringcontrols">
                    <table cellspacing="0" cellpadding="0" class="ms-authoringcontrols">
                      <tbody>
                        <tr>
                          <td valign="top" nowrap="nowrap">
                            <span class="ms-input">
                              <input type="checkbox" name="ctl00$PlaceHolderMain$ifmSQL$ctl01$cbxLogging"
                                  id="ctl00_PlaceHolderMain_ifmSQL_ctl01_cbxLogging"/>
                            </span>
                          </td>
                          <td>
                            <label for="ctl00_PlaceHolderMain_ifmSQL_ctl01_cbxLogging">
                              Globally Enable Error Logging?
                            </label>
                          </td>
                        </tr>
                      </tbody>
                    </table>
                  </td>
                </tr>
                <tr id="ctl00_PlaceHolderMain_ifmSQL_ctl01_tablerow5">
                  <td><img width="1" height="6" alt="" src="/_layouts/images/blank.gif"/></td>
                </tr>
              </tbody>
            </table>
          </td>
          <td width="10"><img width="10" height="1" alt="" src="/_layouts/images/blank.gif"/></td>
        </tr>
        <tr>
          <td></td>
          <td><img width="150" height="13" alt="" src="/_layouts/images/blank.gif"/></td>
          <td></td>
        </tr>
      </tbody>
    </table>
  </td>
</tr>

Wow, that’s a pile of HTML code, isn’t it. With minimal effort we can leverage the power of SharePoint without having to worry about styles and layout.

To create a completely funtional page for our own purpose we need the following:

wssuc:InputFormSection
An InputFormSection provides the basic layout for each section, separated by a thin gray line. We will create two sections: one for the Global Settings and another one for the Exchange Server Settings
wssuc:InputFormControl
The wssuc:InputFormControl is the container for the actual SharePoint-specific controls. It renders a label and allows multiple controls in a single container. In our case we create a new wssuc:InputFormControl container for each InputFormControl.
wssawc:InputFormTextBox
A textbox similar to the traditional ASP.NET component. It’s not augmented with additional functionality.
wssawc:InputFormRequiredFieldValidator
SharePoint’s specially crafted RequiredFieldValidator. In our example every input control is being validated by a properly configured wssawc:InputFormRequiredFieldValidator.
SPSWC:InputFormCheckBox
A checkbox for use within wssuc:InputFormControl containers.
SPSWC:InputFormButtonSection
Renders a button container for the page’s OK and Cancel buttons.
SPSWC:InputFormButtonAtBottom
Renders a simple button to do the page post-back or other actions, depending on the OnClick property setting.

Building the Page

Now that we have identified what is needed to create our admin page, we can try assemble it. C# code is added later.

<%@ Page Language="C#" MasterPageFile="~/_layouts/application.master" %>
 
<%@ 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" %>
<%@ Register TagPrefix="wssawc" Namespace="Microsoft.SharePoint.WebControls"
  Assembly="Microsoft.SharePoint, Version=12.0.0.0,
  Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="SPSWC" Namespace="Microsoft.SharePoint.Portal.WebControls"
  Assembly="Microsoft.SharePoint.Portal, Version=12.0.0.0,
  Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ 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="SG" Namespace="SG.Global.SharePoint"
  Assembly="Test.SPProperties, Version=1.0.0.0,
  Culture=neutral, PublicKeyToken=fb3603850412338a" %>
 
<%@ Import Namespace="Microsoft.SharePoint" %>
<%@ Import Namespace="System.Xml" %>
<%@ Import Namespace="SG.Global.SharePoint" %>
 
<script runat="server">
  /* put code here */
</script>
 
<asp:Content ID="Content2" ContentPlaceHolderID="PlaceHolderPageTitle" runat="server">
  <SharePoint:EncodedLiteral runat="server" Text="My Portal Settings" EncodeMethod='HtmlEncode' />
</asp:Content>
 
<asp:Content ID="Content3" ContentPlaceHolderID="PlaceHolderPageTitleInTitleArea" runat="server">
  <SharePoint:EncodedLiteral runat="server" Text="My Portal Settings" EncodeMethod='HtmlEncode' />
</asp:Content>
 
<asp:Content ContentPlaceHolderID="PlaceHolderPageImage" runat="server">
</asp:Content>
 
<asp:Content ID="contentMain" ContentPlaceHolderID="PlaceHolderMain" runat="server">
  <style type="text/css">
    table.ms-propertysheet {
      height: 100%;
    }
  </style>
  <table cellspacing="0" cellpadding="0" border="0" class="ms-settingsframe">
    <tr>
      <td width="100%" colspan="4" style="padding-top: 0px;">
        <table class="ms-pageinformation" width="100%" cellpadding="0" cellspacing="0">
          <tr>
            <td valign="top" style="padding: 10px;" width="100%" height="100px">
              <table height="100%" width="100%" id="idItemHoverTable">
                <tr>
                  <th scope="col" colspan="2" style="padding-bottom: 8px;">
                    <span class="ms-linksectionheader">
                      <h3 class="ms-standardheader">
                        <SharePoint:EncodedLiteral
                          ID="EncodedLiteral1"
                          runat="server"
                          Text="Admin page for setting custom preferences"
                          EncodeMethod='HtmlEncode' />
                      </h3>
                    </span>
                  </th>
                </tr>
                <tr>
                  <th scope="row" nowrap="nowrap">
                    <SharePoint:EncodedLiteral
                      ID="EncodedLiteral2"
                      runat="server" Text="<% $Resources:wss,settings_siteurl %>"
                      EncodeMethod='HtmlEncode' />:
                  </th>
                  <td dir="ltr">
                    <% SPHttpUtility.HtmlEncode(Web.Url + "/", Response.Output); %>
                  </td>
                </tr>
              </table>
            </td>
          </tr>
        </table>
      </td>
    </tr>
    <tr>
      <td valign="top" style="padding: 4px 0px 4px 0px;" height="100%">
        <table width="100%" border="0" cellspacing="0" cellpadding="0" class="ms-propertysheet">
          <wssuc:InputFormSection Title="Global Settings" id="ifmSQL"
            Description="Logging and permissions for i5 users" runat="server">
            <template_inputformcontrols>            
             <wssuc:InputFormControl LabelText="Domain (NETBIOS)" runat="server">
              <Template_Control>
                <wssawc:InputFormTextBox CssClass="ms-input"
                  ID="tbDomain" Runat="server" Columns="60" />
                <wssawc:InputFormRequiredFieldValidator ID="reqDomain"
                  ControlToValidate="tbDomain" Text="Error" runat="server"
                  ErrorMessage ="Required" EnableClientScript="true" Display="Dynamic">
                </wssawc:InputFormRequiredFieldValidator>
              </Template_Control>
            </wssuc:InputFormControl>
            <wssuc:InputFormControl LabelText="Error Logging" runat="server"
              Description="Globally Enable Logging">
              <Template_Control>
                <SPSWC:InputFormCheckBox CssClass="ms-input" ID="cbxLogging"
                  runat="server" Text="Globally Enable Error Logging?" />
              </Template_Control>
            </wssuc:InputFormControl> 
            </template_inputformcontrols>
          </wssuc:InputFormSection>
          <wssuc:InputFormSection Title="Exchange Server Settings" id="ifmExchange"
            Description="Settings for communication with the Exchange Server" runat="server">
            <template_inputformcontrols>            
              <wssuc:InputFormControl LabelText="Exchange Server OWA Address" runat="server">
                <Template_Control>
                  <wssawc:InputFormTextBox CssClass="ms-input"
                    ID="tbExchangeServerHostname" Runat="server" Columns="60" />
                  <wssawc:InputFormRequiredFieldValidator ID="reqHostName"
                    ControlToValidate="tbExchangeServerHostname" Text="Error"
                    runat="server" ErrorMessage ="Required"
                    EnableClientScript="true" Display="Dynamic">
                  </wssawc:InputFormRequiredFieldValidator>
                </Template_Control>
              </wssuc:InputFormControl> 
            </template_inputformcontrols>
          </wssuc:InputFormSection>
          <SPSWC:InputFormButtonSection runat="server">
            <SPSWC:InputFormButtonAtBottom runat="server" ID="cmdOK" OnClick="OnClickOK"
              TextLocId="Page_OkButton_Text" />
            <SPSWC:InputFormButtonAtBottom runat="server" ID="cmdCancel" OnClick="OnClickCancel"     
              TextLocId="Page_CancelButton_Text" CausesValidation="false" />
          </SPSWC:InputFormButtonSection>
        </table>
      </td>
    </tr>
  </table>
</asp:Content>

For now, we have an admin page with all required fields and controls but without any functionality because the OK button does not work and all user input is dicarded. It is time to get our SPProperties class back into play.

To get it working, we have to register the assembly by adding the following directive to the aspx file:

<%@ Register TagPrefix="SG" Namespace="SG.Global.SharePoint"
  Assembly="Test.SPProperties, Version=1.0.0.0,
  Culture=neutral, PublicKeyToken=fb3603850412338a" %>

Make sure to adjust the Namespace and Assembly properties to fit your needs.

Next, we have to write some code to handle the button events and to load/store data. Whenever the page is called we use the Load event to retrieve all data, if any. Clicking on the OK button invokes the OnClickOK event handler which persists all user data to database. See below how this is done:

<script runat="server">
  SPWeb Web;
 
  protected override void OnLoad(EventArgs e) {
    base.OnLoad(e);
    Web = SPControl.GetContextWeb(Context);
 
    if (!Page.IsPostBack) {
      SPProperties settings = new SPProperties(Web);
 
      tbDomain.Text = settings["Domain"];
      cbxLogging.Checked = 
        (String.IsNullOrEmpty(settings["Logging"]) ? 
        false :
        Convert.ToBoolean(settings["Logging"]));
      tbExchangeServerHostname.Text = settings["ExchangeServerAddress"];
 
      settings = null;
    }
  }
 
  protected void OnClickOK(Object Sender, EventArgs e) {
    SPProperties settings = new SPProperties(Web);
 
    settings.SetValue("Domain", tbDomain.Text);
    settings.SetValue("Logging", cbxLogging.Checked.ToString());
    settings.SetValue("ExchangeServerAddress", tbExchangeServerHostname.Text);
    settings = null;
 
    Response.Redirect("../Settings.aspx");
  }
 
  protected void OnClickCancel(Object Sender, EventArgs e) {
    Response.Redirect("../Settings.aspx");
  }
</script>

When we load the page for the first time after feature activation, no data is read and accessing the SPProperties internal StringDictionary returns String.Empty for non-existing keys. The OnClickOK event handler reads user input just as we are used to from traditional ASP.NET development. SPProperties‘ method SetValue takes two Strings: the first argument is the key to identify the slot in the collection and the second is the value being associated with the key. As soon as our work is done, we redirect to SharePoint’s Settings.aspx page.

On page load, we obtain a reference to the current web and instantiate a new SPProperties object. All we have to do is parsing the values and assign them to the appropriate controls. Remember that only Strings are stored so that we have to utilize Convert.ToXXX in order to retrieve a boolean value for the checkbox.

Make sure that you did not forget to associate event handlers with the button controls by properly setting the OnClick property:

<SPSWC:InputFormButtonSection runat="server">
<SPSWC:InputFormButtonAtBottom runat="server" ID="cmdOK" OnClick="OnClickOK"
  TextLocId="Page_OkButton_Text" />
<SPSWC:InputFormButtonAtBottom runat="server" ID="cmdCancel" OnClick="OnClickCancel"     
  TextLocId="Page_CancelButton_Text" CausesValidation="false" />
</SPSWC:InputFormButtonSection>

We’re done! The last step involves provisioning the page as a feature.

Deploying the Page

Ideally, you would want to create a SharePoint Feature for your project which is responsible for deploying the page.

Creating a Feature

We need two files for the Feature: Feature.xml and Elements.xml. The first describes the Feature’s characteristics, such as title or scope and the second file specifies actions and auxiliary files belonging to the Feature.

To create a Feature, make a new directory in ..\TEMPLATES\Features folder within the 12 hive. Give it a unique name like MyCustomAdminPage (alternatively, make a new sub folder ‘Test’ and keep your test Features there) and create a Feature.xml file within this directory:

<?xml version="1.0" encoding="utf-8"?>
<Feature  Id="4E35B4E5-D0E0-44c2-9BE4-F2160A3D476C"
          Title="Custom Settings"
          Description="Admin page for setting custom preferences"
          Version="12.0.0.0"
          Scope="Site"
          xmlns="http://schemas.microsoft.com/sharepoint/">
  <ElementManifests>
    <ElementManifest Location="Elements.xml" />
    <ElementFile Location="Pages/MyCustomSettings.aspx"/>
  </ElementManifests>
</Feature>

Don’t forget to create a new GUID to make sure it is globally unique. The ElementManifests container tells SharePoint that the details about the Feature are located in Elements.xml and that we want another file to be provisioned, namely MyCustomSettings.aspx

Next, create the Elements.xml file in the same Feature folder:

<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <CustomAction
    Id="MyPortal.MyCustomSettingsAction"
    Location="Microsoft.SharePoint.SiteSettings"
    GroupId="SiteCollectionAdmin"
    Title="My Custom Settings"
    Sequence="1001"
    Rights="ManageWeb"
    Description="Admin page for setting custom preferences"
    ImageUrl="~sitecollection/_layouts/images/lg_ICCONFIG.gif">
    <UrlAction Url ="~sitecollection/_layouts/__Test/MyCustomSettings.aspx"/>
  </CustomAction>
</Elements>

Id is a free-form string which must be unique in the farm. Location tells SharePoint where we want the custom action to be applied, here: Microsoft.SharePoint.SiteSettings. Depending on the Location value GroupId specifies the column of the Site Settings page (Site Actions — Site Settings — Modify all Site Settings). The following groups exist by default in the SiteSettings area: UsersAndPermissions, Customization, QuickLaunch, Galleries, SiteAdministration and SiteCollectionAdmin.

For Location chose from one of these:

  • Microsoft.SharePoint.ContentTypeTemplateSettings
  • Microsoft.SharePoint.ContentTypeSettings
  • Microsoft.SharePoint.Administration.ApplicationCreated
  • Office.Server.ServiceProvider.Administration (Shared Services/SSP links)
  • Microsoft.SharePoint.ListEdit.DocumentLibrary
  • Microsoft.SharePoint.Workflows
  • NewFormToolbar
  • DisplayFormToolbar
  • EditFormToolbar
  • Microsoft.SharePoint.StandardMenu (SiteActions menu)
  • Mcrosoft.SharePoint.Create (_layouts/create.aspx – the screen used to specify what you want to create on your site)
  • Microsoft.SharePoint.ListEdit (the screen used to edit the properties of a list item)
  • EditControlBlock

The UrlAction element represents the URL for the link being displayed in the SiteCollectionAdmin group.

Please note that I usually put my test Features, controls and application pages in sub folders below TEMPLATE\Features, TEMPLATE\Layouts and TEMPLATE\ControlTemplates usually called __Test to keep all shortliving files in a single place. Please adjust the paths in the examples appropriately to meet your preferences.

Finally, we have to create our MyCustomSettings.aspx and place it in the Pages directory below the feature base dir just as described in the Location attribute in Feature.xml. Additionally copy MyCustomSettings.aspx in the TEMPLATE\Layouts folder on the web front-end if you don’t plan to use a Solution for deployment.

Copy the complete Feature directory into the TEMPLATE\Features folder and invoke the following commands to install and activate the feature for you web. Open a console window and enter:

  1. cd C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\BIN
  2. stsadm -o installfeature -name __Test/MyCustomAdminPage
  3. stsadm -o activatefeature -name __Test/MyCustomAdminPage -url http://dev.local
  4. iisreset /timeout:0

The url parameter is the URL of the site collection we want to activate the feature for. Navigate to yor web’s settings page and locate you custom admin page to verify that everything has been deployed properly.

Summary

In this lesson you’ve learned two things:

  • how to facilitate the SPWeb.Properties data structure to securely store web-specific data in your web app’s content database
  • how to create custom admin pages for your web applications.

The MOSS 2007 SDK documentation provides useful insights but don’t expect any help about how to do things. Look out for fresh MSDN and TechNet articles instead. Anyhow, taking advantage of application pages in administrative areas is extremely helpful and a flexible, highly secure solution for persisting per-web information to the SharePoint content DB.

To get started download the code for the SPProperties class and the custom admin page: Source Code: SPProperties Article
Note: This is a Visual Studio 2008 Solution, get Microsoft Visual Studio 2008 Express from here.

Feel free to post your ideas to improve this article. Thanks for reading.







27 Responses to “Using the SPPropertyBag with Custom Admin Pages in SharePoint”

Great post! Thank you, Steve.

Christos added these pithy words on Jan 12 08 at 12:05

Nice post.

You should add a FormDigest web control to your page to ensure security is not breached. Check the Microsoft.SharePoint.FormDigest class for details.

AndersR added these pithy words on Apr 02 08 at 10:18

[...] Using the SPPropertyBag with Custom Admin Pages in SharePoint  [...]

SharePoint, SharePoint and stuff : SharePoint Kaffeetasse #63 added these pithy words on May 08 08 at 10:54

Great post. I have been looking how to do this for a while. Keep up the good WSS/MOSS posts.

LeeFAR added these pithy words on May 08 08 at 19:23

[...] **** Using the SPPropertyBag with Custom Admin Pages in SharePoint [...]

Links (5/8/2008) « Steve Pietrek - Everything SharePoint added these pithy words on May 09 08 at 01:29

@AndersR

Thanks for the hint. I am going to update this post shortly.

Steve Graegert added these pithy words on May 13 08 at 15:19

Hi,

Excellent information, thanks for sharing this.

I’m currently trying to achieve exactly the same thing, using a custom admin page to store settings in the property bag of the site.

Dennis Meenhuis added these pithy words on Jun 23 08 at 10:42

there’s a missing forward slash after Pages
Pages/MyCustomSettings.asp

Kee added these pithy words on Jul 12 08 at 02:19

@Kee,

typo has been corrected. Thanks for pointing it out.

Steve Graegert added these pithy words on Jul 12 08 at 08:34

[...] 3, 2008 por jdieguez En el blog de Steve Graegert he encontrado un post muy interesante(Using the SPPropertyBag with Custom Admin Pages in SharePoint) que describe como crear una pagina de Administracin(application page) que permite establecer una [...]

Crear una pagina de configuración/administración para SharePoint « Jorge Dieguez Blog added these pithy words on Aug 03 08 at 21:00

[...] el blog de Steve Graegert he encontrado un post muy interesante(Using the SPPropertyBag with Custom Admin Pages in SharePoint) que describe como crear una pagina de Administracin(application page) que permite establecer una [...]

Crear una pagina de configuración/administración para SharePoint - Jorge Dieguez Blog added these pithy words on Aug 03 08 at 21:02

[...] el blog de Steve Graegert he encontrado un post muy interesante(Using the SPPropertyBag with Custom Admin Pages in SharePoint) que describe como crear una pagina de Administracin(application page) que permite establecer una [...]

Crear una pagina de configuración/administración para SharePoint - Jorge Dieguez Blog - Navegando por la Red (jdieguez.wordpress.com) added these pithy words on Aug 03 08 at 21:04

[...] you want to manage this through UI. Refer this link. It’s a fantastic option you will [...]

SPPropertyBag « SharePoint - The MOSS added these pithy words on Sep 23 08 at 12:35

Good post thanks, a useful class library. There’s a typo in the definition of ConfigFileSettings in that Existskey should be ExistsKey with upper case K.

Lars Nielsen added these pithy words on Nov 12 08 at 11:48

[...] Referencias de intershttp://msdn.microsoft.com/en-us/library/bb892187.aspxhttp://graegert.com/?p=505 [...]

Diferentes tipos de páginas de SharePoint « Jorge Dieguez Blog added these pithy words on Dec 08 08 at 18:42

[...] Estas pginas se alojan en una carpeta del sistema de archivos(12TEMPLATESLAYOUTS) de los servidores frontales de SharePoint(veremos mas adelante que existen pginas SharePoint que se alojan en base de datos). La principal caracterstica de este tipo de pginas es que tienen un rendimiento muy bueno(al no estar alojadas en base de datos). Otro caracterstica importante es que estas pginas se comparten para todos los sitios SharePoint de una granja.Adems estas pginas no pueden ser personalizadas(customized) ni pueden contener WebParts.Todas las pginas de aplicacin utilizan la misma pgina maestra: ~/_layouts/application.masterReferencias de intershttp://msdn.microsoft.com/en-us/library/bb892187.aspxhttp://graegert.com/?p=505 [...]

Diferentes tipos de páginas de SharePoint - Jorge Dieguez Blog added these pithy words on Dec 08 08 at 18:45

wonderful post))

Quad added these pithy words on Jan 18 09 at 22:56

[...] Using the SPPropertyBag with Custom Admin Pages in SharePoint [...]

Links 2009-05-15 - Gunnar Peipman's ASP.NET blog added these pithy words on May 15 09 at 11:35

Thanks for post. Nice to see such good ideas.

Zashkaser added these pithy words on Aug 05 09 at 17:23

Nice article. One question is the design-time support in Visual Studio or Sharepoint Designer?

Joey Chömpff added these pithy words on Mar 24 10 at 11:58

Joey – there is no such thing I am afraid. Yet, if you’re curious what kind of information is present you may want to take a look at SharePoint Manager 2007 that is capable of displaying all kinds of things and all values of a SPWeb’s property bag.

Steve Graegert added these pithy words on Mar 24 10 at 13:31

That’s pro stuff! Not only useful but also instructive. Thank you for sharing.

Half added these pithy words on Jul 30 10 at 08:29

[...] Referencias de interéshttp://msdn.microsoft.com/en-us/library/bb892187.aspxhttp://graegert.com/?p=505 [...]

Diferentes tipos de páginas de SharePoint added these pithy words on Jun 21 11 at 22:10

[...] el blog de Steve Graegert he encontrado un post muy interesante(Using the SPPropertyBag with Custom Admin Pages in SharePoint) que describe como crear una pagina de Administración(application page) que permite establecer una [...]

Crear una pagina de configuración/administración para SharePoint added these pithy words on Jun 21 11 at 22:12

[...] тынц Like this:НравитсяБудьте первым, кому понравился этот . Опубликовано в Sharepoint. Оставьте комментарий » [...]

Using the SPPropertyBag with Custom Admin Pages in SharePoint « ИТ Блог added these pithy words on Oct 20 11 at 13:28

Leave a Reply