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:
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.
Nice! Finally starts to feel like you wont need a rocket scientist team to customize properties. ;)
Very nice Linus. Keep up the good posts. Very true Fredrik! ;)
Great post, Linus. Now creating custom properties using dojo feels a little less like 'the dark side' :)
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.
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
I think that the code should work in 7.1 as well, though I have not tested it myself.
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.
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 :-)
@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.
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!
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 :-)
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?
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.