Friday, November 20, 2009

Phone Number Custom Field Control

SharePoint custom fields are a useful technique for ensuring a consistent UI and data validation rules. Unfortunately, they are a little complex and unwieldy.  The pain can be eased a bit by using the defacto visual studio templates for SharePoint, WSPBuilder. If you don't have it installed, you should really think about going over to codeplex and downloading a copy. In this post I'll explain one implementation of a phone number field. In a nutshell, custom fields are user controls with special supporting classes.




Here are screen shots of how
the field will be displayed

This is the add column screen with
the new field type selected.
















To help you visualize what is going
on, here is a screen shot of the solution
explorer for the project.














The following code snippet is the entirety of the user control we need in this case.
<%@ Control Language="C#" Inherits="PhoneNumberCF.PhoneNumberFieldEditor, PhoneNumberCF, Version=1.0.0.0, Culture=neutral, PublicKeyToken=ec0a1394243e2c9c"    compilationMode="Always" %>


This is the class referenced by the user control. Again very basic.
using System;
using System.Collections.Generic;
using System.Text;
using System.Web.UI;
using System.Web.UI.WebControls;
using Microsoft.SharePoint;
using Microsoft.SharePoint.WebControls;

namespace PhoneNumberCF
{
    public class PhoneNumberFieldEditor : UserControl, IFieldEditor
    {
        // Fields
        private PhoneNumber fldPhoneNumber;

        public void InitializeWithField(SPField field)
        {
            this.fldPhoneNumber = field as PhoneNumber;

            if (this.Page.IsPostBack)
            { return; }
        }


        public void OnSaveChange(SPField field, bool bNewField)
        {
            PhoneNumber lookup = (PhoneNumber)field;
            lookup.IsNew = bNewField;
        }


        // Properties
        public bool DisplayAsNewSection
        {
            get
            { return false; }
        }
    }

}

This is the PhoneNumber class instantiated by the PhoneNumberFieldEditor. There is a good deal more going on here. Luckily most of this is coming straight from the visual studio project and has to do with setting and getting custom properties. The methods to pay attention to here are FieldRenderingControl, GetFieldValue and GetValidatedString. They create the field, return the fields value and validate the data entered into the control.
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.SharePoint;
using Microsoft.SharePoint.WebControls;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Text.RegularExpressions;


namespace PhoneNumberCF
{
    public class PhoneNumber : SPFieldMultiColumn
    {
        private static string[] CustomPropertyNames = new string[] { "CustProp" };

        public PhoneNumber(SPFieldCollection fields, string fieldName)
            : base(fields, fieldName)
        { InitProperties(); }

        public PhoneNumber(SPFieldCollection fields, string typeName, string displayName)
            : base(fields, typeName, displayName)
        { InitProperties(); }


        #region Property storage and bug workarounds - do not edit

        /// <summary>
        /// Indicates that the field is being created rather than edited. This is necessary to
        /// work around some bugs in field creation.
        /// </summary>
        public bool IsNew
        {
            get { return _IsNew; }
            set { _IsNew = value; }
        }
        private bool _IsNew = false;

        /// <summary>
        /// Backing fields for custom properties. Using a dictionary to make it easier to abstract
        /// details of working around SharePoint bugs.
        /// </summary>
        private Dictionary<string, string> CustomProperties = new Dictionary<string, string>();

        /// <summary>
        /// Static store to transfer custom properties between instances. This is needed to allow
        /// correct saving of custom properties when a field is created - the custom property
        /// implementation is not used by any out of box SharePoint features so is really buggy.
        /// </summary>
        private static Dictionary<string, string> CustomPropertiesForNewFields = new Dictionary<string, string>();

        /// <summary>
        /// Initialise backing fields from base property store
        /// </summary>
        private void InitProperties()
        {
            foreach (string propertyName in CustomPropertyNames)
            {
                CustomProperties[propertyName] = base.GetCustomProperty(propertyName) + "";
            }
        }

        /// <summary>
        /// Take properties from either the backing fields or the static store and
        /// put them in the base property store
        /// </summary>
        private void SaveProperties()
        {
            foreach (string propertyName in CustomPropertyNames)
            {
                base.SetCustomProperty(propertyName, GetCustomProperty(propertyName));
            }
        }

