Sunday, July 25, 2010

Custom settings for a SharePoint list or library

Someone asked me this week if it is possible to store custom settings (configuration) on a SharePoint (WSS 3.0 and MOSS 2007) list or library.
Example: You have a list or library and you want to create a custom property (metadata)field and provide the user with an GUI to manage a setting value.

I looked into the various options and found it is indeed very simple to accomplish.
 Many thanks to Margriet Bruggeman and Nikander Bruggeman for their contributions.
You can use the Rootfolder object which contains a System.Collections.Hashtable for storing and accessing properties.It is very simple.

To store the value you use:
SPContext.Current.List.RootFolder.Properties[strPropertyKey] = strValue;

SPContext.Current.List.RootFolder.Update ();
 
To retrieve the value you use:
strValue = SPContext.Current.List.RootFolder.Properties[strPropertyKey].ToString();

In order to make it easy for the user to manage such settings, one would add a custom action with Location="Microsoft.SharePoint.ListEdit".
This means that when the user go to a list or library and then select "Settings" a new hyperlink (shortcut) will exist on the settings page from where the custom settings can be managed. (this is not overriding the settings page but rather just adding a shortcut on it)

This tutorial will show how easy it is to develop the described functionality.

Overview:
We will develop the following 2 components:
  1-Feature with Custom Action.
  2-Custom aspx page with code behind to view,insert,edit & delete settings.

Lets get started:

Create a new VS2008 WSPBuilder Project.

















Add a new item to the project. Select WSPBuilder and then 'Blank Feature'















Set the scope of the feature to web.

The Feature.xml file will look like:
<?xml version="1.0" encoding="utf-8"?>

<Feature Id="65a738a0-a095-489e-be19-82c077edea9f"
Title="ListCustomSettings"
Description="Allow Custom Settings on any list"
Version="12.0.0.0"
Hidden="FALSE"
Scope="Web"
DefaultResourceFile="core"
xmlns="http://schemas.microsoft.com/sharepoint/">
<ElementManifests>
<ElementManifest Location="elements.xml"/>
</ElementManifests>
</Feature>

Now we will use the feature to implement a custom action.
Change the Elements.xml file of the Feature to look like:
<?xml version="1.0" encoding="utf-8" ?>

<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
<CustomAction
Id="Custom.Configuration.ListCustomSettings"
GroupId="GeneralSettings"
Location="Microsoft.SharePoint.ListEdit"
RequireSiteAdministrator="FALSE"
Sequence="100"
Title="Custom Settings">
<UrlAction
Url="_layouts/ListCustomSettings/ListSetting.aspx?List={ListId}" />
</CustomAction>
</Elements>

The feature (as above) will give us a new hyperlink on the List Settings Page (ListEdit). It will look as follow:












The next step is to create a custom page which we can redirect to and on which we can mange the custom settings.

Add a .aspx page to your project with an associated .aspx.cs file.
(there is a little trick to adding this aspx page to the project... please see the footnote at the bottom of this post for step-by-step instructions)

Ensure that the two files are placed inside an appropriate solution folder within the LAYOUTS Folder. Also ensure that the name of the aspx file corresponds to the url used in the custom action definition (above).
<UrlAction Url="_layouts/ListCustomSettings/ListSetting.aspx?List={ListId}" />



















Next we add controls, assembly references etc to our aspx page. I am not going into detail of each line of code as it is standard code.
Ensure your .aspx file look as follow:
(please take special note of the public token key as yours might be different)
<%@ Assembly Name="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>

