November Happy Hour will be moved to Thursday December 5th.

Linus Ekström
Dec 10, 2013
  12864
(4 votes)

Auto suggest editor in EPiServer 7.5

Update: This functionality is now built in. Read more about it here.

This is an updated version of the editor that was done in a blog post around the release of EPiServer 7 about a year ago that has been simplified quite a lot (the original blog post can be found here). The goal of this blog post is to add an editor that can be used to select values for a property that is feed from a store using Web API 2.0. The result looks something like this:

AuthorSelection

As with the previos blog post the usage is to add a UI hint to a property:

public class MyPage : SitePageData
{
    [UIHint("author")]        
    public virtual string ResponsibleAuthor { get; set; }
}    

Then we add an editor descriptor that is responsible for defining the widget used for editing as well as sending the store URL to the widget:

using System;
using System.Collections.Generic;
using System.Web;
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/AutoSuggestEditor";
        }
 
        public override void ModifyMetadata(Shell.ObjectEditing.ExtendedMetadata metadata, IEnumerable<Attribute> attributes)
        {
            base.ModifyMetadata(metadata, attributes);
 
            metadata.EditorConfiguration["storeurl"] = VirtualPathUtility.ToAbsolute("~/api/authors/");
        }
    }
}

Creating the service

I base the server side logic that is responsible to feed the widget suggestions on Web API 2.0 and attribute routing. I followed the following guide to set up the attribute routing (http://www.asp.net/web-api/overview/web-api-routing-and-actions/attribute-routing-in-web-api-2). Please note that if you are running an existing project you need to add a call the config.MapHttpAttributeRoutes() method in the application start up as suggested in the guide to get attribute mappings to work.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;
 
namespace EPiServer.Templates.Alloy.UIExtensions.Rest
{
    [RoutePrefix("api/authors")]
    public class AuthorWebStoreController : ApiController
    {
        private List<string> _editors = new List<string>{
            "Adrian", "Ann", "Anna", "Anne", "Linus", "Per",
            "Joel", "Shahram", "Ted", "Patrick", "Erica", "Konstantin", "Abraham", "Tiger"        
        };
 
        [Route]
        public IEnumerable<AuthorSearchResult> GetAuthors(string name)
        {
            //Remove * in the end of name
            if (name.EndsWith("*"))
            {
                name = name.Substring(0, name.Length - 1);
            }
 
            return QueryNames(name);
        }
 
        [Route("{name}")]
        public AuthorSearchResult GetAuthorByName(string name)
        {
            string author = _editors.FirstOrDefault(e => e.StartsWith(name, StringComparison.OrdinalIgnoreCase));
 
            return String.IsNullOrEmpty(author) ? null : new AuthorSearchResult { name = author, id = author };
        }
 
        private IEnumerable<AuthorSearchResult> QueryNames(string name)
        {
            IEnumerable<string> matches;
            if (String.IsNullOrEmpty(name) || String.Equals(name, "*", StringComparison.OrdinalIgnoreCase))
            {
                matches = _editors;
            }
            else
            {
 
                matches = _editors.Where(e => e.StartsWith(name, StringComparison.OrdinalIgnoreCase));
            }
            return matches
                .OrderBy(m => m)
                .Take(10)
                .Select(m => new AuthorSearchResult { name = m, id = m });
        }
    }
 
    public class AuthorSearchResult
    {
        public string name { get; set; }
        public string id { get; set; }
    }
}

Note(1) that the route prefix matches the URL sent to the editing widget.

Note(2) that the casing for the properties of the result class is using lower case. This is since the default json contract resolver in .NET web API uses a standard contract resolver instead of the camel case contract resolver that was used in the EPiServer Rest stores. This can of course be changed for the entire application but that’s not part of this blog post.

The editing widget

The editing widget has been greatly simplified, using inherritance instead of composition which in this case reduces complixity. (given that you have set up a script module, you can add the content below in a file placed under “ClientResources/Scripts/Editors/AutoSuggestEditor.js”)

define([
    "dojo/_base/declare",
    "dojo/store/JsonRest",
 
    "dijit/form/FilteringSelect"
],
function (
    declare,
    JsonRest,
 
    FilteringSelect
) {
 
    return declare([FilteringSelect], {
 
        postMixInProperties: function () {
 
            var store = new JsonRest(dojo.mixin({
                target: this.storeurl
            }));
 
            this.set("store", store);
 
            // call base implementation
            this.inherited(arguments);
        }
    });
});

Note that since the store is feed to the widget, the same widget can be used against different service APIs.

Required field and validation bug

When testing this i noticed that there seems to be a bug in the filteringselect widget when the widget has focus and is cleared. If the value is required then the validation will not be shown to the user if the search result is open. The second time the field is left (with an empty valur) the validation appears.

Dec 10, 2013

Comments

Dec 10, 2013 09:14 PM

Nice! Finally starts to feel like you wont need a rocket scientist team to customize properties. ;)

