Second Life of a Hungarian SharePoint Geek

November 9, 2011

Setting form field visibility and editability through custom list field iterators

Filed under: Custom forms, Dynamic method, SP 2010 — Tags: , , — Peter Holpar @ 00:27

In a recent post I showed you the basic of SharePoint list form customizations via custom list filed iterators.

In this post I provide a sample that can serve as a base for your form customization needs. From the sample you will learn how to alter the default display mode of the form fields, for example, hide fields on the edit or display form, or display fields as read-only on edit form. As a bonus, I will show you how to inject your code into the saving mechanism of list items and override the default behavior.

Before writing our own code, let’s see how the default field rendering is working. The following code snippet is the CreateChildControls method of the Microsoft.SharePoint.WebControls.ListFieldIterator class.

  1. protected override void CreateChildControls()
  2. {
  3.     this.Controls.Clear();
  4.     if (this.ControlTemplate == null)
  5.     {
  6.         throw new ArgumentException("Could not find ListFieldIterator control template.");
  7.     }
  8.     for (int i = 0; i < base.Fields.Count; i++)
  9.     {
  10.         SPField field = base.Fields[i];
  11.         if (!this.IsFieldExcluded(field))
  12.         {
  13.             TemplateContainer child = new TemplateContainer();
  14.             this.Controls.Add(child);
  15.             child.ControlMode = base.ControlMode;
  16.             child.FieldName = field.InternalName;
  17.             this.ControlTemplate.InstantiateIn(child);
  18.         }
  19.     }
  20. }

As you can see, hiding a field is quite straightforward, you should simply skip the field similarly to the IsFieldExcluded condition. In fact, the IsFieldExcluded property is a useful way to hide fields, but the method shown in the post hopefully provides a more flexible solution.

Displaying the field as read-only on the edit form would be possible through setting the ControlMode property of the TemplateContainer instance in the overriden version of CreateChildControls method.

Unfortunately, both ControlMode and FieldName properties are internal members of the TemplateContainer class, so there is no simple way to work with them in our custom CreateChildControls method.

That is where Reflection or dynamic methods come into the picture. In this case I chose the second one, and created the ILUtils helper class:

  1. public delegate void GenericSetter(object target, object value);
  2. public delegate object GenericGetter(object target);
  3.  
  4. class ILUtils
  5. {
  6.     ///
  7.     /// Creates a dynamic setter for the property
  8.     ///
  9.     public static GenericSetter CreateSetMethod(Type targetType, String propName)
  10.     {
  11.  
  12.         GenericSetter result = null;
  13.  
  14.         PropertyInfo propertyInfo = targetType.GetProperty(propName,
  15.             BindingFlags.NonPublic | BindingFlags.Instance);
  16.  
  17.         if (propertyInfo != null)
  18.         {
  19.             MethodInfo setMethod = propertyInfo.GetSetMethod(true);
  20.             if (setMethod != null)
  21.             {
  22.  
  23.                 Type[] arguments = new Type[2];
  24.                 arguments[0] = arguments[1] = typeof(object);
  25.  
  26.                 DynamicMethod setter = new DynamicMethod(
  27.                   String.Concat("_Set", propertyInfo.Name, "_"),
  28.                   typeof(void), arguments, propertyInfo.DeclaringType);
  29.                 ILGenerator generator = setter.GetILGenerator();
  30.                 generator.Emit(OpCodes.Ldarg_0);
  31.                 generator.Emit(OpCodes.Castclass, propertyInfo.DeclaringType);
  32.                 generator.Emit(OpCodes.Ldarg_1);
  33.  
  34.                 if (propertyInfo.PropertyType.IsClass)
  35.                     generator.Emit(OpCodes.Castclass, propertyInfo.PropertyType);
  36.                 else
  37.                     generator.Emit(OpCodes.Unbox_Any, propertyInfo.PropertyType);
  38.  
  39.                 generator.EmitCall(OpCodes.Callvirt, setMethod, null);
  40.                 generator.Emit(OpCodes.Ret);
  41.  
  42.                 result = (GenericSetter)setter.CreateDelegate(typeof(GenericSetter));
  43.             }
  44.         }
  45.         return result;
  46.     }
  47.  
  48.     ///
  49.     /// Creates a dynamic getter for the property
  50.     ///
  51.     public static GenericGetter CreateGetMethod(Type targetType, String propName)
  52.     {
  53.  
  54.         GenericGetter result = null;
  55.  
  56.         PropertyInfo propertyInfo = targetType.GetProperty(propName,
  57.             BindingFlags.NonPublic | BindingFlags.Instance);
  58.  
  59.         if (propertyInfo != null)
  60.         {
  61.             MethodInfo getMethod = propertyInfo.GetGetMethod(true);
  62.             if (getMethod != null)
  63.             {
  64.  
  65.                 Type[] arguments = new Type[1];
  66.                 arguments[0] = typeof(object);
  67.  
  68.                 DynamicMethod getter = new DynamicMethod(
  69.                   String.Concat("_Get", propertyInfo.Name, "_"),
  70.                   typeof(object), arguments, propertyInfo.DeclaringType);
  71.                 ILGenerator generator = getter.GetILGenerator();
  72.                 generator.DeclareLocal(typeof(object));
  73.                 generator.Emit(OpCodes.Ldarg_0);
  74.                 generator.Emit(OpCodes.Castclass, propertyInfo.DeclaringType);
  75.                 generator.EmitCall(OpCodes.Callvirt, getMethod, null);
  76.  
  77.                 if (!propertyInfo.PropertyType.IsClass)
  78.                     generator.Emit(OpCodes.Box, propertyInfo.PropertyType);
  79.  
  80.                 generator.Emit(OpCodes.Ret);
  81.  
  82.                 result = (GenericGetter)getter.CreateDelegate(typeof(GenericGetter));
  83.             }
  84.         }
  85.  
  86.         return result;
  87.  
  88.     }
  89.  
  90. }

