World is now on Opti ID! Learn more

Linus Ekström
Nov 12, 2012
  14797
(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
Make Global Assets Site- and Language-Aware at Indexing Time

I had a support case the other day with a question around search on global assets on a multisite. This is the result of that investigation. This co...

dada | Jun 26, 2025

The remote server returned an error: (400) Bad Request – when configuring Azure Storage for an older Optimizely CMS site

How to fix a strange issue that occurred when I moved editor-uploaded files for some old Optimizely CMS 11 solutions to Azure Storage.

Tomas Hensrud Gulla | Jun 26, 2025 |

Enable Opal AI for your Optimizely products

Learn how to enable Opal AI, and meet your infinite workforce.

Tomas Hensrud Gulla | Jun 25, 2025 |

Deploying to Optimizely Frontend Hosting: A Practical Guide

Optimizely Frontend Hosting is a cloud-based solution for deploying headless frontend applications - currently supporting only Next.js projects. It...

Szymon Uryga | Jun 25, 2025

World on Opti ID

We're excited to announce that world.optimizely.com is now integrated with Opti ID! What does this mean for you? New Users:  You can now log in wit...

Patrick Lam | Jun 22, 2025

Avoid Scandinavian Letters in File Names in Optimizely CMS

Discover how Scandinavian letters in file names can break media in Optimizely CMS—and learn a simple code fix to automatically sanitize uploads for...

Henning Sjørbotten | Jun 19, 2025 |