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