<%@ Import Namespace="Microsoft.SharePoint" %>
<%@ Import Namespace="Microsoft.SharePoint.ApplicationPages" %>
<%@ Import Namespace="System.Data" %>
<%@ 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="ToolBarButton" Src="~/_controltemplates/ToolBarButton.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" %>
<%@ Page Language="C#" MasterPageFile="~/_layouts/application.master" AutoEventWireup="true" Inherits="ListConfig.ListConfig, ListCustomSettings,Version=1.0.0.0,Culture=neutral,PublicKeyToken=29d1ff49fe67cd5b" %>
<asp:Content ID="Content2" ContentPlaceHolderID="PlaceHolderPageTitle" runat="server">
Custom list config settings page
</asp:Content>
<asp:Content ID="Content3" ContentPlaceHolderID="PlaceHolderPageTitleInTitleArea" runat="server">
Custom list config settings
</asp:Content>
<asp:Content ID="Content1" ContentPlaceHolderID="PlaceHolderMain" runat="server">
<table width="50%" class="ms-propertysheet" cellspacing="0" cellpadding="0" border="0">
<tr>
<td class="ms-error">
<asp:Label ID="lblMessage" runat="server" EnableViewState="False" />
</td>
</tr>
</table>
<table width="50%" class="ms-propertysheet" cellspacing="0" cellpadding="0" border="0">
<tr><td class="ms-descriptionText"><asp:Label ID="LabelMessage" Runat="server" EnableViewState="False" class="ms-descriptionText"/></td></tr>
<tr><td class="ms-error"><asp:Label ID="LabelErrorMessage" Runat="server" EnableViewState="False" /></td></tr>
<tr><td class="ms-descriptionText"><asp:ValidationSummary ID="ValSummary" HeaderText="Errors" DisplayMode="BulletList" ShowSummary="True" runat="server"></asp:ValidationSummary></td> </tr> <tr> <td><img src="/_layouts/images/blank.gif" width="10" height="1" alt="" /></td></tr>
</table>
<table width="50%" border="0" cellspacing="0" cellpadding="0" class="ms-propertysheet">
<!-- Key -->
<wssuc:InputFormSection Title="Key" runat="server">
<Template_Description>
<SharePoint:EncodedLiteral ID="EncodedLiteral1" runat="server" text="Specify a key for the entry." EncodeMethod='HtmlEncodeAllowSimpleTextFormatting'/>
</Template_Description>
<Template_InputFormControls>
<wssuc:InputFormControl LabelText="" runat="server">
<Template_control>
<wssawc:InputFormTextBox Title="Key" class="ms-input" ID="txtKey" Columns="35" Runat="server" MaxLength=255 />
</Template_control>
</wssuc:InputFormControl>
</Template_InputFormControls>
</wssuc:InputFormSection>
<!-- Value -->
<wssuc:InputFormSection Title="Value" runat="server">
<Template_Description>
<SharePoint:EncodedLiteral ID="EncodedLiteral2" runat="server" text="Specify the value of the entry" EncodeMethod='HtmlEncodeAllowSimpleTextFormatting'/>
</Template_Description>
<Template_InputFormControls>
<wssuc:InputFormControl LabelText="" runat="server">
<Template_control>
<wssawc:InputFormTextBox Title="Value" class="ms-input" ID="txtValue" Columns="35" Runat="server" MaxLength=255 />
</Template_control>
</wssuc:InputFormControl>
</Template_InputFormControls>
</wssuc:InputFormSection>
<wssuc:ButtonSection runat="server" ShowStandardCancelButton="false">
<template_buttons>
<asp:PlaceHolder ID="PlaceHolderInsertUpdate" runat="server">
<asp:Button UseSubmitBehavior="false" runat="server" class="ms-ButtonHeightWidth" Text="Insert" id="cmdInsertUpdate" OnClick="cmdInsertUpdate_OnClick"/>  
<asp:Button UseSubmitBehavior="false" runat="server" class="ms-ButtonHeightWidth" Text="Cancel" id="cmdInsertUpdateCancel" OnClick="cmdInsertUpdateCancel_OnClick"/>  
</asp:PlaceHolder>
</template_buttons>
</wssuc:ButtonSection>
<br />
<br />
</table>
<SharePoint:SPGridView ID="grdEntries" runat="server" AutoGenerateColumns="false" Width="50%" AllowSorting="True" OnRowDeleting="grdEntries_RowDeleting" OnRowEditing="grdEntries_RowEditing" EnableViewState="false">
<AlternatingRowStyle CssClass="ms-alternating" />
</SharePoint:SPGridView>
<wssuc:ButtonSection runat="server" ShowStandardCancelButton="false">
<template_buttons>
<asp:PlaceHolder ID="PlaceHolder1" runat="server">
<asp:Button UseSubmitBehavior="false" runat="server" class="ms-ButtonHeightWidth" OnClick="cmdOK_OnClick" Text="OK" id="cmdOK" causesvalidation=false />
</asp:PlaceHolder>
</template_buttons>
</wssuc:ButtonSection>
<br />
<br />
<SharePoint:FormDigest ID="FormDigest1" runat="server" />
</asp:Content>

