Skip to main content

Turim's Blog

Go Search
TurimSaint.com
Turim's Blog
Saint's Blog
  

> Turim's Blog
Easily build a rich UI for a web part without using SmartPart

Hey Folks,

I'm trying to keep my promise to myself to post once/week, so hopefully this will suffice.  Today I'm going to write about enabling design surface support in visual studio for web part development. 

For this post we'll make a simple echo web part that incorporates a user control which you can easily add asp.net server controls and HTML markup. I find this makes developing web parts much easier than having to programmatically write out HTML in the webpart's CreateChildControl method. Furthermore by developing a non-compiled user control you will have more control over the web part's layout.


 

 

 

 

 

 

 

 

 

I am assuming that you have created a web part before, so I won't go into the details on the various plumbing, or deployment methods. There is plenty of information on how to start building web parts already out there on the web, you can start here. The project is comprised of the following moving parts:

  1. EchoWebPart.xml (more plumbing, where to store the web part in the site when activated via a feature)
  2. EchoWebPart.webpart (plumbing the file that defines the web part)
  3. EchoWebPart.cs (the web part class – traditionally, all UI is created in this classes CreateChildControl's method)
  4. EchoWebPartControl.ascx (the user control with design surface)
  5. EchoWebPartControl.ascx.cs (the UserControl class – i.e. the user control code-behind file)

EchoWebPart.xml

This file simply instructs SharePoint where and how to store the uploaded web part. This file isn't necessary if you upload the web part directly to SharePoint.

<?xml version="1.0" encoding="utf-8" ?>

<Elements

Id="3FF674B2-ACD1-473d-8823-8EB6D60F1E32"

xmlns="http://schemas.microsoft.com/sharepoint/" >

<Module Name="WebParts" List="113" Url="_catalogs/wp">

<File Path="EchoWebPart/EchoWebPart.webpart"

Url="EchoWebPart.webpart"

Type="GhostableInLibrary"

/>

</Module>

</Elements>

 

EchoWebPart.webpart

More web part plumbing, if you upload the web part directly to SharePoint this file is generated automatically.

<?xml version="1.0" encoding="utf-8"?>

<webParts>

<webPart xmlns="http://schemas.microsoft.com/WebPart/v3">

<metaData>

<type name="TurimSaint.Blog.Turim.EchoWebPart, BlogDemos, Version=1.0.0.0, Culture=neutral, PublicKeyToken=baa61394caf47627" />

<importErrorMessage>Cannot import Echo Web Part.</importErrorMessage>

</metaData>

<data>

<properties>

<property name="Title" type="string">Echo Web Part</property>

<property name="Description" type="string">Demonstrating how to enable a Visual Design Surface for web part development</property>

</properties>

</data>

</webPart>

</webParts>

EchoWebPartControl.ascx

Here's the UI for the control, a very simple ASP.NET user control. The only thing special here is to get the fully qualified name of the web part assembly (this assembly). Also not that the Inherits attribute of the Control directive specifies the control class definition.

<%@ Assembly Name="BlogDemos, Version=1.0.0.0, Culture=neutral, PublicKeyToken=baa61394caf47627" %>

<%@ Control Language="C#" Inherits="TurimSaint.Blog.Turim.EchoWebPartControl" %>

<div>

Echo: <asp:TextBox runat="server" ID="txtEchoText" />

<br /><br />

<asp:Label runat="server" ID="lblOutput" />

<br />

<asp:Button runat="server" ID="btnEcho" OnClick="btnEcho_Click" Text="Echo" />

</div>

EchoWebPartControl.ascx.cs

This is the Control class definition. There are some subtleties here, basically we need to manually wire up the page controls as properties in the class. The user control also exposes a property matching the web part configuration property just to demonstrate how this can be done.

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Web.UI;

using System.Web.UI.WebControls;

 

namespace TurimSaint.Blog.Turim

{

public class EchoWebPartControl : UserControl

{

/*

* Wire up the page server controls

*/

protected TextBox txtEchoText

{

get

{ return FindControl("txtEchoText") as TextBox; }

}

 

protected Label lblOutput

{

get

{ return FindControl("lblOutput") as Label; }

}

 

protected Button btnEcho

{

get

{ return FindControl("btnEcho") as Button; }

}

/*

* End server controls

*/

 

/*

* This is a web part configuration property, it will be set when the web part creates this user control

*/

public string EchoString

{

get

{

if (string.IsNullOrEmpty(ViewState["EchoString"].ToString()))

EchoString = string.Empty;

 

return ViewState["EchoString"].ToString();

}

 

set

{ ViewState["EchoString"] = value; }

}

 

protected override void CreateChildControls()

{

        //nothing to see here

}

 

protected void btnEcho_Click(object sender, EventArgs e)

{

        //just some output

lblOutput.Text =

string.Format("[{0} {1}] <b>{2}</b>",

DateTime.Now.ToLongTimeString(),

EchoString,

txtEchoText.Text

);

}

}

}

 

EchoWebPart.cs

This is my very simple web part class; it simply contains a single configuration property, and the CreateChildControls method, which is very simple and where the magic happens. We simply load a traditional ASP.NET user control from a well known location, and add it to the web parts Control collection. I've also included a bogus method CreateChildControls_TRADITIONAL which shows how'd you have to build the same UI strictly in code.

using System;

using System.Runtime.InteropServices;

using System.Web.UI;

using System.Web.UI.WebControls;

using System.Web.UI.WebControls.WebParts;

using System.Xml.Serialization;

using Microsoft.SharePoint;

using Microsoft.SharePoint.Utilities;

using Microsoft.SharePoint.WebControls;

using Microsoft.SharePoint.WebPartPages;

using System.ComponentModel;

namespace TurimSaint.Blog.Turim

{

[Guid("C9937F13-A985-4386-987E-9970F9A82E3E")]

public class EchoWebPart : Microsoft.SharePoint.WebPartPages.WebPart

{

private string _es = string.Empty;

[WebBrowsable(true)]

[Personalizable(PersonalizationScope.Shared)]

[SPWebCategoryName("Echo Web Part")]

[Description("A string that is emitted with the echo web part")]

[FriendlyName("Echo string")]

[DefaultValue("default echo string")]

public string EchoString

{

get

{ return _es; }

 

set

{ _es = value; }

}

 

/*

*

* Easy

*

*/

protected override void CreateChildControls()

{

EchoWebPartControl l_ewpc = Page.LoadControl("/_layouts/TurimSaint/TurimBlog/EchoWebPartControl.ascx") as EchoWebPartControl;

l_ewpc.EchoString = EchoString;

Controls.Add(l_ewpc);

}

 

/*

*

* Hard

*

*/

protected override void CreateChildControls_TRADITIONAL()

{

LiteralControl l_container = new LiteralControl("div");

TextBox txtEcho = new TextBox();

txtEcho.ID = "txtEchoText";

l_container.Controls.Add(txtEcho);

 

LiteralControl l_breaks = new LiteralControl("<br /><br />");

l_container.Controls.Add(l_breaks);

 

Label l_lblOutput = new Label();

l_lblOutput.ID = "lblOutput";

l_container.Controls.Add(l_lblOutput);

 

LiteralControl l_break = new LiteralControl("<br />");

l_container.Controls.Add(l_break);

 

Button l_btnEcho = new Button();

l_btnEcho.ID = "btnEcho";

l_btnEcho.Text = "Echo";

l_btnEcho.Click += new EventHandler(l_btnEcho_Click);

}

 

void l_btnEcho_Click(object sender, EventArgs e)

{

//implement handler

}

}

}

Wrap Up

There is one issue that you should be aware of, code access security. If you plan on deploying your web part the bin directory than you will need to modify the web.config to increase the trust under which the web application runs. The problem occurs when the web part class (in this example: TurimSaint.Blog.Turim.EchoWebPart) tries to load the user control. Unfortunately, the trust setting WSS_Medium isn't high enough to accommodate this operation. The best practice is too create your own trust policy (all gory details to be found here), but I haven't dug in to figure out the least privileged settings. Of course the FULL trust setting works fine.

 

Thanks for reading,

Mahalo!

jt

MasterDetail Field: an alternative solution for cascading dropdowns

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.

Solution Concept & Design

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.

 

none selected

Figure 1: MasterDetail field with no Master record selected

 

master selected

Figure 2: MasterDetail field displaying Master and associated Details

 

master selected multiple details

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

 

master selected no details

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.

 

master site column

Figure 5: Master choice type site column

 

ice cream

Figure 6: Detail choice type site column

cake

Figure 7: Detail choice type site column

cookie

Figure 8: Detail choice type site column

 

Field Type Definition

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.

list settings

Figure 9: Field Type Definition attributes on List Settings page

 add column

Figure 10: Field Type Definition attributes on Add Column page

Custom Field Properties

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.

Field Class

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("&nbsp;", " ");

        }

 

        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;

            }

        }

    }

}

 

Admin Control

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>

list admin page

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

    }

}

 

Display Control

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;

        }

    }

}

 

Value Class

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.

values

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 = { "&nbsp;<br/>" };

        private readonly string[] MasterDetailDelimiter = { ":&nbsp;" };

        private readonly string[] DetailDelimiter = { ",&nbsp;" };

 

        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);

        }

    }

}

 

Wrap Up

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

 ‭(Hidden)‬ Admin Links