Tuesday, November 5, 2013

EPiServer 6: Sharing plugins between applications

In this example I will demonstrate the possibility to share EPiServer plugins between applications. The idea is to make this as seamless as possible without configuration or having to add front-end files into every project.

What we will do is to create a class library project, build a release DLL and put this DLL into the bin folder of another project. And all of this will produce a new admin tool as depicted below.


Create the plugin


We create the plugin and what is worth noting here is the Url on line 4 (this is used with the route helper, see below) and the inheritance of the IPageTypeUsage interface on line 5.


   1:  [GuiPlugIn(DisplayName = "Page Type Usage",
   2:              Description = "See the usage of a selected page type.",
   3:              Area = PlugInArea.AdminMenu,
   4:              Url = "/PageTypeUsage.aspx")]
   5:  public class PageTypeUsage : Page, IPageTypeUsage

We will not use an ASPX file at all but instead add all the controls needed to the ControlCollection of the plugin page. And this must be added in the OnInit method in order to be able to use them properly.


   1:  protected override void OnInit(EventArgs e)
   2:  {
   3:      AddControls();
   4:      InitPageTypesDropDown();
   5:      base.OnInit(e);
   6:  }

A little example of the AddControls method. Once added they will render properly without us having to doing anything in particular.


   1:  private void AddControls()
   2:  {
   3:      HtmlGenericControl html = new HtmlGenericControl("html");
   4:      HtmlHead head = new HtmlHead();
   5:      HtmlTitle title = new HtmlTitle { Text = "Page Type Usage" };
   6:      HtmlGenericControl javascript = ControlHelper.GetJavaScriptLinkControl("/util/javascript/system.js");
   7:      HtmlGenericControl css1 = ControlHelper.GetCssLinkControl("/epi/Shell/ClientResources/ShellCore.css");
   8:      Controls.Add(html);
   9:      html.Controls.Add(head);
  10:      head.Controls.Add(title);
  11:      head.Controls.Add(javascript);
  12:      head.Controls.Add(css1);

Route handler


In order for this to work without using an ASPX file and having a virtual path we will use the power of Routes.


   1:      public class RouteHandler : PlugInAttribute, IRouteHandler
   2:      {
   3:          #region IRouteHandler Members
   4:          public IHttpHandler GetHttpHandler(RequestContext requestContext)
   5:          {
   6:              var interfaceType = typeof(IPageTypeUsage);
   7:              var classType = AppDomain.CurrentDomain.GetAssemblies().SelectMany(s => s.GetTypes()).First(p => interfaceType.IsAssignableFrom(p) && p.IsClass);
   8:              return Activator.CreateInstance(classType) as IHttpHandler;
   9:          }
  10:          #endregion
  11:   
  12:          public static void RegisterRoutes()
  13:          {
  14:              RouteTable.Routes.Add(new Route("PageTypeUsage.aspx", new RouteHandler()));
  15:          }
  16:   
  17:          public static void Start()
  18:          {
  19:              RegisterRoutes();
  20:          }
  21:      }

By inheriting PlugInAttribute on line 1 we can access the Start method (on line 17) and register the routes. And we register the route "PageTypeUsage.aspx" - which is the same as we specified on the plugin. Once this route is hit or accessed the GetHttpHandler method on line 4 is used. Basically this utilize a reflection search of the interface IPageTypeUsage and instantiates a PageTypeUsage object (on line 8).

And voila! We have a plugin that we can share between different projects!

Tuesday, October 29, 2013

EPiServer 7: A custom property combined with attributes and a custom EditorDescriptor - UPDATED

In this post I will demonstrate the possibility to create an EPiServer 7 custom property and combine it with property attributes for edit mode configuration (which is utilized by a custom EditorDescriptor).

This entire post has been revised and updated to work with EPiServer 7.5.

What I have created is a custom property to let the editor create key-value items that can be used to render, for instance, a drop-down list.

Overview

In order to achieve this we have to create the files marked in blue in the image below. And I will go trough each and every one of them to try and explain how to create a custom property in EPiServer 7.5.



StartPage.cs


   1:  [ContentType(GUID = "333e2a8b-46d4-4ed9-8f05-ac4a3c09058e", GroupName = SystemTabNames.Content)]
   2:  public class StartPage : PageData
   3:  {
   4:      [Display(GroupName = SystemTabNames.Content, Order = 100)]
   5:      [BackingType(typeof(PropertyKeyValueItems))]
   6:      [KeyValueItems("Name", "E-mail", "Add", "X", "", "", @"^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$", "The entered value is not valid. Must be a valid e-mail.")]
   7:      public virtual IEnumerable<KeyValueItem> KeyValueItems { get; set; }
   8:  }

Three things are of importance here.
1. The BackingType attribute specified on line 5 which is specifying what property to use when retrieving and storing data.
2. The KeyValueItems attribute on line 6 which is the attribute that lets us add specific parameters for this particular property
3. The IEnumerable on line 7 which match the type used in the backing type property


PropertyKeyValueItems.cs


   1:  [PropertyDefinitionTypePlugIn(Description = "A property for list of key-value-items.", DisplayName = "Key-Value Items")]
   2:  public class PropertyKeyValueItems : PropertyLongString
   3:  {
   4:      public override Type PropertyValueType
   5:      {
   6:          get { return typeof(IEnumerable<KeyValueItem>); }
   7:      }
   8:   
   9:      public override object Value
  10:      {
  11:          get
  12:          {
  13:              var value = base.Value as string;
  14:              if (value == null) { return null; }
  15:              JavaScriptSerializer serializer = new JavaScriptSerializer();
  16:              return serializer.Deserialize(value, typeof(IEnumerable<KeyValueItem>));
  17:          }
  18:          set
  19:          {
  20:              if (value is IEnumerable<KeyValueItem>)
  21:              {
  22:                  JavaScriptSerializer serializer = new JavaScriptSerializer();
  23:                  base.Value = serializer.Serialize(value);
  24:              }
  25:              else { base.Value = value; }
  26:          }
  27:      }
  28:  }
  29:   
  30:  public class KeyValueItem
  31:  {
  32:      public string Key { get; set; }
  33:      public string Value { get; set; }
  34:  }

I have edited the file and in the above code only display the essential (the source code for all files are found at the botton of this post).

The are two classes here: 1. PropertyKeyValueItems, 2. KeyValueItem.

PropertyKeyValueItems is used to retrieve and store the value from editor input. Since the property is of type 'IEnumerable' as specified in property 'PropertyValueType' but it does in fact inherit 'PropertyLongString' we need to serialize and deserialize the data and this is done in property 'Value'. The 'KeyValueItem' class is simply used to help with the serialization and deserialization.

KeyValueItemsAttribute.cs


   1:  [AttributeUsage(AttributeTargets.Property)]
   2:  public class KeyValueItemsAttribute : Attribute
   3:  {
   4:      public string KeyLabel { get; set; }
   5:      public string ValueLabel { get; set; }
   6:      public string AddButtonLabel { get; set; }
   7:      public string RemoveButtonLabel { get; set; }
   8:      public string KeyValidationExpression { get; set; }
   9:      public string ValueValidationExpression { get; set; }
  10:      public string KeyValidationMessage { get; set; }
  11:      public string ValueValidationMessage { get; set; }
  12:   
  13:      public KeyValueItemsAttribute(string keyLabel, 
  14:                                      string valueLabel, 
  15:                                      string addButtonLabel, 
  16:                                      string removeButtonLabel, 
  17:                                      string keyValidationExpression,
  18:                                      string keyValidationMessage,
  19:                                      string valueValidationExpression,
  20:                                      string valueValidationMessage)
  21:      {
  22:          KeyLabel = keyLabel;
  23:          ValueLabel = valueLabel;
  24:          AddButtonLabel = addButtonLabel;
  25:          RemoveButtonLabel = removeButtonLabel;
  26:          KeyValidationExpression = keyValidationExpression;
  27:          KeyValidationMessage = keyValidationMessage;
  28:          ValueValidationExpression = valueValidationExpression;
  29:          ValueValidationMessage = valueValidationMessage;
  30:      }
  31:  }

This is a class used to dress up a specific property. In this case we say that the value the key is 'Name' and the value is 'E-mail'. We send all of these calues up to the front layer with the help of the EditorDescriptor coming next.

KeyValueItemsEditorDescriptor.cs


   1:  [EditorDescriptorRegistration(TargetType = typeof(IEnumerable<KeyValueItem>))]
   2:  public class KeyValueItemsEditorDescriptor : EditorDescriptor
   3:  {
   4:      public KeyValueItemsEditorDescriptor()
   5:      {
   6:          ClientEditingClass = "episerver75.editors.KeyValueItems";
   7:      }
   8:   
   9:      protected override void SetEditorConfiguration(ExtendedMetadata metadata)
  10:      {
  11:          var keyValueItemsAttribute = metadata.Attributes.FirstOrDefault(a => typeof(KeyValueItemsAttribute) == a.GetType()) as KeyValueItemsAttribute;
  12:          if (keyValueItemsAttribute != null)
  13:          {
  14:              if (!String.IsNullOrEmpty(keyValueItemsAttribute.KeyLabel))
  15:                  EditorConfiguration["keyLabel"] = keyValueItemsAttribute.KeyLabel;
  16:              if (!String.IsNullOrEmpty(keyValueItemsAttribute.ValueLabel))
  17:                  EditorConfiguration["valueLabel"] = keyValueItemsAttribute.ValueLabel;
  18:              if (!String.IsNullOrEmpty(keyValueItemsAttribute.AddButtonLabel))
  19:                  EditorConfiguration["addButtonLabel"] = keyValueItemsAttribute.AddButtonLabel;
  20:              if (!String.IsNullOrEmpty(keyValueItemsAttribute.RemoveButtonLabel))
  21:                  EditorConfiguration["removeButtonLabel"] = keyValueItemsAttribute.RemoveButtonLabel;
  22:              if (!String.IsNullOrEmpty(keyValueItemsAttribute.KeyValidationExpression))
  23:                  EditorConfiguration["keyValidationExpression"] = keyValueItemsAttribute.KeyValidationExpression;
  24:              if (!String.IsNullOrEmpty(keyValueItemsAttribute.KeyValidationMessage))
  25:                  EditorConfiguration["keyValidationMessage"] = keyValueItemsAttribute.KeyValidationMessage;
  26:              if (!String.IsNullOrEmpty(keyValueItemsAttribute.ValueValidationExpression))
  27:                  EditorConfiguration["valueValidationExpression"] = keyValueItemsAttribute.ValueValidationExpression;
  28:              if (!String.IsNullOrEmpty(keyValueItemsAttribute.ValueValidationMessage))
  29:                  EditorConfiguration["valueValidationMessage"] = keyValueItemsAttribute.ValueValidationMessage;
  30:          }
  31:          base.SetEditorConfiguration(metadata);
  32:      }
  33:  }

This is the piece of code that binds everything together (back-end with front-end). First we need to specify that whenever editing a property of the type 'IEnumerable' then this is the code to use (line 1). In the constructor we specify which front-end code to use (line 6, more on this path late i 'module.config'). Third we override the 'SetEditorConfiguration' method and simply reads the 'KeyValueItemsAttribute' and its properties and add them to the EditorConfiguration property we get when inheriting the 'EditorDescriptor' class.

module.config


   1:  <?xml version="1.0" encoding="utf-8"?>
   2:  <module>
   3:      <clientResources>
   4:          <add name="epi-cms.widgets.base" path="Styles/Styles.css" resourceType="Style"/>
   5:      </clientResources>
   6:      <dojo>
   7:          <paths>
   8:              <add name="episerver75" path="Scripts" />
   9:          </paths>
  10:      </dojo>
  11:  </module>

This config file specifies the base path for the client resources used by our custom front-end code (on line 8). This is what makes the 'ClientEditingClass = "episerver75.editors.KeyValueItems";' work in the EditorDescriptor class described above.

Style.css

Any styles needed for the front-end custom property.

KeyValueItems.js


   1:  define(
   2:  "episerver75/editors/KeyValueItems",
   3:  [
   4:  "dojo/_base/array",
   5:  "dojo/_base/declare",
   6:  "dojo/_base/lang",
   7:  "dojo/_base/json",
   8:  "dojo/query",
   9:  "dojo/dom-construct",
  10:  "dojo/on",
  11:  "dijit/focus",
  12:  "dijit/_TemplatedMixin",
  13:  "dijit/_Widget",
  14:  "dijit/form/ValidationTextBox",
  15:  "dijit/form/Button",
  16:  "epi/shell/widget/_ValueRequiredMixin"
  17:  ],
  18:  function (array, declare, lang, json, query, domConstruct, on, focus, templatedMixin, widget, textbox, button, valueRequiredMixin) {
  19:  return declare([widget, templatedMixin, valueRequiredMixin], {
  20:      templateString: "<div data-dojo-attach-point=\"stateNode, tooltipNode\" class=\"dijit dijitReset dijitInline\"> \
  21:                          <div data-dojo-attach-point=\"keyValueItemsNode\" class=\"dijit dijitReset\"></div> \
  22:                          <div class=\"dijit dijitReset\"> \
  23:                              <button data-dojo-attach-event=\"onclick:addKeyValueItem\" type=\"button\" class=\"\">${addButtonLabel}</button> \
  24:                          </div> \
  25:                      </div>",
  26:      baseClass: "keyValueItems",
  27:      keyLabel: "Key",
  28:      valueLabel: "Value",
  29:      addButtonLabel: "Add",
  30:      removeButtonLabel: "X",
  31:      keyValidationExpression: "",
  32:      keyValidationMessage: "",
  33:      valueValidationExpression: "",
  34:      valueValidationMessage: "",
  35:      valueIsCsv: true,
  36:      valueIsInclusive: true,
  37:      value: null,
  38:      widgetsInTemplate: true,
  39:      constructor: function() {
  40:          this._keyValueItems = [];
  41:      },
  42:      postMixInProperties: function() {
  43:          this.inherited(arguments);
  44:          if (this.params.keyLabel)
  45:              this.keyLabel = this.params.keyLabel;
  46:          if (this.params.valueLabel)
  47:              this.valueLabel = this.params.valueLabel;
  48:          if (this.params.addButtonLabel)
  49:              this.addButtonLabel = this.params.addButtonLabel;
  50:          if (this.params.removeButtonLabel)
  51:              this.removeButtonLabel = this.params.removeButtonLabel;
  52:          if (this.params.keyValidationExpression)
  53:              this.keyValidationExpression = this.params.keyValidationExpression;
  54:          if (this.params.keyValidationMessage)
  55:              this.keyValidationMessage = this.params.keyValidationMessage;
  56:          if (this.params.valueValidationExpression)
  57:              this.valueValidationExpression = this.params.valueValidationExpression;
  58:          if (this.params.valueValidationMessage)
  59:              this.valueValidationMessage = this.params.valueValidationMessage;
  60:      },
  61:      destroy: function() {
  62:          var _a;
  63:          while (_a = this._keyValueItems.pop()) {
  64:              _a.div.destroyRecursive();
  65:          }
  66:          this.inherited(arguments);
  67:      },
  68:      focus: function() {
  69:          try {
  70:              if (this._keyValueItems.length > 0) {
  71:                  focus.focus(this._keyValueItems[0].div.keyValueItemsNode);
  72:              }
  73:          } catch (e) {
  74:          }
  75:      },
  76:      onChange: function() {},
  77:      onBlur: function() {},
  78:      onFocus: function() {},
  79:      isValid: function() {
  80:          var isValid = true;
  81:          array.forEach(this._keyValueItems, function(entry) {
  82:              var keyTextbox = entry.keyTextbox,
  83:                  valueTextbox = entry.valueTextbox;
  84:   
  85:              isValid = isValid && keyTextbox.isValid() && valueTextbox.isValid();
  86:          });
  87:          return isValid;
  88:      },
  89:      _calculateValue: function () {
  90:          var value = [];
  91:          array.forEach(this._keyValueItems, function(entry) {
  92:              var keyTextbox = entry.keyTextbox,
  93:                      valueTextbox = entry.valueTextbox;
  94:   
  95:              if (keyTextbox.value && valueTextbox.value && keyTextbox.isValid() && valueTextbox.isValid()) {
  96:                  var keyValuePair = new Object();
  97:                  keyValuePair.key = keyTextbox.value;
  98:                  keyValuePair.value = valueTextbox.value;
  99:                  value.push(keyValuePair);
 100:              }
 101:          });
 102:   
 103:          this._set("value", value);
 104:      },
 105:      _setValueAttr: function (value) {
 106:          this._set("value", value);
 107:   
 108:          array.forEach(value, this._addKeyValueTextboxesForItem, this);
 109:      },
 110:      _onBlur: function () {
 111:          this.inherited(arguments);
 112:          this.onBlur();
 113:      },
 114:      addKeyValueItem: function () {
 115:          this._addKeyValueTextboxesForItem({ "Key": "", "Value": "" });
 116:      },
 117:      _addKeyValueTextboxesForItem: function (keyValueItem) {
 118:          var div = domConstruct.create("div", null, this.keyValueItemsNode);
 119:          div.setAttribute("class", "keyValueItemContainer");
 120:   
 121:          var keyTextbox = this._getTextbox(keyValueItem.key, "keyTextbox", this.keyValidationMessage, this.keyValidationExpression);
 122:          var valueTextbox = this._getTextbox(keyValueItem.value, "valueTextbox", this.valueValidationMessage, this.valueValidationExpression);
 123:   
 124:          keyTextbox.placeAt(div);
 125:          valueTextbox.placeAt(div);
 126:   
 127:          var btn = new button({
 128:              label: this.removeButtonLabel,
 129:              main: this,
 130:              container: div
 131:          });
 132:          btn.on("click", function () {
 133:              this.main._removeKeyValueItem(this.container);
 134:              domConstruct.destroy(this.container);
 135:              this.main._calculateValue();
 136:              this.main.onChange(this.main.value);
 137:   
 138:          });
 139:          btn.placeAt(div);
 140:   
 141:          this._pushKeyValueItem(div, keyTextbox, valueTextbox);
 142:      },
 143:      _removeKeyValueItem: function (div) {
 144:          var newKeyValueItems = [];
 145:   
 146:          array.forEach(this._keyValueItems, function (entry) {
 147:              if (entry.div != div) {
 148:                  newKeyValueItems.push(entry);
 149:              }
 150:          });
 151:   
 152:          this._keyValueItems = newKeyValueItems;
 153:      },
 154:      _pushKeyValueItem: function(div, keyTextbox, valueTextbox) {
 155:          var o = new Object();
 156:          o.div = div;
 157:          o.keyTextbox = keyTextbox;
 158:          o.valueTextbox = valueTextbox;
 159:   
 160:          this._keyValueItems.push(o);
 161:      },
 162:      _getTextbox: function (value, cssClass, message, expression) {
 163:          var tb = new textbox({
 164:              value: value,
 165:              invalidMessage: message,
 166:              regExp: expression
 167:          });
 168:          tb.setAttribute("class", cssClass);
 169:   
 170:          tb.on("change", lang.hitch(this, function () {
 171:              this._calculateValue();
 172:              this.onChange(this.value);
 173:          }));
 174:          tb.on("focus", lang.hitch(this, function () {
 175:              this._set("focused", true);
 176:              this.onFocus();
 177:          }));
 178:          tb.on("blur", lang.hitch(this, function () {
 179:              this._set("focused", false);
 180:              this._onBlur();
 181:          }));
 182:   
 183:          return tb;
 184:      }
 185:  });
 186:  });

Ok. This is where everything is happening on the fron-end. The concept is the get the value and then render the controls needed as well as setting a new value based on the rendered controls if this makes sense. And, I see now that I forgot to use the 'Key' and 'Value' labels but it works fine without them. ;-)

