ASP.NET MVC & jQuery UI autocomplete Part 2 (EditorFor)

UPDATE: I've blogged about an more flexible way to wire up the editor template here.

I recently blogged about how to enable autocompletion of a text field using jQuery UI, and how to easily hook this behaviour up in ASP.NET MVC. In this post I’ll go a step further and show how to hook this behaviour up even if you are using Html.EditorFor.

In the previous post, we specified the source of the autocomplete data in the view as we were rendering the input element, but when using EditorFor there isn’t a nice way to do this. However, EditorFor works from the model metadata and allows for rich customisation of the rendering templates. To achieve our goal of autocomplete with EditorFor we will add some extra information to the metadata for our model to indicate which properties should have autocomplete and where the data comes from, and then supply a custom template that looks for this information.

Enriching the metadata

Prior to MVC 3, adding to the metadata would have been hard work. In MVC 3 we can take advantage of a new interface: IMetadataAware. This interface can be implemented by custom attributes to indicate that you’d like to manipulate the metadata.

We’ll start by creating an AutoCompleteAttribute:

[AttributeUsage(AttributeTargets.Property)]
public class AutoCompleteAttribute : Attribute, IMetadataAware
{
    private readonly string _controller;
    private readonly string _action;

    public AutoCompleteAttribute(string controller, string action)
    {
        _controller = controller;
        _action = action;
    }

    public void OnMetadataCreated(ModelMetadata metadata)
    {
        metadata.SetAutoComplete(_controller, _action);
    }
}

This attribute can be applied to properties on our model to specify the names of the controller and action to use for the autocomplete data. It implements IMetadataAware, and in the OnMetadataCreated the values are stored in the metadata using the helper below:

private const string AutoCompleteControllerKey = "AutoCompleteController";
private const string AutoCompleteActionKey = "AutoCompleteAction";
public static void SetAutoComplete(this ModelMetadata metadata, string controller, string action)
{
    metadata.AdditionalValues[AutoCompleteControllerKey] = controller;
    metadata.AdditionalValues[AutoCompleteActionKey] = action;
}

Handling the metadata in editor templates

Now that we have the metadata available to us, we can supply a custom template for rendering text inputs. I discussed this approach for adding aria-required attributes, so if you’re not familiar with how the EditorFor templates work then take a quick read as we’ll follow a similar pattern! The starting String.cshtml is

@Html.TextBox("", ViewData.TemplateInfo.FormattedModelValue, new { @class = "text-box single-line" })

We can tweak this a little:

@model string
@using AutocompleteBlog.Infrastructure;
@{
    var attributes = new RouteValueDictionary
                         {
                            { "class", "text-box single-line"}
                         };
    string autocompleteUrl = Html.GetAutoCompleteUrl(ViewContext.ViewData.ModelMetadata);
    if (!string.IsNullOrEmpty(autocompleteUrl))
    {
        attributes.Add("data-autocomplete-url", autocompleteUrl);
    }
}
@Html.TextBox("", ViewContext.ViewData.TemplateInfo.FormattedModelValue, attributes)

This is quite similar to the aria-required scenario. The main difference is that we now have a helper method: GetAutoCompleteUrl. This method is used to look in the model metadata to see if it contains the autocomplete data (controller & action), and if so to return the corresponding url. If the data is not found then it returns null. If we get an autocomplete URL back then we add the data-autocomplete-url attribute as in the original autocomplete post. The code for GetAutoCompleteUrl is pretty simple, but is included below:

public static string GetAutoCompleteUrl(this HtmlHelper html, ModelMetadata metadata)
{
    string controller = metadata.AdditionalValues.GetString(AutoCompleteControllerKey);
    string action = metadata.AdditionalValues.GetString(AutoCompleteActionKey);
    if(string.IsNullOrEmpty(controller)
        || string.IsNullOrEmpty(action))
    {
        return null;
    }
    return UrlHelper.GenerateUrl(null, action, controller, null, html.RouteCollection, html.ViewContext.RequestContext, true);
}
private static string GetString(this IDictionary<string, object> dictionary, string key)
{
    object value;
    dictionary.TryGetValue(key, out value);
    return (string)value;
}

Putting it into action

Now that we have an attribute that can store the metadata and a template that checks for it, we’re ready to give it a try!

First off, our we need to annotate our model:

public class SomeOtherModel
{
    [AutoComplete("Home", "Autocomplete")]
public string SomeValue { get; set; }
    public string SomeOtherValue { get; set; }
}

Note the AutoComplete attribute that points to the Autocomplete action on the Home controller. Next up, our view:

@model AutocompleteBlog.Models.Home.SomeOtherModel
@{
    ViewBag.Title = "UnobtrusiveEditorFor";
}
<h2>UnobtrusiveEditorFor</h2>
@Html.EditorForModel()

We’re using Html.EditorForModel() which renders an editor for the whole model – you could also write Html.EditorFor(model=>model).

Running this gives us our autocomplete behaviour:

image

 

Wrapping up

One of the great things about ASP.NET MVC is that you typically have options for how to implement the behaviour you want (actually, some people don’t view choice as positive!). I find myself using the templated helpers (EditorFor/DisplayFor) more and more as they give a lot of convenience and flexibility. Custom HTML Helpers can still be useful and are easy to get started with. Hopefully this post (and the aria-required post) have given you a sense that with a little bit more familiarity with model metadata and with how the templates are chosen it is possible to customise the templated helpers to work the way you want them to.

AutoCompleteSample with EditorFor.zip