Showing posts with label Attribute. Show all posts
Showing posts with label Attribute. Show all posts

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