Above idea and most of the code are borrowed from this great post.

We will store display rule exceptions using FieldDisplayRuleItem instances. FieldDisplayRule determines if we should hide the field or display it as read-only. FieldNames and ControlModes stores the field names and control modes the rule applies to. We will see this class in action soon.

  1. // helper class to store display exception rules
  2. // you can add your own properties to extend logic
  3. public class FieldDisplayRuleItem
  4. {
  5.     // list of field names the rule applies to
  6.     public List<String> FieldNames { get; set; }
  7.     // list of control modes the rule applies to
  8.     public List<SPControlMode> ControlModes { get; set; }
  9.     // the resulting display exeption rule
  10.     public FieldDisplayRule Rule { get; set; }
  11. }
  12.  
  13. public enum FieldDisplayRule
  14. {
  15.     Hidden,
  16.     Display
  17. }

Note: you can extend the FieldDisplayRuleItem class as your requirements dictate. For example if the items you work with have a Status field it is common to include this field in the rules. Of course, in this case you should alter the logic that selects the items to get the hidden / read-only fields as illustrated later.

Using the dynamic methods helper class it is easy to create a setter for the ControlMode and FieldName properties of the TemplateContainer class, that we can use in the overriden CreateChildControls method of our base class.

  1. public class BaseListFieldIterator : Microsoft.SharePoint.WebControls.ListFieldIterator
  2. {
  3.     // create dynamic setter methods that wrap the internal
  4.     // ControlMode and FieldName properties of the TemplateContainer class       
  5.     protected static GenericSetter set_TemplateContainer_ControlMode =
  6.             ILUtils.CreateSetMethod(typeof(TemplateContainer), "ControlMode");
  7.     protected static GenericSetter set_TemplateContainer_FieldName =
  8.         ILUtils.CreateSetMethod(typeof(TemplateContainer), "FieldName");
  9.  
  10.     protected List<FieldDisplayRuleItem> _dynamicRules = new List<FieldDisplayRuleItem>();
  11.  
  12.     // get references for the frequently used objects
  13.     protected SPSite _site = SPContext.Current.Site;
  14.     protected SPWeb _web = SPContext.Current.Web;
  15.     protected SPContext _context = SPContext.Current;
  16.     protected SPFormContext _formContext = SPContext.Current.FormContext;
  17.  
  18.     protected override void CreateChildControls()
  19.     {
  20.         this.Controls.Clear();
  21.         if (this.ControlTemplate == null)
  22.         {
  23.             throw new ArgumentException("Could not find ListFieldIterator control template.");
  24.         }
  25.  
  26.         for (int i = 0; i < base.Fields.Count; i++)
  27.         {
  28.             SPField field = base.Fields[i];
  29.             String fieldName = field.InternalName;
  30.  
  31.             // check if the current field is on the list of "hidden" fields in the current display mode
  32.             // or whether there is a "global" rule to hide fields
  33.             FieldDisplayRuleItem exception = _dynamicRules.FirstOrDefault(
  34.                 // empty (null) value means there is no restriction for the control mode
  35.                 e => ((e.ControlModes == null) || (e.ControlModes.Contains(ControlMode))) &&
  36.                 // empty (null) value means there is no restriction for the field name
  37.                 ((e.FieldNames == null) || (e.FieldNames.Contains(fieldName))) &&
  38.                 (e.Rule == FieldDisplayRule.Hidden));
  39.  
  40.             if ((!this.IsFieldExcluded(field)) && (exception == null))
  41.             {
  42.                 TemplateContainer child = new TemplateContainer();
  43.                 this.Controls.Add(child);
  44.                 SPControlMode controlMode = GetControlMode(fieldName);
  45.                 // use the dynamic setter to access internal properties
  46.                 set_TemplateContainer_ControlMode(child, controlMode);
  47.                 set_TemplateContainer_FieldName(child, fieldName);
  48.                 this.ControlTemplate.InstantiateIn(child);
  49.             }
  50.         }
  51.     }
  52.  
  53.     private SPControlMode GetControlMode(string fieldName)
  54.     {
  55.  
  56.         FieldDisplayRuleItem rule = _dynamicRules.FirstOrDefault(
  57.             e => ((e.ControlModes == null) || (e.ControlModes.Contains(ControlMode))) &&
  58.             ((e.FieldNames == null) || (e.FieldNames.Contains(fieldName))) &&
  59.             (e.Rule == FieldDisplayRule.Display));
  60.  
  61.         SPControlMode result = (rule == null) ? ControlMode : SPControlMode.Display;
  62.  
  63.         return result;
  64.     }
  65.  
  66. }

Instead of using string literals in my methods, I usually use static classes to store string constants like field or list names:

  1. public static class SampleTaskListFields
  2. {
  3.     public static readonly String Id = "ID";
  4.     public static readonly String Title = "Title";
  5.     public static readonly String PercentComplete = "PercentComplete";
  6.     public static readonly String AssignedTo = "AssignedTo";
  7.     public static readonly String Status = "Status";
  8.     public static readonly String Predecessors = "Predecessors";
  9.     public static readonly String StartDate = "StartDate";
  10.     public static readonly String DueDate = "DueDate";
  11. }
  12.  
  13. public static class Lists
  14. {
  15.     public static readonly String SampleList = "SampleTaskList";
  16. }
  17.  
  18.  
  19. public static class TaskStates
  20. {
  21.     // we only declare the state names used in the sample
  22.     public static readonly String NotStarted = "Not Started";
  23.     public static readonly String Completed = "Completed";
  24. }

If you prefer working with Guids then you may consider using the SPBuiltInFieldId class instead.

The exact implementation is derived from the BaseListFieldIterator class. We store static (global) rules in _staticRules and dynamic rules in _dynamicRules.

In the current example we are working with a task list and would like to achieve the following results:

Static rules:

  • The Title field can be edited only when the task is created.
  • Status and % Complete fields are hidden on task creation (we will set this values from our custom save handler method later).

Dynamic rules:

  • Predecessors field is only editable by site administrators.
  • Start Date is only editable if the item is in the Not Started status.

The following code creates the rules for these requirements.

  1. public class ListFieldIterator : BaseListFieldIterator
  2. {
  3.  
  4.     protected static List<FieldDisplayRuleItem> _staticRules = new List<FieldDisplayRuleItem>();
  5.  
  6.     static ListFieldIterator()
  7.     {
  8.  
  9.         // task title can be set on item creation
  10.         // but can't be altered later
  11.         _staticRules.Add(
  12.              new FieldDisplayRuleItem
  13.              {
  14.                  FieldNames = new List<String>() { SampleTaskListFields.Title },
  15.                  ControlModes = new List<SPControlMode>() { SPControlMode.Edit },
  16.                  Rule = FieldDisplayRule.Display
  17.              });
  18.  
  19.         // we don't allow to set status and % info on new item form
  20.         // items will be created with status "Not Started" and 0 %
  21.         // these values are set on item saving from code
  22.         _staticRules.Add(
  23.              new FieldDisplayRuleItem
  24.              {
  25.                  FieldNames = new List<String>() { SampleTaskListFields.Status, SampleTaskListFields.PercentComplete },
  26.                  ControlModes = new List<SPControlMode>() { SPControlMode.New },
  27.                  Rule = FieldDisplayRule.Hidden
  28.              });
  29.     }
  30.  
  31.  
  32.     protected override void OnInit(EventArgs e)
  33.     {
  34.         base.OnInit(e);
  35.  
  36.         // register save handler if not in display mode and form is posted back
  37.         if ((Page.IsPostBack) && (ControlMode != SPControlMode.Display))
  38.         {
  39.             _formContext.OnSaveHandler += new EventHandler(SaveHandler);
  40.         }
  41.  
  42.         CreateDynamicExceptions();
  43.  
  44.     }
  45.  
  46.     private void CreateDynamicExceptions()
  47.     {
  48.         _dynamicRules.AddRange(_staticRules);
  49.  
  50.         // only site admins are allowed to edit predecessor tasks
  51.         // others can see only the predecessors regardless of the control mode
  52.         if (!_web.CurrentUser.IsSiteAdmin)
  53.         {
  54.             _dynamicRules.Add(
  55.                 new FieldDisplayRuleItem
  56.                 {
  57.                     FieldNames = new List<String>() { SampleTaskListFields.Predecessors },
  58.                     Rule = FieldDisplayRule.Display
  59.                 });
  60.         }
  61.  
  62.         // start date can be set only if the task is not started
  63.         if ((String)Item[SampleTaskListFields.Status] != TaskStates.NotStarted)
  64.         {
  65.             _dynamicRules.Add(
  66.                 new FieldDisplayRuleItem
  67.                 {
  68.                     FieldNames = new List<String>() { SampleTaskListFields.StartDate },
  69.                     Rule = FieldDisplayRule.Display
  70.                 });
  71.         }
  72.  
  73.     }
  74.  
  75.     protected void SaveHandler(object sender, EventArgs e)
  76.     {
  77.         Page.Validate();
  78.  
  79.         if (Page.IsValid)
  80.         {
  81.             // do custom activities, send mail, create task,
  82.             // set permissions etc.
  83.  
  84.             // new tasks are created with status 'Not Started' and 0% complete
  85.             if (ControlMode == SPControlMode.New)
  86.             {
  87.                 Item[SampleTaskListFields.PercentComplete] = 0;
  88.                 Item[SampleTaskListFields.Status] = TaskStates.NotStarted;
  89.             }
  90.  
  91.             // we should save the item explicitly
  92.             Item.Update();
  93.         }
  94.     }
  95. }

In the OnInit method we register of custom save handler method. In the SaveHandler method we set the values for the Status and % Complete fields if the item is a new task. That is the method you can include your custom actions into, like sending mails or setting permissions on items.

To test the solution, we should first deploy the assembly as well as our user control. Then create a Tasks list called SampleTaskList, and register our custom ListFieldIterator to all of its forms as shown in my former post.

image

Logged on as a site admin, we create a task item.

image

Although the Status and % Complete fields are not visible on the form, the values of the fields are populated.

image

Next, we create another task item, and set the first one as a predecessor.

image

After saving the item, we open it again in edit mode. The Title field displayed as read-only.

image

We log in as a standard site member (non-admin), and open the item once again. The Predecessors field is also read-only in this case. We set the Status to In Progress.

image

After re-opening the item we see that the Start Date field became read-only.

image 

I hope this example helps you to create SharePoint forms that fit better to the common customer needs. In the following posts I’m planning to build on this sample with other interesting features.

You can download the sample application from here.

About these ads

4 Comments »

  1. Thanks Peter this wonderful post and sharing Knowledge.I am trying to create class library for baseFieldIterator and then use this in sharepoint project but its not working.i mean Form load but display nothing if i remove CustomFiledIterator from Tempate its work.

    Please advise

    Comment by Ronak — November 15, 2011 @ 20:13

    • Just Update if i put everything Under one Project it works but if try to make dll and then use it doesn’t work.

      Comment by Ronak — November 15, 2011 @ 20:24

      • Ronak, try to download the sample application I’ve published here.

        Comment by Peter Holpar — November 15, 2011 @ 21:01

  2. Thanks Peter i am loving this u just open lost of opportunities to customize sharepoint List forms and just in time for me as i have req to hide fields based on member ship within group.
    Once again thanks for Sample code.

    Comment by Ronak — November 15, 2011 @ 21:26


RSS feed for comments on this post. TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Theme: Shocking Blue Green. Get a free blog at WordPress.com

Follow

Get every new post delivered to your Inbox.

Join 54 other followers

%d bloggers like this: