A critical vulnerability was discovered in React Server Components (Next.js). Our systems remain protected but we advise to update packages to newest version. Learn More

Linus Ekström
Nov 12, 2012
  14842
(0 votes)

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);
The reason why we define the store property using the pattern this.store = this.store || ... is that this makes it possible to send in a fake store to the widget when writing unit tests.

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

Nov 12, 2012

Comments

Please login to comment.
Latest blogs
A day in the life of an Optimizely OMVP: Learning Optimizely Just Got Easier: Introducing the Optimizely Learning Centre

On the back of my last post about the Opti Graph Learning Centre, I am now happy to announce a revamped interactive learning platform that makes...

Graham Carr | Jan 31, 2026

Scheduled job for deleting content types and all related content

In my previous blog post which was about getting an overview of your sites content https://world.optimizely.com/blogs/Per-Nergard/Dates/2026/1/sche...

Per Nergård (MVP) | Jan 30, 2026

Working With Applications in Optimizely CMS 13

💡 Note:  The following content has been written based on Optimizely CMS 13 Preview 2 and may not accurately reflect the final release version. As...

Mark Stott | Jan 30, 2026

Experimentation at Speed Using Optimizely Opal and Web Experimentation

If you are working in experimentation, you will know that speed matters. The quicker you can go from idea to implementation, the faster you can...

Minesh Shah (Netcel) | Jan 30, 2026