Some of the important methods here are:

postMixInProperties

This is where we read the parameters we specified in 'KeyValueItemsEditorDescriptor'.

isValid

This is where we makes sure if the user input is valid or not.

_calculateValue

This is used to calculate the new value based on user input.

_setValueAttr

This is in this cased used to get the value stored in the database and render the controls.

_addKeyValueTextboxesForItem

This is the method to render the controls base on a single KeyValueItem value.

Output




Index.cshtml


   1:  @Html.DropDownListFor(m => m.KeyValueItems, new SelectList(Model.KeyValueItems, "Value", "Key"))

This is an example of how to render the property as a drop-down list.

That's it

Yes, a lot of code and examples but I hope you get the idea. :-)

Source code.

Thursday, October 10, 2013

EPiServer 7: Set default values on properties with attribute

I have been looking into setting default values on a property in EPiServer 7 MVC to be set if the value is null, but so far I have only stumbled upon setting a default value on a property when creating a new page.

I want to be able to specify the default value as an attribute to the property and found what I thought would work and that is System.ComponentModel.DefaultValueAttribute.


   1:  [DefaultValue("Y")]
   2:  public virtual string DisplayTeasers { get; set; }

It looked perfect, but didn't work. :-(

So, I tweaked it a little to make it work the way I wanted it to. Perhaps you need to be familiar with the EPiServer 7 MVC templates in order to achieve this but the idea is to "transform" to CurrentPage object and set the default values if the current property value is null, and we do this when creating the PageViewModel.

The attribute contains constructors for setting the default values and some static methods to transform an entire CurrentPage object based on any default value attributes specified.


   1:  public class ViewModelDefaultValueAttribute : DefaultValueAttribute
   2:  {
   3:      public ViewModelDefaultValueAttribute(string value) : base(value)
   4:      { }
   5:   
   6:      public ViewModelDefaultValueAttribute(int value) : base(value)
   7:      { }
   8:   
   9:      public ViewModelDefaultValueAttribute(bool value) : base(value)
  10:      { }
  11:   
  12:   
  13:      public static T SetDefaultValues<T>(T currentPage) where T : SitePageData
  14:      {
  15:          if (currentPage == null)
  16:              return currentPage;
  17:   
  18:          Type currentPageType = GetType<T>(currentPage);
  19:          if (currentPageType == null)
  20:              return currentPage;
  21:   
  22:          T clone = currentPage.CreateWritableClone() as T;
  23:   
  24:          Type defaultValueType = typeof(ViewModelDefaultValueAttribute);
  25:   
  26:          foreach (PropertyInfo property in currentPageType.GetProperties())
  27:          {
  28:              if (clone[property.Name] != null)
  29:                  continue;
  30:   
  31:              object[] attributes = property.GetCustomAttributes(defaultValueType, false);
  32:              if (attributes == null || attributes.Length == 0)
  33:                  continue;
  34:   
  35:              ViewModelDefaultValueAttribute defaultValue = attributes.Cast<ViewModelDefaultValueAttribute>().Single();
  36:              if (defaultValue == null)
  37:                  continue;
  38:   
  39:              clone.Property[property.Name].Value = defaultValue.Value;
  40:          }
  41:   
  42:          if (clone.IsModified)
  43:          {
  44:              clone.MakeReadOnly();
  45:              return clone;
  46:          }
  47:   
  48:          return currentPage;
  49:      }
  50:   
  51:      private static Type GetType<T>(T currentPage) where T : SitePageData
  52:      {
  53:          Type type = currentPage.GetType();
  54:   
  55:          while (type != null && !type.FullName.Contains("EPi7.Models.Pages"))
  56:          {
  57:              type = type.BaseType;
  58:          }
  59:   
  60:          return type;
  61:      }
  62:  }

As you can see we inherit the DefaultValueAttribute in order to utilize existing functionality.

The static method SetDefaultValues is used to "transform" the CurrentPage object and it loops through all the properties on the page type and then finds any ViewModelDefaultValueAttributes specified and if the property value is null sets the default value specified.

SitePageData is the base class for all pages types.


   1:  /// <summary>
   2:  /// Base class for all page types
   3:  /// </summary>
   4:  public abstract class SitePageData : PageData

You specify the default value like this on a property.


   1:  [ViewModelDefaultValue("Y")]
   2:  public virtual string DisplayTeasers { get; set; }

We also need to use the SetDefaultValues method somewhere and this is done in the constructor when creating the PageViewModel.


   1:  public class PageViewModel<T> : IPageViewModel<T> where T : SitePageData
   2:  {
   3:      public PageViewModel(T currentPage)
   4:      {
   5:          CurrentPage = ViewModelDefaultValueAttribute.SetDefaultValues(currentPage);
   6:      }
   7:  }

When putting all of this together you can specify a default value as a attribute on a property and before the CurrentPage object is sent to the view (via view model) it is tranformed to set default values if the property is null.

As you can see in the image above the default value "Y" is displayed (as an example) on the page but when editing the property it is blank (null).

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.