Now we need to code the ListConfig class which is the code-behind of our aspx page.
Open the ListSettings.aspx.cs in code view and add the following code:
using System;

using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using Microsoft.SharePoint;
using Microsoft.SharePoint.WebControls;
using Microsoft.SharePoint.Utilities;
using System.Xml;

namespace ListConfig
{
  public partial class ListConfig : LayoutsPageBase
  {
    private DataTable _dtEntries;
    private DataView _dvEntriesView;
    protected SPGridView grdEntries = new SPGridView();
    protected Label lblMessage = new Label();
    protected InputFormTextBox txtKey = new InputFormTextBox();
    protected InputFormTextBox txtValue = new InputFormTextBox();
  }
}

 Next, add a Page_Init event to the ListConfig class. In this event will we add two data columns and two command cokumns to our grid.
protected void Page_Init(object sender, EventArgs e)

{
  BoundField objKeyField = new BoundField();
  objKeyField.ItemStyle.Width = Unit.Percentage(30);
  objKeyField.DataField = "Key";
  objKeyField.HeaderText = "Key";
  grdEntries.Columns.Add(objKeyField);

  BoundField objValueField = new BoundField();
  objValueField.DataField = "Value";
  objValueField.HeaderText = "Value";
  grdEntries.Columns.Add(objValueField);

  CommandField objEditField = new CommandField();
  objEditField.ShowEditButton = true;
  objEditField.HeaderText = "Edit";
  objEditField.ItemStyle.Width = Unit.Pixel(20);
  objEditField.ButtonType = ButtonType.Image;
  objEditField.EditImageUrl = "/_layouts/images/edit.gif";
  grdEntries.Columns.Add(objEditField);

  CommandField objDeleteField = new CommandField();
  objDeleteField.ShowDeleteButton = true;
  objDeleteField.HeaderText = "Delete";
  objDeleteField.ItemStyle.Width = Unit.Pixel(20);
  objDeleteField.ButtonType = ButtonType.Image;
  objDeleteField.DeleteImageUrl = "/_layouts/images/delete.gif";
  grdEntries.Columns.Add(objDeleteField);
}


Next, add a Page_Load event to the ListConfig class. In this event we set allowunsafeupdates=true and we bind the grid to data.
protected void Page_Load(object sender, EventArgs e)

{
  SPContext.Current.Web.AllowUnsafeUpdates = true;
  BindGrid();
} 

Next, add a LoadData function to help load the data into the datatable. You will notice the important thing here is that we iterate through the RootFolder.Properties.Keys to get all custom settings for the current list, and then we load the data in to the datatable.
private void LoadData(string strSortOrder)

{
_dtEntries = new DataTable();
DataColumn objKeyColumn = new DataColumn();
objKeyColumn.DataType = Type.GetType("System.String");
objKeyColumn.ColumnName = "Key";
objKeyColumn.ReadOnly = true;
_dtEntries.Columns.Add(objKeyColumn);

DataColumn objValueColumn = new DataColumn();
objValueColumn.DataType = Type.GetType("System.String");
objValueColumn.ColumnName = "Value";
objValueColumn.ReadOnly = true;
_dtEntries.Columns.Add(objValueColumn);

foreach (string strKey in SPContext.Current.List.RootFolder.Properties.Keys)
{
if (strKey.StartsWith("vti_")) continue;
if (strKey.StartsWith("_reporting")) continue;
if (SPContext.Current.List.RootFolder.Properties[strKey] == null) continue;
if (SPContext.Current.List.RootFolder.Properties[strKey].ToString() == String.Empty) continue;

DataRow objRow = _dtEntries.NewRow();
try
{
objRow["Key"] = strKey;
}
catch
{
objRow["Key"] = "Error";
}
try
{
objRow["Value"] = SPContext.Current.List.RootFolder.Properties[strKey];
}
catch
{
objRow["Value"] = "Error";
}
_dtEntries.Rows.Add(objRow);
}
_dvEntriesView = _dtEntries.DefaultView;
_dvEntriesView.Sort = "[Key] asc";
}

Next, add a helper function to populate the grid:
private void BindGrid()

{
try
{
LoadData(String.Empty);
grdEntries.DataSource = _dvEntriesView;
grdEntries.DataBind();
}
catch (Exception err)
{
lblMessage.Text = lblMessage.Text + "Failed to bind data to page. Please try to reload the page.";
}
}

Add Event handlers for the grid behaviour:
public void grdEntries_RowDeleting(object sender, GridViewDeleteEventArgs e)

{
try
{
string strKey = grdEntries.Rows[e.RowIndex].Cells[0].Text;
strKey = SPHttpUtility.HtmlDecode(strKey);
if (SPContext.Current.List.RootFolder.Properties.ContainsKey(strKey))
{
SPContext.Current.List.RootFolder.Properties[strKey] = null;
SPContext.Current.List.RootFolder.Update();
}
BindGrid();
}
catch (Exception err)
{
lblMessage.Text = lblMessage.Text + err.Message + "\n";
}
}

public void grdEntries_RowEditing(object sender, GridViewEditEventArgs e)
{
string strKey = grdEntries.Rows[e.NewEditIndex].Cells[0].Text;
txtKey.Text = strKey;
txtKey.Enabled = false;
txtValue.Text = SPContext.Current.List.RootFolder.Properties[strKey].ToString();
}

public void grdEntries_Sorting(object sender, GridViewEditEventArgs e)
{
grdEntries.DataSource = _dvEntriesView;
grdEntries.DataBind();
}

Add a helper function to build a redirect url (which will be used when the form close):
private string GetRedirectUrl()

{
return SPContext.Current.Web.Url + String.Format("/_layouts/listedit.aspx?List={0}", Request.QueryString["List"]);
}

Add the Button Click Events which corresponds to the definition in the aspx file:
public void cmdOK_OnClick(object sender, EventArgs e)

{
SPUtility.Redirect(GetRedirectUrl(), SPRedirectFlags.UseSource, HttpContext.Current);
}

public void cmdInsertUpdate_OnClick(object sender, EventArgs e)
{
try
{
string strKey = txtKey.Text;
if (strKey.Length == 0)
{
throw new Exception("You must provide a key.");
}
if (strKey.Contains("&"))
{
throw new Exception("& is not allowed. You must provide an alternative name.");
}
string strValue = txtValue.Text;
if (strValue.Length == 0)
{
throw new Exception("You must provide a value.");
}
if (SPContext.Current.List.RootFolder.Properties.ContainsKey(strKey))
{
SPContext.Current.List.RootFolder.Properties[strKey] = strValue;
}
else
{
SPContext.Current.List.RootFolder.Properties.Add(strKey, strValue);
}
SPContext.Current.List.RootFolder.Update();
strKey = "";
strValue = "";
txtKey.Text = "";
txtKey.Enabled = true;
txtValue.Text = "";
BindGrid();
}
catch (Exception err)
{
lblMessage.Text = err.Message;
}
}

public void cmdInsertUpdateCancel_OnClick(object sender, EventArgs e)
{
txtKey.Text = "";
txtKey.Enabled = true;
txtValue.Text = "";
}

Override the OnUload event of the page to set the AllowUnsafeUpdates=false:
protected override void OnUnload(EventArgs e)

{
SPContext.Current.Web.AllowUnsafeUpdates = false;
}

WOW, it looks like a lot of code, but in fact, its not much!
Next, build your project and if all is successfull you are ready to test the outcome.

Go to your project properties and set the 'start browser with' setting to the target SP site url on which you want to test.













Save your project. Right click on your project, select WSPBuilder and then select "Build WSP" - keep an eye on the output pane to ensure that there are no problems. Once successful, right click on the project, select WSPBuilder and then select "Deploy". 
Once done, go to the target SP site and refresh the site features list.
You will notice the following new site feature is available:






Activate the feature.
Go to any list or library and open the settings page.














You will notice the new Custom Action URL which we added:













When you click on the 'custom settings' custom action url you will be redirected to the new custom page:










On this page you can manage custom settings for the list.












There you go ! a very nice tool which can be used to store custom settings on any list or library.

Enjoy !!!

Note: Trick to add a aspx page to the project:
If you want to add a aspx page to the project you click on project and select "Add New Item..." but you will notice that the Web Form / .aspx item is not available to be selected.
When you want to add a aspx page with an associated aspx.cs file to the project there is a little trick that you need to do.
Open another instance of Visual Studio.
Select to create a new project and choose C# --> ASP.Net Web Application
















Provide a projace name and click on "OK".
Once the project has been created you will see it contains a web form (aspx).
Open the Solution Explorer view --> right click and select "Open Folder in Windows Explorer".






















From Windows Explorer copy the aspx file and aspx.cs file into your SharePoint project folder (where the solution of this tutorial project is located) and then in your project add an existing item and select the two files which you copied.

13 comments:

Himanshu Shrivansh said...

Hello Johan Olivier,

I am unable to add .aspx page from WSPBuilder. I am unable to found .aspx item in Add->item list.

Please reply soon

Regards
Himanshu Shrivansh

Johan Olivier said...

Hi Himanshu Shrivansh,
I am glad to read that you are using the tutorial.
You are right, if you try to add a new item to the project, the aspx page is not an option to be selected.
Please see that I added a little section at the end of this post to show you step-by-step how to get the aspx file into the project.
Enjoy !

Anonymous said...

Hi Johan,

Great walk-through.

Do you know if there is a way to create "Custom settings" for just one specific list instead of applying the option for all lists on the website?

Regards
Thorsten Dörge

Unknown said...

Hello Johan Olivier,

I was able to following your tutorial to make a custom list settings page.
The problem that I have is that I need to make modifications to the layout of the page.
When I add your code to a blank aspx page it gives me errors that the masterpage could not be found. Therefore I cannot use the design pane to edit my layout.
What I did then is created a regular aspx page without any masterpage links and copy/paste it into the asp:content tags.
Problem here is that any object used in the asp:content is not reachable from my code behind. (I cannot call the text boxes, or in my case a checkbox)

So now my question:
- How did you make your layout in such way? Did you use a tool? Or was it just coding? Any more hints for creating a custom aspx page?

Thank you

Anonymous said...

Johan Olivier, Thanks.

It's possible to create "Custom settings" for just one specific list ?

Thorsten Dörge , do you found answer?

Bugeda Alexey

Johan Olivier said...

Hi Bugeda Alexy and Thorsten Dörge,
Thank you for reading my posts and for asking the very good question.

I am also keen to explore the option to target a specific list, but at the moment I am flooded with project work and will only be able to properly respond later.

For now you can maybe try the following:
http://social.msdn.microsoft.com/Forums/en-US/sharepointdevelopment/thread/40076dc2-8be6-4019-bf1e-54067880e8a8/

I have not tried it yet so please let me know it it worked.

Regards

Anonymous said...

Johan Olivier, Great!!
Thanks! It's Worked!

<Elements <ListTemplate
Type="ListTemplateType"

and
<Elements <CustomAction
RegistrationId="ListTemplateType"

Type and RegistrationId are equal


Regards
Bugeda Alexey

Ironiff said...

You are my hero!
Thank you for this Article.

Unknown said...

Hello Johan Olivier,
Thank you very much. It is Very useful post. :)

Steel Thomas said...

Restrict users to have access to a set of SharePoint lists or items and respectively only these lists or records would be usable by SharePoint Web parts for the specified user or user groups.

Unknown said...

Good Information on custom setting it was really helpful for me, because I am new to this sharepoint hosting technology.

Amar said...

Given the lack of concrete information on this subject on the internet this is one of the best articles I have read. Thanks a lot sir!

Unknown said...

its very helpful sharing! thank you so much!!
New Crack Software

Post a Comment