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.
Would you be able to share some of the code for the keyvalueitem list, especially the Dojo part? There isn't that many code samples out there, so that would be great!
ReplyDeleteI uploaded the source code for this little example and hope it is something you find useful! :-)
DeleteHi Peter, could you explain, what is the "KeyValueItemsParameters" type, I couldn't find description for this type in your example? Thanks
ReplyDeleteWell, it's not really a type but simply a means to get parameters from backend to frontend. In this example we pass on values specified on the KeyValueItemsAttribute, such as button texts and validation expressions, and these are passed on up to the dojo editor as a Json object via the EditorDescriptor.
DeleteHi Peter, thanks for this great example, it really helps me to understand custom properties and custom editors. I have a similar question to valik. When I try to implement this, I get the error "The type or namespace name 'KeyValueItemsParameters' could not be found (are you missing a using directive or an assembly reference?)" in reference to KeyValueItemsAttribute.cs (line 55). Your code uses EPi7.Business.Classes, and I assume that you have a class 'KeyValueItemsParameters' in there? If I was a little smarter I could probably figure out what should go in there, but so far I haven't been able to get it working. Could you tell me what that class needs to look like?
DeleteHi Rudi,
DeleteI will look into this and update the source accordingly. It is a simple class to hold the parameters sent to the front layer via json serialization, if I remember correctly. I have struggled with understanding dojo and find it rather difficult but will try to post a better example of what I have learned so far. It's just a question of finding the time. :-)
Hi Peter, Thanks for sharing your code, its a very neat way of doing custom property. I am trying to implement it with Episerver 7.5 (dojo version 1.8). The editor loads first time when there are no items are present, but once added one or more, it stops working i.e. the page doesnt allow you to go into the property editor mode. It seems that the _setValueAttr method gets called befor the div tag with id '_content' exists.
ReplyDeleteHi Punjabisoul, I was seeing the same issue you were. Were you able to come up with a solution?
DeleteI noticed this too when trying to implement it as a test in my EPiServer 7.5 project. There must be some changes made between 7 and 7.5 concerning this. I will try to figure out what the issue is and update the information. Or if I ever find the time post a better example if creating a custom property. :-)
DeleteHi Peter! Thanks for replying and updating the code to work with 7.5, works very well :)
DeleteGreat post and resource! Thank you for sharing it!
ReplyDeleteJust one small thing: seems like SaveData function in PropertyKeyValueItems class, is essential to make it work (at least in EPi7.1). Otherwise I end up with a EPi/dojo error: Unable to cast object of type 'System.Collections.Generic.List`1[KeyValueItem]' to type 'System.String'.
I guess I'm the only one that tries it out without downloading the source first ;)
I am getting this issue when I copy pasted the code in version 7.0/7.1. This is the error I receive on clicking on edit window
ReplyDeleteCan you please mention, what specific changes I need to do to work with version 7.0 and 7.1
TypeError: ctor is not a constructor
_229()widgets.js (line 2)
.cache["dojo/_base/lang"]/</_388/<()dojo.js (line 15)
_c6()dojo.js (line 15)
_36()dojo.js (line 15)
_16()dojo.js (line 15)
req()dojo.js (line 15)
.cache["epi/shell/widget/WidgetFactory"]/</<.defaultWidgetInstantiator()widgets.js (line 2)
.cache["dojo/_base/lang"]/</_388/<()dojo.js (line 15)
.cache["epi/shell/widget/WidgetFactory"]/</<._createInternal()widgets.js (line 2)
.cache["epi/shell/widget/WidgetFactory"]/</<._createWidgets/_215<()widgets.js (line 2)
.cache["dojo/_base/lang"]/</_388/<()dojo.js (line 15)
map()dojo.js (line 15)
.cache["epi/shell/widget/WidgetFactory"]/</<._createWidgets()widgets.js (line 2)
.cache["epi/shell/widget/WidgetFactory"]/</<._createInternal/<()widgets.js (line 2)
.cache["dojo/_base/lang"]/</_388/<()dojo.js (line 15)
_174()dojo.js (line 15)
.cache["dojo/_base/Deferred"]/</dojo.Deferred/this.then()dojo.js (line 15)
.cache["dojo/_base/Deferred"]/</dojo.when()dojo.js (line 15)
.cache["epi/shell/widget/WidgetFactory"]/</<._createInternal()widgets.js (line 2)
.cache["epi/shell/widget/WidgetFactory"]/</<._createWidgets/_215<()widgets.js (line 2)
.cache["dojo/_base/lang"]/</_388/<()dojo.js (line 15)
map()dojo.js (line 15)
.cache["epi/shell/widget/WidgetFactory"]/</<._createWidgets()widgets.js (line 2)
.cache["epi/shell/widget/WidgetFactory"]/</<.createWidgets/</<()widgets.js (line 2)
.cache["dojo/_base/lang"]/</_388/<()dojo.js (line 15)
_c6()dojo.js (line 15)
_36()dojo.js (line 15)
_7a()dojo.js (line 15)
_32/_ee()dojo.js (line 15)
req.injectUrl/_109()dojo.js (line 15)
...s);if(this.anchor){if(!this.getItem(this.anchor.id)){this.anchor=null;}}var t=[]...
Thanks Peter! This was exactly what I was looking for. You really describe how to combine both backend and frontend to build a complex property. Dojo/Dijit is still a bit scary for most developers and the documentation is non-existent.
ReplyDeleteI wonder how the search works though. I guess it shouldn't be a problem since the property inherits from PropertyLongString but have you tested it?
Thanks again for a great post!!
Hello Peter,
ReplyDeleteI am running EpiServer 7.18 and on creating a new keyValue it does not pick up the change (for publishing) unless I click another keyvalue textfield.
No errors are presented in the logs.
Could you help me in pointing me into the right direction as to why that is the case
Im getting the same issue. Is there a way to trigger the CMS to show the Publish button?
DeleteI got the same issue, changes to the first KeyValue-pair I added displays the Publish button. If I add new pairs I have to edit my first pair to make the Publish button appear. Have anyone got this to work yet?
DeleteThe code just works fine for me to enter the values for the key-value pair. But after save or publish the values are not maintained and it starts from the first, asking to "Add" new fields. It would be great if I could figure out this :(
ReplyDeleteSame us above. Althought I get an exception in Episerver edit mode: "Unable to cast object of type 'System.Collections.Generic.List`1[EpiTutorial.Models.Properties.KeyValueItem]' to type 'System.String'."
ReplyDeleteDoes anyone have a solution to this?
Thanks for sharing with us.Great post
ReplyDeleteDot Net Training in Chennai
Thank you for an additional great post. Exactly where else could anybody get that kind of facts in this kind of a ideal way of writing? I have a presentation next week, and I’m around the appear for this kind of data.
ReplyDeleteclick here
Selenium Training in Bangalore|
Selenium Training in Chennai
I enjoy what you guys are usually up too. This sort of clever work and coverage! Keep up the wonderful works guysl.Good going.
ReplyDeleteoneplus service center chennai
oneplus service center in chennai
oneplus service centre chennai
Thanks for sharing the knowledgeable stuff to enlighten us no words for this amazing blog.. learnt so many things I recommend everyone to learn something from this blogger and blog.. I am sharing it with others also
ReplyDeleteIT Software Training in Chennai | Python Training in Chennai | Dot Net Training in Chennai
Thank you so much for posting this hub, Nice work on the Dot net Training..
ReplyDeleteRegards,
https://www.softlogicsys.in/datascience-training-in-chennai/
https://www.softlogicsys.in/machine-learning-training-in-chennai/
https://www.softlogicsys.in/linux-training-in-chennai/
https://www.softlogicsys.in/dba-administration-training-in-chennai/
Thanks for sharing such an informative blog.Awaiting for your next update.
ReplyDeleteSelenium Training in chennai | Selenium Training in annanagar | Selenium Training in omr | Selenium Training in porur | Selenium Training in tambaram | Selenium Training in velachery
Damien Grant
ReplyDeleteDamien Grant
Damien Grant
Damien Grant
Damien Grant
Damien Grant
Damien Grant
Damien Grant
Damien Grant
ReplyDeleteDamien Grant
Damien Grant
Damien Grant
Damien Grant
Damien Grant
Damien Grant
Damien Grant
Book Tenride call taxi in Chennai at most affordable taxi fare for Local or Outstation rides. Get multiple car options with our Chennai cab service
ReplyDeletechennai to kochi cab
bangalore to kochi cab
kochi to bangalore cab
chennai to hyderabad cab
hyderabad to chennai cab
This comment has been removed by the author.
ReplyDeleteThanks for sharing such an informative blog.Awaiting for your next update.salesforce training in chennai
ReplyDeletesoftware testing training in chennai
robotic process automation rpa training in chennai
blockchain training in chennai
devops training in chennai
online chemistry tuition
ReplyDeletegreat blog thanks for information
ReplyDeleteRudraksha Beads
Deepam oil
how do i know if my husband is infertile
ReplyDeletemale infertility testing
abnormal sperm
ReplyDeleteGamsat organic chemistry
CBSE organic chemistry
IIT organic chemistry
Organic Chemistry Notes
smm panel
ReplyDeleteSmm panel
İş İlanları Blog
İNSTAGRAM TAKİPÇİ SATIN AL
hirdavatciburada.com
beyazesyateknikservisi.com.tr
servis
jeton hile
It is a very simple and straight-forward article. There is no beating around the bush, the writer tells you exactly what you need to know.bus rental Dubai airport transfer
ReplyDelete남양주콜걸마사지
ReplyDelete포천콜걸마사지
수원콜걸마사지
성남콜걸마사지
안산콜걸마사지
용인콜걸마사지
가평콜걸마사지
ReplyDelete김천콜걸
화순콜걸
김천콜걸
김해콜걸
장흥콜걸
밀양콜걸
안동콜걸
안동콜걸
울산콜걸
ReplyDelete서울콜걸
부산콜걸
대구콜걸
인천콜걸
세종콜걸
광주콜걸
대전콜걸
You'll instantly receive the license key on the next webpage. Copy it down. The process has bit changed, the activation code will now Kaspersky Crack
ReplyDeleteNow its working well, thank you
ReplyDeleteBus Rental Ajman