        /// <summary>
        /// Get an identifier for the field being added/edited that will be unique even if
        /// another user is editing a property of the same name.
        /// </summary>
        /// <param name="propertyName"></param>
        /// <returns></returns>
        private string GetCacheKey(string propertyName)
        {
            return SPContext.Current.GetHashCode() + "_" + (ParentList == null ? "SITE" : ParentList.ID.ToString()) + "_" + propertyName;
        }

        /// <summary>
        /// Replace the buggy base implementation of SetCustomProperty
        /// </summary>
        /// <param name="propertyName"></param>
        /// <param name="propertyValue"></param>
        new public void SetCustomProperty(string propertyName, object propertyValue)
        {
            if (IsNew)
            {
                // field is being added - need to put property in cache
                CustomPropertiesForNewFields[GetCacheKey(propertyName)] = propertyValue + "";
            }

            CustomProperties[propertyName] = propertyValue + "";
        }

        /// <summary>
        /// Replace the buggy base implementation of GetCustomProperty
        /// </summary>
        /// <param name="propertyName"></param>
        /// <param name="propertyValue"></param>
        new public object GetCustomProperty(string propertyName)
        {
            if (!IsNew && CustomPropertiesForNewFields.ContainsKey(GetCacheKey(propertyName)))
            {
                string s = CustomPropertiesForNewFields[GetCacheKey(propertyName)];
                CustomPropertiesForNewFields.Remove(GetCacheKey(propertyName));
                CustomProperties[propertyName] = s;
                return s;
            }
            else
            {
                return CustomProperties[propertyName];
            }
        }

        /// <summary>
        /// Called when a field is created. Without this, update is not called and custom properties
        /// are not saved.
        /// </summary>
        /// <param name="op"></param>
        public override void OnAdded(SPAddFieldOptions op)
        {
            base.OnAdded(op);
            Update();
        }

        #endregion


        public override BaseFieldControl FieldRenderingControl
        {
            get
            {
                BaseFieldControl fieldControl = new PhoneNumberControl(this);
                fieldControl.FieldName = InternalName;
                return fieldControl;
            }
        }


        public override object GetFieldValue(string value)
        {
            if (String.IsNullOrEmpty(value))
                return null;

            SPFieldMultiColumnValue phoneNumberValue = new SPFieldMultiColumnValue(value);
            return phoneNumberValue;
        }


        public override void Update()
        {
            SaveProperties();
            base.Update();
        }


        public override string GetValidatedString(object value)
        {
            if ((this.Required == true) && (value.ToString() == ""))
            { throw new SPFieldValidationException(this.Title + " must have a value."); }

            SPFieldMultiColumnValue phoneNumberValue = value as SPFieldMultiColumnValue;
            if (phoneNumberValue == null)
                return String.Empty;

            if (String.IsNullOrEmpty(phoneNumberValue[0]) || String.IsNullOrEmpty(phoneNumberValue[1])
                || String.IsNullOrEmpty(phoneNumberValue[2]))
                throw new SPFieldValidationException(SPResource.GetString(Strings.MissingRequiredField));

            Regex threeN = new Regex(@"\d{3}");
            Regex fourN = new Regex(@"\d{4}");

            if (!threeN.IsMatch(phoneNumberValue[0]))
            { throw new SPFieldValidationException("Area Code must be numeric"); }

            if (!threeN.IsMatch(phoneNumberValue[1]))
            { throw new SPFieldValidationException("Phone number must be numeric"); }

            if (!fourN.IsMatch(phoneNumberValue[2]))
            { throw new SPFieldValidationException("Phone number must be numeric"); }

            return base.GetValidatedString(value);


        }
    }

}


This class is very much like web part class. The CreateChildControls method is overridden to add the controls used during adding and editing the field in list items. Here it's three textboxes. The UpdateFieldValueInItem method is responsible for invoking the custom validation and displaying the validation error message if one is generated.
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.SharePoint;
using Microsoft.SharePoint.WebControls;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Text.RegularExpressions;


namespace PhoneNumberCF
{
    public class PhoneNumberControl : BaseFieldControl
    {
        private PhoneNumber field;
        private TextBox p1;
        private TextBox p2;
        private TextBox p3;


        public PhoneNumberControl(PhoneNumber number)
        {
            this.field = number;
            p1 = new TextBox();
            p2 = new TextBox();
            p3 = new TextBox();
        }

        public override object Value
        {
            get
            {
                EnsureChildControls();
                SPFieldMultiColumnValue phoneNumberValue = new SPFieldMultiColumnValue(3);
                phoneNumberValue[0] = p1.Text.Trim();
                phoneNumberValue[1] = p2.Text.Trim();
                phoneNumberValue[2] = p3.Text.Trim();
                return phoneNumberValue;
            }
            set
            {
                EnsureChildControls();
                SPFieldMultiColumnValue phoneNumberValue = value as SPFieldMultiColumnValue;
                p1.Text = phoneNumberValue[0];
                p2.Text = phoneNumberValue[1];
                p3.Text = phoneNumberValue[2];
            }
        }
      

        protected override void CreateChildControls()
        {
            if (this.Field != null && this.ControlMode != SPControlMode.Display)
            {
                base.CreateChildControls();
                p1 = new TextBox();
                p2 = new TextBox();
                p3 = new TextBox();

                p1.MaxLength = 3;
                p1.Width = new Unit(30);
                p2.MaxLength = 3;
                p2.Width = new Unit(30);
                p3.MaxLength = 4;
                p3.Width = new Unit(40);
                this.Controls.Add(p1);
                this.Controls.Add(new LiteralControl("-"));
                this.Controls.Add(p2);
                this.Controls.Add(new LiteralControl("-"));
                this.Controls.Add(p3);
                this.Controls.Add(new LiteralControl("</br>"));
            }
        }


        public override void UpdateFieldValueInItem()
        {
            this.EnsureChildControls();

            try
            {
                this.ItemFieldValue = this.Value;
            }
            catch (Exception ex)
            {
                this.IsValid = false;
                this.ErrorMessage = "* " + ex.Message;
            }
        }

   }
}


This is the xml file that is for the most part generated by the visual studio project. There are two areas of interest. The TypeShortDescription attribute is what is displayed on the add columns screen. The ParentType attribute is important and needs to be set properly for the field to be displayed properly. The last and most important area is the DisplayPattern. This defines the template that will be used to display your custom field.
<?xml version="1.0" encoding="utf-8" ?>
<FieldTypes>
  <FieldType>
    <Field Name="TypeName">PhoneNumber</Field>
    <Field Name="ParentType">MultiColumn</Field>
    <Field Name="TypeDisplayName">Phone Number</Field>
    <Field Name="TypeShortDescription">Phone Number</Field>
    <Field Name="UserCreatable">TRUE</Field>
    <Field Name="Sortable">TRUE</Field>
    <Field Name="AllowBaseTypeRendering">TRUE</Field>
    <Field Name="Filterable">TRUE</Field>
    <Field Name="FieldTypeClass">PhoneNumberCF.PhoneNumber, PhoneNumberCF, Version=1.0.0.0, Culture=neutral, PublicKeyToken=ec0a1394243e2c9c</Field>
    <Field Name="FieldEditorUserControl">/_controltemplates/PhoneNumberFieldEditor.ascx</Field>
    <PropertySchema>
        <Fields>
        </Fields>
    </PropertySchema>
    <RenderPattern Name="DisplayPattern">
        <Switch>
            <Expr>
                <Column />
            </Expr>
            <Case Value="" />
            <Default>
                <HTML><![CDATA[(]]></HTML>
                <Column SubColumnNumber="0" HTMLEncode="TRUE" />
                <HTML><![CDATA[) ]]></HTML>
                <Column SubColumnNumber="1" HTMLEncode="TRUE" />
                <HTML><![CDATA[ - ]]></HTML>
                <Column SubColumnNumber="2" HTMLEncode="TRUE" />
            </Default>
        </Switch>
    </RenderPattern>
</FieldType>
</FieldTypes>

1 comment: