Thursday, October 10, 2013

EPiServer 7: Attribute for rendering generic drop-down list or check-boxes

Ever wanted to help your EPiServer 7 editor with choosing from a set of options in edit mode? In this example we work with the hypothetical scenario of letting an editor chose to display teaser or not (and in the next post I will demonstrate how to set default values with property attributes). We achieve this by utilizing a generic data annotation attribute for a property and configuring editors for EPiServer 7.

The image above depicts what we will achieve, a simple drop-down list with the options "Yes" and "No" (we'll discuss the default in the next post).

However, this generic attribute and editor configuration we will create will also handle rendering the options as check-boxes, such as the image below.


First we create the attribute.

   1:  [AttributeUsage(AttributeTargets.Property)]
   2:  public class ListItemsAttribute : Attribute
   3:  {
   4:      public enum ControlRenderType { Dropdown, Checkboxes };
   5:   
   6:      public IEnumerable<ISelectItem> Items { get; protected set; }
   7:   
   8:      public ControlRenderType ControlType { get; protected set; }
   9:   
  10:   
  11:      public ListItemsAttribute(string listItems)
  12:          : this(listItems, ControlRenderType.Dropdown, true)
  13:      { }
  14:   
  15:      public ListItemsAttribute(string listItems, ControlRenderType controlType)
  16:          : this(listItems, controlType, true)
  17:      { }
  18:   
  19:      public ListItemsAttribute(string listItems, ControlRenderType controlType, bool addEmpty)
  20:      {
  21:          if (string.IsNullOrWhiteSpace(listItems))
  22:              throw new Exception("List items string cannot be null or empty");
  23:   
  24:          var selectItems = new List<SelectItem>();
  25:   
  26:          string[] items = listItems.Split(new char[] { ';' });
  27:          foreach (string item in items)
  28:          {
  29:              string[] pair = item.Split(new char[] { ':' });
  30:                  
  31:              if (pair.Length > 2)
  32:                  throw new Exception("List items string contains faulty item");
  33:   
  34:              string text = pair[0];
  35:              string value = pair.Length == 2 ? pair[1] : pair[0];
  36:   
  37:              selectItems.Add(new SelectItem() { Text = text, Value = value });
  38:          }
  39:   
  40:          if (addEmpty && controlType == ControlRenderType.Dropdown)
  41:              selectItems.Insert(0, new SelectItem());
  42:   
  43:          Items = selectItems;
  44:   
  45:          ControlType = controlType;
  46:      }
  47:  }

The idea here is to add a string with the options as well as the type to be rendered (either drop-down list or check-boxes) and if we should add an empty selection to the drop-down list or not.

Basically a string with this pattern "Text1:Value1;Text2:Value2;TextN;ValueN" is passed into a the constructor and parsed into the property Items of IEnumerable<ISelectItem>, as displayed below.


   1:  [ListItems("Yes (default):Y;No:N", ListItemsAttribute.ControlRenderType.Dropdown)]
   2:  [UIHint("ListItems")]
   3:  public virtual string DisplayTeasers { get; set; }

Ok, so now we have the attribute but now we need to configure the editors in order to render the property properly in edit mode. Basically you inherit the EditorDescriptor class and override the ModifyMetadata method. The hook-up that connects the property (in this example "DisplayTeasers") and the EditorDescriptor is made by both the property's attribute and the EditorDescriptor class EditorDescriptorRegistration UIHint having the same value of, in this example, "ListItems".


   1:  [EditorDescriptorRegistration(TargetType = typeof(string), UIHint = "ListItems")]
   2:  public class ListItemsEditorDescriptor : EditorDescriptor
   3:  {
   4:      public override void ModifyMetadata(ExtendedMetadata metadata, IEnumerable<Attribute> attributes)
   5:      {
   6:          SelectionFactoryType = typeof(ListItemsSelectionFactory);
   7:              
   8:          ListItemsAttribute.ControlRenderType controlType = ListItemsSelectionFactory.GetListItemsControlType(metadata);
   9:          switch (controlType)
  10:          {
  11:              case ListItemsAttribute.ControlRenderType.Dropdown:
  12:                  ClientEditingClass = "epi/cms.contentediting.editors.SelectionEditor";
  13:                  break;
  14:              case ListItemsAttribute.ControlRenderType.Checkboxes:
  15:                  ClientEditingClass = "epi/cms.contentediting.editors.CheckBoxListEditor";
  16:                  break;
  17:          }
  18:   
  19:          base.ModifyMetadata(metadata, attributes);
  20:      }
  21:  }

Based on the ListItemsAttribute and its ControlRenderType we either render a drop-down list or check-boxes. We also specify a custom SelectionFactoryType, as described below, and it is this piece of code that supplies the editor control with the specified options.


   1:  public class ListItemsSelectionFactory : ISelectionFactory
   2:  {
   3:      public IEnumerable<ISelectItem> GetSelections(ExtendedMetadata metadata)
   4:      {
   5:          ListItemsAttribute attribute = GetListItemsAttribute(metadata);
   6:   
   7:          return attribute == null
   8:              ? new List<SelectItem>()
   9:              : attribute.Items;
  10:      }
  11:   
  12:      public static ListItemsAttribute GetListItemsAttribute(ExtendedMetadata metadata)
  13:      {
  14:          return metadata.Attributes.FirstOrDefault(a => typeof(ListItemsAttribute) == a.GetType()) as ListItemsAttribute;
  15:      }
  16:   
  17:      public static ListItemsAttribute.ControlRenderType GetListItemsControlType(ExtendedMetadata metadata)
  18:      {
  19:          ListItemsAttribute attribute = GetListItemsAttribute(metadata);
  20:   
  21:          return attribute == null
  22:              ? ListItemsAttribute.ControlRenderType.Dropdown
  23:              : attribute.ControlType;
  24:      }
  25:  }

We read the the ExtendedMetadata variable supplied in the method and get the ListItemsAttribute which contains all the items to be rendered.

And all of this together let us define an input field in edit mode with a data annotation attribute.

No comments:

Post a Comment