Existing solutions and what’s different
A lot of work has already been done to solve the problem of cascading dropdown lists in SharePoint. The first solution I found was provided by DataCogs. Their solution provides allows for a single parent field to control the contents of a specific child field (think US States, and Cities).
Their solution has subsequently been extended by a number of people including Anbu Thangarathinam and Patrick Clarke to allow for a cascading chain of unlimited depth (think Countries, States/Territories, Cities, and Postal Codes).
My business partner and general great-guy Ed St. Lawrence has also come up with a solution for cascading dropdowns that involve look-up lists.
All of the aforementioned work is excellent however it seems to be overkill for some of the situations I’ve encountered.
First, I found myself creating a bunch of lists with 2 columns to model the parent/child relationship.
In order for the parent field to update the child(ren) field(s) they had to search for the field in the list recursively. In a high volume SharePoint site this can be a performance hit; and it just doesn’t feel right to me.
I found the whole semantics of the solution very difficult. Each custom field needs to track its own values, the child list, the column in the child list to search for matching values, and the column in the child list to display. Actually, that was very concise and made it seem easy, but when I was going through the exercise of implementing the field I found it all very confusing and difficult to manage.
Finally, and this only applies to the DataCogs solution, I don’t like the dependency on multiple fields and field types in my list. Too many people have the ability to modify lists, and I can envision a scenario where a power-user decides that the Country field shouldn’t be of type ‘Parent’ and removes it, thus breaking the cascading chain.
I wanted to build a cascading field control that addressed these issues.
There are 2 major differences in my approach than the current solutions out there. First it is based on Choice type Site Columns, and next all (currently only 2 levels are supported) of the dropdowns are part of the same field.

Figure 1: MasterDetail field with no Master record selected

Figure 2: MasterDetail field displaying Master and associated Details

Figure 3: MasterDetail field displaying Master and allowing multiple Detail values

Figure 4: MasterDetail field displaying no available details
In order to affect the relationship I rely on the following ‘magic’ each choice in a given Choice type Site Column must have the same name as another Choice type Site Column that holds the tertiary data.
For example Deserts MasterDetail field would use the following Site Columns
|
Site Column Name |
Choices |
Displays When |
|
Deserts |
Cake, Ice Cream, Cookies, Coffee |
User selects value from dropdown list |
|
Cake |
Chocolate, Layer Cake, Cheesecake |
User selects ‘Cake’ option |
|
Ice Cream |
|
User selects ’Ice Cream’ option |
|
Cookies |
Butter, Sugar |
User selects ‘Cookies’ option |
If the user selects “Coffee” from the Master Dropdown list, no Detail is displayed because there is no corresponding Site Column – no harm, no foul.

Figure 5: Master choice type site column

Figure 6: Detail choice type site column

Figure 7: Detail choice type site column

Figure 8: Detail choice type site column
The Field Type Definition is the plumbing that registers a custom control for use with SharePoint.
The following explanation if from the MSDN documentation on creating custom fields:
This field type definition is an XML file that contains the information that Windows SharePoint Services needs to correctly render the field, including its column header, on list view pages (such as AllItems.aspx). It is typically also used to render the field on the view list item page (DispForm.aspx) and sometimes it is used to render the field on the New or Edit (list item) pages. It can also declare and define special variable properties of the field type whose values will be set whenever a column is created based on the field type. Most importantly, it contains information about the assembly that includes the compiled field type.
Field Type definition files are stored in the directory : <12 hive>\TEMPLATE\XML
Custom field type definition files should follow the naming convention: fldtypes_<text_identifier>.xml
Here is a copy of the field type definition file for my custom field.
<?xml version="1.0" encoding="utf-8"?>
<FieldTypes>
<FieldType>
<Field Name="TypeName">MasterDetailField</Field>
<Field Name="TypeDisplayName">Display master detail data</Field>
<Field Name="TypeShortDescription">Single select master data.</Field>
<Field Name="ParentType">Text</Field>
<Field Name="UserCreatable">TRUE</Field>
<Field Name="FieldTypeClass">TurimSaint.Fields.MasterDetailField, Fields, Version=1.0.0.31, Culture=neutral, PublicKeyToken=c54b7145719717d8</Field>
<Field Name="FieldEditorUserControl">/_controltemplates/FieldMasterDetailSettings.ascx</Field>
<PropertySchema>
<Fields>
<Field Name="SiteColumnName" Hidden="TRUE" Type="Text" />
<Field Name="MultiDetails" Hidden="TRUE" Type="Text" />
</Fields>
</PropertySchema>
<RenderPattern Name="DisplayPattern">
<Column />
</RenderPattern>
</FieldType>
</FieldTypes>
TypeName: This is the name of the field class (defined in your source code)
TypeDisplayName: Shown on column create pages (alongside ‘Single line of text’)
TypeShortDescription: Shown on the list settings pages (alongside, err, ‘Single line of text’ – but its different)
ParentType: The name of the field control this custom field inherits from (I’m not sure why it’s necessary, as it’s defined in code). I compiled this list of possible ParentTypes by going through the fldtypes.xml file that ships with MOSS (The FieldTypeClass, if specified, is in parentheses), anything I thought was interesting I put in brackets.
· Counter
· Text (SPFieldText)
· Note (SPFieldMultiLineText)
· Choice (SPFieldChoice)
· MultiChoice (SPFieldMultiChoice)
· GridChoice (SPFieldRatingScale)
· Integer
· Number (SPFieldNumber)
· ModStat [This is the moderation status field]
· Currency (SPFieldCurrency)
· DateTime (SPFieldDateTime)
· Lookup (SPFieldLookup)
· Boolean (SPFieldBoolean)
· Threading
· ThreadIndex
· Guid
· Computed (SPFieldComputed)
· File
· Attachments
· User (SPFieldUser)
· URL (SPFieldUrl)
· Calculated (SPFieldCalculated)
· Recurrence
· CrossProjectLink
· ContentTypeId
· MultiColumn [ParentType = ‘Note’]
· LookupMulti [ParentType = ‘Lookup’]
· UserMulti [ParentType = ‘LookupMulti’]
· WorkflowStatus
· AllDayEvent
· WorkflowEventType [ParentType = ‘Integer’]
· PageSeparator (SPFieldPageSeparator)
UserCreatable: Whether or not the field can be created via the UI
FieldTypeClass: The custom field’s fully qualified class name (namespace + TypeName) including the strongly typed assembly name.
FieldEditorUserControl: The path to the user control that will be displayed to list editor’s when they add this field to a list. It is optional if the new field does not need to collect any data at creation time, this attribute can be blank.
PropertySchema: The custom properties the new field contains. If the field requires custom properties, it is likely that a FieldEditorUserControl will need to be created to capture those properties at field creation time (when the field is added to a list). See the following section for more information on custom field properties.
RenderPattern: The dark arts. This dictates how the field will be rendered in its various modes (design, edit, display) – but it’s not well documented, and very difficult to infer from existing field type definitions and is optional. The most common method for rendering custom field controls in display and edit mode is by creating a user control.

Figure 9: Field Type Definition attributes on List Settings page