Joshua Folkerts
Joshua Folkerts Dec 11, 2013 06:08 AM

Very nice Linus. Keep up the good posts. Very true Fredrik! ;)

Mari Jørgensen
Mari Jørgensen Dec 11, 2013 07:42 AM

Great post, Linus. Now creating custom properties using dojo feels a little less like 'the dark side' :)

Dec 11, 2013 07:50 AM

Actually I hope that we can add two widgets that do this into the core and add attribute support to them on the server to remove the need to do anything on the client at all. Let's hope we can get that in in a not to far future.

Al Higgs
Al Higgs Dec 16, 2013 05:35 PM

Hi Linus - great post. I realise this article is targetted to 7.5 - but are there are any version dependencies? i.e. would this work in 7.1? Thanks. Al

Dec 17, 2013 09:16 AM

I think that the code should work in 7.1 as well, though I have not tested it myself.

Matti Mertojoki
Matti Mertojoki Dec 19, 2013 02:23 PM

About EPi 7.1.. In example there is used attribute routing which needs Visual Studio 2013 or NuGet packages. Both of them will need .Net 4.5 framework.

EPi 7.1 uses .Net framework 4.0 in basic configuration so you need to upgrade EPi 7.1 project to use .NET 4.5 if you want to use this example code.

I haven't tried this code yet.

thomassvensen
thomassvensen Jan 27, 2014 11:02 AM

Thank you for a very useful tip, Linus. I was struggling a little with " setting up a script module", being totally new to Dojo et. al. But once I understood that, it worked well. Except for one thing:

After I have selected a person, when I come back to the properties page, the field is BLANK. I can see from the developer tools that it does indeed perform a rest call and retrieves the name to be displayed. But it isn't rendered on screen. I can click into the property box and re-select the user, but this is a bad user experience for the editor.

Any hints as to why are most welcome :-)

Jan 28, 2014 02:51 PM

@Thomas: I reproduced your issue and have updated the service class with a fix for the problem. Basically, we need to methods to be able to both query items as well as returning a single item. Took me some time to figure out how to register the routes for the two methods on the same "level" of URL.

thomassvensen
thomassvensen Jan 30, 2014 12:53 PM

Ah, excellent, now it works, and I also got a better understanding of web api 2.0 routing from looking at the code :-)

Thank you!

Arve Systad
Arve Systad May 16, 2014 02:47 PM

What would be truly awesome is if we could do everything in an Editor descriptor, selection factory or the like. One centralized, built in API controller could recieve the string you're searching for along with the property name. That should be enough info to dig out the correct class back end and send back some results. And maybe there could even be a simple switch to choose between "Static list" and "Ajax autocomplete" for different use cases.

Cheers :-)

Jun 23, 2014 02:29 PM

Out of curiousity, the 7.0 version of this article used RestController, while here you use Web API, is Web API apicontroller the preferred method over restcontroller for doing calls in 7.5 and onwards?

Jun 23, 2014 07:40 PM

Yes Shamrez, your observation is correct. The EPiServer API system using the RestController was built before Web API was released. We hope to be able to change/remove the EPiServer implementation in the future and therefore recommend Web API where possible.

Please login to comment.
Latest blogs
Optimizely SaaS CMS DAM Picker (Interim)

Simplify your Optimizely SaaS CMS workflow with the Interim DAM Picker Chrome extension. Seamlessly integrate your DAM system, streamlining asset...

Andy Blyth | Nov 21, 2024 | Syndicated blog

Optimizely CMS Roadmap

Explore Optimizely CMS's latest roadmap, packed with developer-focused updates. From SaaS speed to Visual Builder enhancements, developer tooling...

Andy Blyth | Nov 21, 2024 | Syndicated blog

Set Default Culture in Optimizely CMS 12

Take control over culture-specific operations like date and time formatting.

Tomas Hensrud Gulla | Nov 15, 2024 | Syndicated blog

I'm running Optimizely CMS on .NET 9!

It works 🎉

Tomas Hensrud Gulla | Nov 12, 2024 | Syndicated blog

Recraft's image generation with AI-Assistant for Optimizely

Recraft V3 model is outperforming all other models in the image generation space and we are happy to share: Recraft's new model is now available fo...

Luc Gosso (MVP) | Nov 8, 2024 | Syndicated blog

ExcludeDeleted(): Prevent Trashed Content from Appearing in Search Results

Introduction In Optimizely CMS, content that is moved to the trash can still appear in search results if it’s not explicitly excluded using the...

Ashish Rasal | Nov 7, 2024