Creating a more advanced property editor
Note: There is an updated version of this blog post for EPiServer 7.5 here.
In this fourth and last blog post in my series on how to extend the user interface in EPiServer 7 we will take a look how we can build a more advanced editorial widget. We will use two of the built in widgets in Dijit that creates a select-like ui component that will present possible alternatives to the editor as they type which will give us the following editor widget:
There are two widgets in Dijit that are very similar to each other:
- FilteringSelect forces the user to use one of the suggested values.
- ComboBox lets the user type what she want’s and merely gives suggestions. Perfect for tags for instance.
It’s possible to bind the widgets to either a list of values or to connect it to a store that will search for alternatives as the editor types. We’ll go for the later in this sample.
Implementing the server parts
First, we add a property to our page type and mark it with an UIHint attribute that points to a custom editor identifier.
[ContentType(GUID = "F8D47655-7B50-4319-8646-3369BA9AF05E")]
public class MyPage : SitePageData
{
[UIHint("author")]
public virtual string ResponsibleAuthor { get; set; }
}
Then we add the editor descriptor that is responsible assigning the widget responsible for editing. Since we add the EditorDescriptiorRegistration attribute this means that all strings that are marked with an UIHint of “author” will use this configuration.
using EPiServer.Shell.ObjectEditing.EditorDescriptors;
namespace EPiServer.Templates.Alloy.Business.EditorDescriptors
{
[EditorDescriptorRegistration(TargetType = typeof(string), UIHint = "author")]
public class EditorSelectionEditorDescriptor : EditorDescriptor
{
public EditorSelectionEditorDescriptor()
{
ClientEditingClass = "alloy/editors/AuthorSelection";
}
}
}
Before we head over to the client parts let’s add the store that is responsible for giving the results to the client:
using System;
using System.Collections.Generic;
using System.Linq;
using EPiServer.Shell.Services.Rest;
namespace EPiServer.Templates.Alloy.Rest
{
[RestStore("author")]
public class AuthorStore : RestControllerBase
{
private List<string> _editors = new List<string>{
"Adrian", "Ann", "Anna", "Anne", "Linus", "Per",
"Joel", "Shahram", "Ted", "Patrick", "Erica", "Konstantin", "Abraham", "Tiger"
};
public RestResult Get(string name)
{
IEnumerable<string> matches;
if (String.IsNullOrEmpty(name) || String.Equals(name, "*", StringComparison.OrdinalIgnoreCase))
{
matches = _editors;
}
else
{
//Remove * in the end of name
name = name.Substring(0, name.Length - 1);
matches = _editors.Where(e => e.StartsWith(name, StringComparison.OrdinalIgnoreCase));
}
return Rest(matches
.OrderBy(m => m)
.Take(10)
.Select(m => new {Name = m, Id = m}));
}
}
}
We inherit from the class EPiServer.Shell.Services.Rest.RestControllerBase. Now we can implement the REST-methods that we want. In this case we only implement GET which is used for fetching data but we can also implement POST, PUT and DELETE. EPiServer has also added SORT and MOVE to the list of accepted methods (these are not part of the REST-specification but rather the WebDav specification). To register a service end point for our store we add the attribute RestStore(“author”). We will add some client side logic to resolve the URL for this store.
Implementing the client
Setting up the client side store that works against the server side REST store feels logical to do in a module initializer so let’s make sure that we have a client module initializer registered in our module.config file:
<?xml version="1.0" encoding="utf-8"?>
<module>
<assemblies>
<!-- This adds the Alloy template assembly to the "default module" -->
<add assembly="EPiServer.Templates.Alloy" />
</assemblies>
<dojoModules>
<!-- Add a mapping from alloy to ~/ClientResources/Scripts to the dojo loader configuration -->
<add name="alloy" path="Scripts" />
</dojoModules>
<clientModule initializer="alloy.ModuleInitializer"></clientModule>
</module>
And we add a file named “ModuleInitializer” in the ClientResources/Scripts folder:
define([
// Dojo
"dojo",
"dojo/_base/declare",
//CMS
"epi/_Module",
"epi/dependency",
"epi/routes"
], function (
// Dojo
dojo,
declare,
//CMS
_Module,
dependency,
routes
) {
return declare("alloy.ModuleInitializer", [_Module], {
// summary: Module initializer for the default module.
initialize: function () {
this.inherited(arguments);
var registry = this.resolveDependency("epi.storeregistry");
//Register the store
registry.create("alloy.customquery", this._getRestPath("author"));
},
_getRestPath: function (name) {
return routes.getRestPath({ moduleArea: "app", storeName: name });
}
});
});
In the initializer we call resolveDependency to get the store registry from the client side IOC-container. The store registry has a method to create and register a store that takes an identifier for the store as a string as well as an URL to the store. In our case we resolve the URL to the store with the name of the shell module and the registered name of the store “author”.
Note: In this case we are using the “built in” shell module in the site root which is simply named “App”.
So, lets go ahead and create the actual editor widget:
define([
"dojo/_base/connect",
"dojo/_base/declare",
"dijit/_CssStateMixin",
"dijit/_Widget",
"dijit/_TemplatedMixin",
"dijit/_WidgetsInTemplateMixin",
"dijit/form/FilteringSelect",
"epi/dependency",
"epi/epi",
"epi/shell/widget/_ValueRequiredMixin",
//We are calling the require module class to ensure that the App module has been set up
"epi/RequireModule!App"
],
function (
connect,
declare,
_CssStateMixin,
_Widget,
_TemplatedMixin,
_WidgetsInTemplateMixin,
FilteringSelect,
dependency,
epi,
_ValueRequiredMixin,
appModule
) {
return declare("alloy.editors.AuthorSelection", [_Widget, _TemplatedMixin, _WidgetsInTemplateMixin, _CssStateMixin, _ValueRequiredMixin], {
templateString: "<div class=\"dijitInline\">\
<div data-dojo-attach-point=\"stateNode, tooltipNode\">\
<div data-dojo-attach-point=\"inputWidget\" data-dojo-type=\"dijit.form.FilteringSelect\" style=\"width: 300px\"></div>\
</div>\
</div>",
intermediateChanges: false,
value: null,
store: null,
onChange: function (value) {
// Event that tells EPiServer when the widget's value has changed.
},
postCreate: function () {
// call base implementation
this.inherited(arguments);
// Init textarea and bind event
this.inputWidget.set("intermediateChanges", this.intermediateChanges);
var registry = dependency.resolve("epi.storeregistry");
this.store = this.store || registry.get("alloy.customquery");
this.inputWidget.set("store", this.store);
this.connect(this.inputWidget, "onChange", this._onInputWidgetChanged);
},
isValid: function () {
// summary:
// Check if widget's value is valid.
// tags:
// protected, override
return this.inputWidget.isValid();
},
// Setter for value property
_setValueAttr: function (value) {
this.inputWidget.set("value", value);
this._set("value", value);
},
_setReadOnlyAttr: function (value) {
this._set("readOnly", value);
this.inputWidget.set("readOnly", value);
},
// Event handler for the changed event of the input widget
_onInputWidgetChanged: function (value) {
this._updateValue(value);
},
_updateValue: function (value) {
if (this._started && epi.areEqual(this.value, value)) {
return;
}
this._set("value", value);
this.onChange(value);
}
});
});
In short terms, what the widget does it to set up an dijit.form.FilteringSelect as an inner widget, feed this with a store from the store registry and listen to it’s change event. If we take a closer look to some of the parts of the widget we can see the code needed to connect the store defined in our initialization module to the inner widget:
var registry = dependency.resolve("epi.storeregistry");
this.store = this.store || registry.get("alloy.customquery");
this.inputWidget.set("store", this.store);
Requiring a module
When EPiServer starts a view not all modules and components are loaded. When the view is loaded, before we add a component (or gadget) we make sure that we have started the shell module that it is located in. In EPiServer 7.1 this can be done by calling the "epi/RequireModule" class with your module name, in this example:
"epi/RequireModule!App"
And we are done! When editing a page of the given type we can see how the editor changes suggestions as we type and entering an invalid value gives us an validation error:
Doing a simple search and replace in Author.js from FilteringSelect to ComboBox enables the editor to enter any value she likes:
Summary
This ends the series of how to extend the user interface of EPiServer 7. We have looked how to create components for the UI that can either be plugged in automatically or added by the user. We have created a component using either web forms or dojo/dijit. Using jQuery-style gadgets ala EPiServer 6 still works pretty much the same way with the difference that these also works in the EPiServer 7 CMS edit view.
We have also looked how you can add attributes to your models to control both presentation and editing. We have looked into creating an editor using Dojo/Dijit. There are a few examples of the in the new Alloy template package and there is more information about how to extend the user interface in the User Interface section of the EPiServer Framework SDK: http://sdkbeta.episerver.com/Developers-guide/Framework/?id=3511&epslanguage=en.
Extending the User Interface of EPiServer 7
Plugging in a Dojo based component
Creating a content search component
Creating a more advanced property editor
Hi Linus,
This is a good article, I created a property editor, using the above code. I am getting a Javascript Error as below pointing to the requiremodule.js
SyntaxError: function statement requires a name
load: function (/*String*/id, /*function*/require, /*function*/load) {
Am I missing something, or what could be the problem?
Best regards
Kiran
@Kiran: Make sure that you have added a module name when you are requiring the require module in your widget header:
"alloy/requiremodule!App" ("App" is the module required in this case. If you are using the Alloy templates as a base is should probably be "Alloy" instead. Check your module.config file in the site root for the correct module name)
Hi Linus, Thanks for the quick response.
in the module.config
I am sorry I did not understand properly.
I have done the following steps
1. Created new Rest store class:: [RestStore("ClearCache")]
2. Created property and descriptor called "ClearDisciplineCache". My client class in this case is "diamondleague.editors.ClearDisciplineCache"
3. Created a script called "ModuleInitializer.js", copied your code and renamed the widget name to "diamondleague.ModuleInitializer" Also "alloy.customquery" to "diamondleague.customquery"
4. Added the line
5. Created the actual widget, copied the code and changed "alloy/requiremodule!App" to "diamondleague/requiremodule!App", "alloy.editors.AuthorSelection" to "diamondleague.editors.ClearDisciplineCache" and "alloy.customquery" to "diamondleague.customquery"
6 Then added requiremodule.js and copied your code to this file.
After all these steps, when I go to the cms edit mode, I get the javascript error as mentioned above.
I am not sure what I am now missing.
Thanks you for the support
Kiran
Tip: Often the client resources and config files get cached, was double-checking everything and it still didn't work. Had to do an iisreset to force all the files to be loaded again (especially true if you change JavaScript files, make sure they're not cached in the browser).
Hi Linus
I have followed this example and got the property to work with the RestStore. But one thing I cant get my mind over is why the value i selected in the property isn't saved when i publish the page. I can see that the value is being stored in the database but the property is not being pre-populated with that value when I revisit the the page in edit mode.
Is it the FilteringSelect that cleans the value when it loads? Or do I have to get the value myself and populate the property manually?
Thanks in advance!
/Fred
I don't know if it's 7.1 related (new dojo version?), but ended up getting very strange values when I followed this example step by step. Instead of getting the names I got some weird numeric values. When I instead played arround with Memory-stores, I noticed that the property to use for value is "id" not "value". So I changed that in my REST store and I then got the values I wanted. That could be a tip for anyone that's stuck.
@Tomas Eld: I love you! Solved my problem.
This really doesn't work. I've followed this step-by-step, but as soon as I include the EditorDescriptor-class in my project, I cannot navigate to "Forms editing" anymore. No error-message and no exception i firebug.
I have tried this too. Complete waste of time. This does not work in EpiServer 7.1. Symptoms are just same as Calle have. I'm trying to migrate old and exotic custom property with autocomplete from ancient 4.61 Epi in latest Epi. This is the most hardest nut to crack.
After quite a few troublesome hours I finally got it to work! What I did was that I placed RequireModule.js in ClientResource/Scripts folder
and in my editor widget I changed dijit.form.FilteringSelect to dijit/form/FilteringSelect.
In hindsight I think that moving RequireModule.js to ClientResource/Scripts folder would have been enough cause when I change my editor back to dijit.form.FilteringSelect, it still works. (EPiServer 7.1 and win 7 64 bit)
I have updated the code to use the built in RequireModule class in EPiServer 7.1 (epi/RequireModule). In EPiServer 7.5 it will be possible to do this with configuration in the module.config file. I also updated the class name syntax to use the "/" pattern to separate namespaces and classes.
I’m still struggling with this one. Sometimes the forms editing mode hangs, like described by @Calle Bjernekull on Friday(!) 13th of September 2013. Emptying all kinds of cache I can get forms editing to work again, but then; every time I do a "hard" refresh I get the same error as described by @Fred Lundén. The property is not being pre-populated with that value when I revisit the the page in edit mode. In view mode the value seems to be fetched just fine.
Everybody else happy with auto suggest properties in EPiServe CMS 7? I’m hours into trying to fix this and I thought it would be a simple one, having this blog post in mind.
Some hard facts from my testing/development environment:
Testing mainly in Crome, and some in IE.
Episerver 7.1 site running on IIS Express using EPiServer assemblies with version numbers ending with .24.
We are running the site off these NuGet packages: "EPiServer.CMS.Core.7.0.586.24" and "EPiServer.Framework.7.0.859.24".
In ~/modulesbin we have assemblies matching version number 2.0.86.0 (Packaging, Packaging.UI, Shell.UI) and 2.0.79.0 (EPiServer.Cms.Shell.UI.dll)
I have updated an updated version of this editor targetted for EPiServer 7.5 that can be found here: http://world.episerver.com/Blogs/Linus-Ekstrom/Dates/2013/12/Auto-suggest-editor-in-EPiServer-75/
Hi Linus, I have two dojo widgets in my project, but only 1 module.config file. Within that module.config file I have:
But only the first initializer method is ever executed...is there a way of supporting multiple widgets in the same module.config file? Or should I have multiple files?
@Higgsy: As far as I can see from the code, only one clientModule element is allowed (or at least supported) in a module.config file. You can have as many widgets as you'd like, but if you have the need for several modules these should be separated into separate structures.
@Linus - thanks, this is now working.
This code still does not work for me. It just stucks loading edit view and nothing happens. Firebug gives error message:
"A script on this page may be busy, or it may have stopped responding. You can stop the script now, or you can continue to see if the script will complete.
Script: http://www.developmetproject.dev/maintenance/Shell/2.1.90/ClientResources/dtk/dojo/dojo.js:15"
My module.config is identical as in example code. I just changed my project name as assembly name.
EditorDescriptors class is in same file as my pagetype. This file uses namespace, where the RestStore class is.
ModuleInitializer is saved as ClientResources/Scripts/moduleinitializer.js and widget is in same folder (app.js). All the files are identical as in the example. Just namespaces are modified.
I have EPiserver 7.1 and Episerver.dll version is 7.0.586.16
Is there any kind of way to use jQuery autocomplete in here? Massive amounts of dojo files(=slow), no comprehensible error messages. This Dojo tuning feels just frustrating.
@Matti: Have you seen the updated the simplified blog post (http://world.episerver.com/Blogs/Linus-Ekstrom/Dates/2013/12/Auto-suggest-editor-in-EPiServer-75/)?
Regarding jQuery you should be able to use that. There is even an older jQuery version (1.3 I think) loaded in the user interface that you potentially can use depending on what you want to use in jQuery. Otherwise you can always load a separate version of jQuery using the noconflict flag.
Thank you for you prompt answer. I haven't tried that simplified solution yet. Better solution for our project would be using jQuery, because autocomplete using several words and webmethods are already done to keyword search. But how can I transfer this funcitonality to custom page property editing? How can I add jQuery library reference to edit side head tag? I must also add some jQuery script in body tag. Should I override CreateEditControls method to add script or is there better alternative?
Got problem with javascript errors, read more here:
http://world.episerver.com/Modules/Forum/Pages/Thread.aspx?id=86847
Any clue why?