Figure 10: Field Type Definition attributes on Add Column page
There are a couple of issues that custom field developers should be aware of.
1. The property names exposed by Field Class must exactly match those defined in the FldTypes.xml file.
2. There is a bug that prevents the SharePoint runtime from properly saving custom field properties, here is a thorough discussion of the problem and the workaround.
The field class is predominately a plumbing class that the SharePoint runtime use to access the more interesting classes associated with the custom field.
The FieldRenderingControl property returns the class (typically a user control code-behind) that is responsible for rendering the field on pages. There is more detail on this in the Display Control section.
Most of the work this class does is implementing the workaround for saving custom properties. It appears the SharePoint runtime calls the OnAdded(SPAddFieldOptions op) and Update () methods in the wrong order so the custom properties are cleared from memory before they get saved. The workaround involves using a static dictionary to store the custom properties so you can save them properly.
using System;
using System.Runtime.InteropServices;
using System.Security.Permissions;
using Microsoft.SharePoint;
using Microsoft.SharePoint.WebControls;
using Microsoft.SharePoint.Security;
using System.Collections.Generic;
namespace TurimSaint.Fields
{
[CLSCompliant(false)]
[Guid("469cbf54-d8e9-46e7-9528-d82911d90861")]
public class MasterDetailField : SPField
{
public MasterDetailField(SPFieldCollection fields, string fieldName)
: base(fields, fieldName)
{
}
public MasterDetailField(SPFieldCollection fields, string typeName, string displayName)
: base(fields, typeName, displayName)
{
}
public override BaseFieldControl FieldRenderingControl
{
[SharePointPermission(SecurityAction.LinkDemand, ObjectModel = true)]
get
{
BaseFieldControl fieldControl = new MasterDetailFieldControl();
fieldControl.FieldName = this.InternalName;
return fieldControl;
}
}
public override string GetValidatedString(object value)
{
return base.GetValidatedString(value);
}
public override Type FieldValueType
{
get
{
return typeof(SPFieldMasterDetailValue);
}
}
public override object GetFieldValue(string value)
{
return new SPFieldMasterDetailValue(value);
}
public override string GetFieldValueAsText(object value)
{
SPFieldMasterDetailValue MasterDetailValue = value as SPFieldMasterDetailValue;
if (MasterDetailValue == null)
return string.Empty;
return MasterDetailValue.ToString().Replace(" ", " ");
}
public override string GetFieldValueAsHtml(object value)
{
SPFieldMasterDetailValue MasterDetailValue = value as SPFieldMasterDetailValue;
if (MasterDetailValue == null)
return string.Empty;
return MasterDetailValue.ToString();
}
private static Dictionary<string, Guid> customParentColumn = new Dictionary<string, Guid>();
public Guid ParentColumn
{
get
{
if (customParentColumn.ContainsKey(PropertyID))
return customParentColumn[PropertyID];
object propValue = GetCustomProperty("SiteColumnName");
if (propValue == null)
return Guid.Empty;
return new Guid(propValue.ToString());
}
set
{
if (!customParentColumn.ContainsKey(PropertyID))
customParentColumn.Add(PropertyID, value);
else
customParentColumn[PropertyID] = value;
}
}
private static Dictionary<string, Boolean> customMultiDetails = new Dictionary<string, Boolean>();
public Boolean MultiDetails
{
get
{
if (customMultiDetails.ContainsKey(PropertyID))
return customMultiDetails[PropertyID];
object propValue = GetCustomProperty("MultiDetails");
if (propValue == null)
return false;
return Boolean.Parse(propValue.ToString());
}
set
{
if (!customMultiDetails.ContainsKey(PropertyID))
customMultiDetails.Add(PropertyID, value);
else
customMultiDetails[PropertyID] = value;
}
}
public override void Update()
{
SetCustomProperty("SiteColumnName", ParentColumn.ToString());
SetCustomProperty("MultiDetails", MultiDetails.ToString());
base.Update();
if (customParentColumn.ContainsKey(PropertyID))
customParentColumn.Remove(PropertyID);
if (customMultiDetails.ContainsKey(PropertyID))
customParentColumn.Remove(PropertyID);
}
public override void OnAdded(SPAddFieldOptions op)
{
base.OnAdded(op);
Update();
}
private string PropertyID
{
get
{
string l_propID =
(SPContext.Current != null)
? SPContext.Current.GetHashCode().ToString()
: this.Id.ToString();
return l_propID;
}
}
}
}
The administrative control is specified in the Field Type definition file, and it is the user control that is displayed when a list administrator adds the custom field to a list. This control is used the field’s custom property values. The custom properties are in turn used by the Display Control to fetch valid data that an end-user can select for this field when creating a new List Item.
First we have the ascx – the UI that will capture the data we need. If you’ve worked with ASP.NET this is very familiar, if you haven’t its dead-easy. The only interesting things going on here are the page directives.
The Control directive instructs the SharePoint run-time where to find the code-behind for this user control.
The rest of the directives are WSS-centric and essentially ensure that this control will have the same look as the rest of the field property pages.
For the MasterDetailField we simply need to know which SiteColumn contains the list f MasterValue choices, and if the field accepts multiple detail values.
To gather this information I’ve used a dropdown list that is filled with SiteColumn names, and a checkbox.
<%@ Control Language="C#" Inherits="TurimSaint.Fields.FieldMasterDetailSettings, Fields, Version=1.0.0.31, Culture=neutral, PublicKeyToken=c54b7145719717d8" %>
<%@ Register TagPrefix="wssuc" TagName="InputFormControl" src="~/_controltemplates/InputFormControl.ascx" %>
<%@ Register TagPrefix="wssuc" TagName="InputFormSection" src="~/_controltemplates/InputFormSection.ascx" %>
<%@ 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" %>
<wssuc:InputFormSection
runat="Server"
Title="Master/detail properties"
Description="The master site column must be a choice field, each choice must be the name of a site column that contains the details."
>
<Template_InputFormControls>
<wssuc:InputFormControl runat="server">
<Template_Control>
Master site column: <asp:DropDownList runat="server" ID="ddlSiteColumns" />
<br />
Allow multiple detail values: <asp:CheckBox runat="server" ID="cbxMultiDetails" />
</Template_Control>
</wssuc:InputFormControl>
</Template_InputFormControls>
</wssuc:InputFormSection>

Figure 11: MasterDetailSettings control on the List Administration page
The code-behind settings control follows. It is not terribly complicated either. Here are the interesting methods:
InitializeWithField(SPField field): Part of the IFieldEditor interface. This is the entry point when an administrator edits the field, after it’s already been added to a list. It is used to set the initial state of the server controls based on the custom properties of the field being passed in.
OnSaveChange(Microsoft.SharePoint.SPField field, bool isNewField): Part of the IFieldEditor interface. This method is called when an administrator clicks the ‘Ok’ button to save the changes to the list. In this method you set the custom properties on the field being passed in.
CreateChildControls(): A standard event in the ASP.NET user control life-cycle. Use this method to populate your server controls the first time the page loads. For the master detail field we load the site column dropdowns with all of the SiteColumns from the current web and root site collection.
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.SharePoint.WebControls;
using System.Web.UI.WebControls;
using Microsoft.SharePoint;
namespace TurimSaint.Fields
{
public class FieldMasterDetailSettings : System.Web.UI.UserControl, IFieldEditor
{
protected DropDownList ddlSiteColumns;
protected CheckBox cbxMultiDetails;
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
}
protected override void CreateChildControls()
{
base.CreateChildControls();
ddlSiteColumns.AutoPostBack = false;
cbxMultiDetails.AutoPostBack = false;
if (!Page.IsPostBack)
{
foreach (SPField SiteColumn in SPContext.Current.Web.Fields)
{
if (!SiteColumn.Hidden && SiteColumn.Type == SPFieldType.Choice )
ddlSiteColumns.Items.Add(new ListItem(SiteColumn.Title, SiteColumn.Id.ToString()));
}
foreach (SPField SiteColumn in SPContext.Current.Site.RootWeb.Fields)
{
if (!SiteColumn.Hidden && SiteColumn.Type == SPFieldType.Choice)
ddlSiteColumns.Items.Add(new ListItem(SiteColumn.Title, SiteColumn.Id.ToString()));
}
ddlSiteColumns.Items.Insert(0, new ListItem("-- Select --", string.Empty));
}
}
#region IFieldEditor Members
public bool DisplayAsNewSection
{
get { return true; }
}
public void InitializeWithField(SPField field)
{
MasterDetailField masterDetailField = field as MasterDetailField;
if (masterDetailField == null)
return;
EnsureChildControls();
if (!Page.IsPostBack)
{
ListItem li = ddlSiteColumns.Items.FindByValue(masterDetailField.ParentColumn.ToString());
if (li != null)
li.Selected = true;
cbxMultiDetails.Checked = masterDetailField.MultiDetails;
}
}
public void OnSaveChange(Microsoft.SharePoint.SPField field, bool isNewField)
{
MasterDetailField masterDetailField = field as MasterDetailField;
if (masterDetailField == null)
return;
masterDetailField.ParentColumn = new Guid(ddlSiteColumns.SelectedValue);
masterDetailField.MultiDetails = cbxMultiDetails.Checked;
}
#endregion
}
}
The display control is really where the magic happens; this is where the end-user is wowed by a new type of field they’ve never seen before. The user control is fairly straight-forward, but I think the code-behind represents the most complexity.
The DropDownList ddlMaster holds the choices from the selected Site Column. When an end-user selects a new value from this list, it triggers a post back which fills either both the detail lists (only one of the detail lists is shown, depending on whether or not this field is configured to support multiple detail values).
The RenderingTemplate ID field associates this template and the code behind.
<%@ Control Language="C#" Debug="true" %>
<%@ Assembly Name="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="SharePoint" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" Namespace="Microsoft.SharePoint.WebControls" %>
<SharePoint:RenderingTemplate ID="MasterDetail" runat="server">
<Template>
<div style="white-space:nowrap">
<asp:DropDownList runat="server" ID="ddlMaster" Visible="false"/>
<asp:DropDownList runat="server" ID="ddlDetail" Visible="false"/>
<asp:ListBox runat="server" ID="lbDetail" Rows="7" />
</div>
</Template>
</SharePoint:RenderingTemplate>
The code-behind for the display control follows. First off its important to note that this control inherits from the BaseFieldControl. Here are the interesting methods and propeties:
MasterDetailField masterDetailField: This is a helper property that returns the inherited Field property cast as a MasterDetailField.
override string DefaultTemplateName: This returns the value specified in the RenderingTemplate element’s ID attribute on the DisplayControl ascx page.
void OnInit(EventArgs e): This method initializes the field’s value. The control can be in 1 of 3 states, defined in the SPControlMode enumeration (New, Edit, and Display). If the control is in Edit or Display mode, the field value is initialized from the base classes ListItemFieldValue property otherwise a new field value object is created.
override object Value: This property is read/write. The getter returns the field value as an object. First it checks to see which mode the control is in. If we are in Edit mode, we set the value based on the state of the server controls, otherwise the value collected during the OnInit(EventArgs e) method is returned. The setter simply sets the field value.
override void OnLoad(EventArgs e): This method sets the initial state of the server controls if we are not in Display mode. It simply selects the appropriate items in the various ListControls.
void CreateChildControls(): This method fills the MasterValue dropdown list with the values in the selected SiteColumn; it also sets the postback properties on ddlMaster.
void ddlMaster_SelectedIndexChanged(object sender, EventArgs e): This method fills the detail ListControls, and makes the appropriate one visible.
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.SharePoint.WebControls;
using System.Web.UI.WebControls;
using System.Runtime.InteropServices;
using System.Reflection;
using System.Web.UI;
using Microsoft.SharePoint;
namespace TurimSaint.Fields
{
// TODO: Replace, as needed, "TextField" with some other class derived from Microsoft.SharePoint.WebControls.BaseFieldControl.
[CLSCompliant(false)]
[Guid("9f779e63-a131-4dbb-a7fe-0d422612abf6")]
public class MasterDetailFieldControl : BaseFieldControl
{
private SPFieldMasterDetailValue _fieldValue = new SPFieldMasterDetailValue();
protected DropDownList ddlMaster;
protected DropDownList ddlDetail;
protected ListBox lbDetail;
private MasterDetailField masterDetailField
{
get
{ return Field as MasterDetailField; }
}
protected override string DefaultTemplateName
{
get
{
return "MasterDetail";
}
}
public override object Value
{
get
{
EnsureChildControls();
if (this.ControlMode != SPControlMode.Display)
{
_fieldValue.Clear();
EnsureChildControls();
string MasterValue = ddlMaster.SelectedValue;
if (masterDetailField == null)
return null;
if (!masterDetailField.MultiDetails)
_fieldValue.AddMasterDetail(
MasterValue,
ddlDetail.SelectedValue
);
else
{
_fieldValue.AddMaster(MasterValue);
foreach (ListItem li in lbDetail.Items)
{
if (li.Selected)
_fieldValue.AddMasterDetail(
MasterValue,
li.Value
);
}
}
}
return _fieldValue;
}
set
{ _fieldValue = value as SPFieldMasterDetailValue; }
}
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
if (Page.IsPostBack)
return;
if (ControlMode == SPControlMode.Display)
return;
if (_fieldValue == null)
return;
EnsureChildControls();
ListItem li = null;
foreach (string Master in _fieldValue.Keys)
{
ddlMaster.ClearSelection();
ddlDetail.ClearSelection();
li = ddlMaster.Items.FindByValue(Master);
if (li != null)
{
li.Selected = true;
ddlMaster_SelectedIndexChanged(null, e);
}
if (!_fieldValue.HasDetails(Master))
continue;
foreach (string Detail in _fieldValue[Master])
{
li = ddlDetail.Items.FindByValue(Detail);
if (li != null)
{
ddlDetail.Visible = true;
li.Selected = true;
}
li = lbDetail.Items.FindByValue(Detail);
if (li != null)
{
ddlDetail.Visible = true;
li.Selected = true;
}
}
}
}
protected override void OnInit(EventArgs e)
{
//either get the value from an existing item or set it as a new value
if (ControlMode == SPControlMode.Edit || ControlMode == SPControlMode.Display)
{
if (this.ListItemFieldValue != null)
_fieldValue = this.ListItemFieldValue as SPFieldMasterDetailValue;
else
_fieldValue = new SPFieldMasterDetailValue();
}
if (ControlMode == SPControlMode.New)
{
_fieldValue = new SPFieldMasterDetailValue();
}
base.OnInit(e);
}
protected override void CreateChildControls()
{
if (masterDetailField == null)
return;
if (ControlMode == SPControlMode.Display)
return;
base.CreateChildControls();
ddlMaster = (DropDownList)TemplateContainer.FindControl("ddlMaster");
lbDetail = (ListBox)TemplateContainer.FindControl("lbDetail");
ddlDetail = (DropDownList)TemplateContainer.FindControl("ddlDetail");
//set the local vaiables
ddlMaster = (DropDownList)TemplateContainer.FindControl("ddlMaster");
ddlMaster.Items.Insert(0, new ListItem("-- Select --", string.Empty));
ddlMaster.AutoPostBack = true;
ddlMaster.Items.Clear();
SPFieldChoice SiteColumn = null;
try
{ SiteColumn = Web.Fields[masterDetailField.ParentColumn] as SPFieldChoice; }
catch
{
try
{ SiteColumn = Web.Site.RootWeb.Fields[masterDetailField.ParentColumn] as SPFieldChoice; }
catch
{ SiteColumn = null; } //field can't be found
}
if (SiteColumn != null )
{
foreach (string Choice in SiteColumn.Choices)
ddlMaster.Items.Add(new ListItem(Choice));
}
ddlMaster.Items.Insert(0, new ListItem("-- Select --", string.Empty));
ddlMaster.SelectedIndexChanged += new EventHandler(ddlMaster_SelectedIndexChanged);
ddlDetail = (DropDownList)TemplateContainer.FindControl("ddlDetail");
ddlDetail.Visible = false;
lbDetail = (ListBox)TemplateContainer.FindControl("lbDetail");
lbDetail.Visible = false;
}
void ddlMaster_SelectedIndexChanged(object sender, EventArgs e)
{
ddlDetail.Items.Clear();
lbDetail.Items.Clear();
SPFieldChoice DetailField = null;
try
{ DetailField = Web.Fields[ddlMaster.SelectedValue] as SPFieldChoice; }
catch
{
try
{ DetailField = Web.Site.RootWeb.Fields[ddlMaster.SelectedValue] as SPFieldChoice; }
catch
{ DetailField = null; }
}
if (DetailField == null)
{
lbDetail.Visible = false;
ddlDetail.Visible = false;
return;
}
if (masterDetailField == null)
return;
ListControl CurrentListControl =
(masterDetailField.MultiDetails) ? (ListControl)lbDetail : (ListControl)ddlDetail;
foreach (string Choice in DetailField.Choices)
CurrentListControl.Items.Add(new ListItem(Choice));
CurrentListControl.Visible = true;
ddlDetail.Items.Insert(0, new ListItem("-- Select --", string.Empty));
lbDetail.SelectionMode = ListSelectionMode.Multiple;
}
}
}
This field requires a custom value class because we have to store values from multiple columns in a single string, and the value class encapsulates all of the string manipulation and render functionality. The value class supports multiple master values and each master value can have multiple detail values associated with it.
Consumers of the MasterDetailValue class use it as a dictionary, where a MasterValue is associated with a list of DetailValues, so the MasterDetailValue class is derived from a Dictionary<string, list<string>>. The key is the MasterValue and each string in the list represents a single detail value.
The class is pretty straightforward, it has an overloaded constructor to hydrate the object from a persisted string value, 2 methods for rendering the value (depending on whether or not the value contains multiple master values) and 2 helper methods for adding values to the dictionary object.
Taking my queue from the SPFieldLookpValue class, I concatenate all of the values into a single string to persist the data. The 3 string array properties define the delimiters. I chose to use HTML as the delimiters so displaying the field value would be easy, I’m not convinced this is the best way to handle it, but so far it hasn’t caused any problems. Also, since I am using HTML to render the value, I included a CSS class in the rendered value. I don’t use it, but thought it may come in handy at some point.
The SharePoint runtime calls the value classes ToString() method in to display the value on a page.

Figure 12: Rendered values
The ToString() method calls RenderSingleMasterValue() or RenderMultipleMasterValue() depending on whether or not the value contains multiple master values.
using System;
using System.Collections.Generic;
using System.Text;
namespace TurimSaint.Fields
{
public class MasterDetailFieldValue : Dictionary<string, List<string>>
{
private readonly string[] MasterDetailBlockDelimeter = { " <br/>" };
private readonly string[] MasterDetailDelimiter = { ": " };
private readonly string[] DetailDelimiter = { ", " };
public bool HasDetails(string MasterValue)
{
return (this[MasterValue] != null);
}
public SPFieldMasterDetailValue() : base() { }
public SPFieldMasterDetailValue(string value)
{
string[] MasterArray = value.Split(MasterDetailBlockDelimeter, StringSplitOptions.RemoveEmptyEntries);
foreach (string MasterDetailValueSet in MasterArray)
{
string[] MasterDetail = MasterDetailValueSet.Split(MasterDetailDelimiter, StringSplitOptions.RemoveEmptyEntries);
if ( MasterDetail.Length == 1 )
AddMaster(MasterDetail[0]);
else
{
string[] DetailVaues = MasterDetail[1].Split(DetailDelimiter, StringSplitOptions.RemoveEmptyEntries);
Add(MasterDetail[0], new List<string>(DetailVaues));
}
}
}
private StringBuilder Response = new StringBuilder();
private void RemoveTrailingString(string delim)
{
Response.Remove(
Response.Length - delim.Length,
delim.Length
);
}
private void RenderMultipleMasterValue()
{
Response = new StringBuilder(@"<ol class=""tstg_MasterValue"">");
Enumerator e = GetEnumerator();
while (e.MoveNext())
{
string MasterValue = e.Current.Key;
List<string> DetailValues = e.Current.Value;
if (string.IsNullOrEmpty(e.Current.Key))
continue;
Response.AppendFormat(@"<li class=""tstg_DetailValue"">{0}", MasterValue);
if (DetailValues == null)
{
Response.Append("</li>");
continue;
}
Response.Append(MasterDetailDelimiter[0]);
foreach (string DetailValue in this[MasterValue])
Response.AppendFormat("{0}{1}", DetailValue, DetailDelimiter[0]);
RemoveTrailingString(DetailDelimiter[0]);
Response.Append("</li>");
Response.AppendFormat("{0}", MasterDetailBlockDelimeter);
}
RemoveTrailingString(MasterDetailBlockDelimeter[0]);
Response.AppendFormat("</ol>");
}
private void RenderSingleMasterValue()
{
Enumerator e = (Enumerator)this.GetEnumerator();
if (!e.MoveNext())
return;
string MasterValue = e.Current.Key;
List<string> DetailValues = e.Current.Value;
Response = new StringBuilder();
Response.Append(MasterValue);
if ( DetailValues == null )
return;
Response.Append(MasterDetailDelimiter[0]);
foreach (string Detail in DetailValues)
Response.AppendFormat("{0}{1}", Detail, DetailDelimiter[0]);
RemoveTrailingString(DetailDelimiter[0]);
}
public override string ToString()
{
Response = new StringBuilder();
if (this.Keys.Count > 1 )
RenderMultipleMasterValue();
else
RenderSingleMasterValue();
return Response.ToString();
}
public void AddMaster(string MasterValue)
{
if (!ContainsKey(MasterValue))
this.Add(MasterValue, null);
}
public void AddMasterDetail(string MasterValue, string Detail)
{
AddMaster(MasterValue);
if (string.IsNullOrEmpty(Detail))
return;
if (this[MasterValue] == null)
this[MasterValue] = new List<string>();
this[MasterValue].Add(Detail);
}
}
}
I find it interesting that none of the individual classes are particularly complex, the real complexity is keeping track of all the moving parts of the solution.
Keep field projects separate because assembly version info needs to get updated more often. You will be resetting IIS frequently when debugging a custom field. I've found that sometimes doing IISRESET is not enough to clear the field out of the cache, and the only way I've been able to ensure that the latest version of my field's code executes is by incrementing the assembly version number.
Instrument your fields as much as possible because the error handling in SharePoint is poor, and the error messages can be vague.
It’s easiest to deploy custom fields in a solution, look for a future post on how to use NANT to automate the solution creation process.
I’ve created a separate field that supports multiple master values, but didn’t include it in this post. Essentially everything is the same, except for the display control which renders each of the SiteColumn choices in a datagrid with a checkbox next to it. If a choice is selected, the details are displayed next to it in the grid. I think these controls can be wrapped up in a single control, but I haven’t gotten around to refactoring them yet.
Thanks for reading my first ever blog post!
Mahalo,
jt