ASP.NET MVC & jQuery UI autocomplete

UPDATE: I've written a follow-up post that shows how to achieve this using Html.EditorFor here.

If you get me started talking about ASP.NET MVC then it is quite possible that I’ll end up talking about Progressive Enhancement or Unobtrusive JavaScript. Aside from the usability and performance benefits that these techniques can bring, I find that they also help me to write JavaScript in a more modular, more reusable way than if I hack some script into a page. This post will walk through incorporating the jQuery UI autocomplete widget into an ASP.NET MVC application in an unobtrusive manner.

Getting started with jQuery UI autocomplete

First off, let’s take take a look at autocomplete in action, starting with a text input:

<label for="somevalue">Some value:</label><input type="text" id="somevalue" name="somevalue"/>

If we add a reference to the jQuery UI script file and css file, we can add a script block to our view:

<script type="text/javascript" language="javascript">
    $(document).ready(function () {
        $('#somevalue').autocomplete({
            source: '@Url.Action("Autocomplete")'
        });
    })
</script>

This script block identifies the text input by id and then invokes the autocomplete function to wire up the autocomplete behaviour for this DOM element. We pass a URL to identify the source of the data. For this post I’ve simply created an ASP.NET MVC action that returns JSON data (shown below). Note that in the view I used Url.Action to look up the URL for this action in the routing table – avoid the temptation to hard-code the URL as this duplicates the routing table and makes it hard to change your routing later.

public ActionResult Autocomplete(string term)
{
    var items = new[] {"Apple", "Pear", "Banana", "Pineapple", "Peach"};

    var filteredItems = items.Where(
        item => item.IndexOf(term, StringComparison.InvariantCultureIgnoreCase) >= 0
        );
    return Json(filteredItems, JsonRequestBehavior.AllowGet);
}

With these components in place, we now have a functioning autocomplete textbox

image

So, what’s wrong with that? Well, it works… but we’ve got script inline in our page which is not reusable or cacheable.

Unobtrusive autocomplete

Now that we have autocomplete functioning, the next step is to move the script into a separate script file to allow us to clean up the HTML page (and also reduce its size and improve cacheability of the script). The challenge that we face is that our script needs to know which elements in the page to add behaviour to, and what URL to use for each element. One possible approach to the first problem would be to apply a marker CSS class to each element that needs the behaviour and then modify the script to locate elements by class rather than id. However, this doesn’t solve the problem of which URL to use for the autocomplete data. We’ll use a different approach as shown in “Using ASP.NET MVC and jQuery to create confirmation prompts”.

The essence of this approach is to attach additional data to the HTML elements, and then have a separate script that identifies these elements and applies the desired behaviour. This declarative approach is a common technique and is the way that validation works in ASP.NET MVC 3. The method for adding extra data is to add attributes that start with “data-“ as these are ignored by the browser. For autocomplete we will add a data-autocomplete-url attribute to specify the URL to use to retrieve the autocomplete data. The view will now look like the following snippet

<label for="somevalue">Some value:</label><input type="text" id="somevalue" name="somevalue" data-autocomplete-url="@Url.Action("AutoComplete")"/>

Now we can create a separate script file that adds autocomplete behaviour for these elements:

$(document).ready(function () {
    $('*[data-autocomplete-url]')
        .each(function () {
            $(this).autocomplete({
                source: $(this).data("autocomplete-url")
            });
        });
});

This script identifies any elements with the data-autocomplete-url attribute, and then calls autocomplete() to add the jQuery UI autocomplete widget to each element, using the value of the data-autocomplete-url attribute as the data source.

Wiring it up with a model

So far we’ve looked at views where we have been manually generating the input elements. More often in ASP.NET MVC we will be using the HTML Helpers (e.g. Html.TextBoxFor, Html.EditorFor) to create inputs that are bound to our model. In this scenario, our view (without autocomplete) might look like the following

@Html.LabelFor(m=>m.SomeValue)
@Html.TextBoxFor(m=>m.SomeValue)

Note that I’m working with a model that has a “SomeValue” property. To add the autocomplete behaviour we need to add our custom attribute to the generated input. Fortunately there are overloads for TextBoxFor that allow us to pass in additional HTML attributes. These additional attributes can be specified in a RouteValueDictionary instance, or (more commonly) as an object instance. The properties of the object instance are used to populate the attributes (property name gives the attribute name, property value the attribute value). There is one extra piece of magic that ASP.NET MVC 3 added: underscores in property names are converted to dashes in the attribute names. In other words, a property name of “data_autocomplete_url” results in an attribute name of “data-autocomplete-url”. This is particularly handy since C# isn’t too keen on dashes in property names! Armed with this knowledge we can modify our view:

@Html.LabelFor(m=>m.SomeValue)
@Html.TextBoxFor(m=>m.SomeValue, new { data_autocomplete_url = Url.Action("Autocomplete")})

With this change the autocomplete script now adds the behaviour for us

Html.AutocompleteFor

Finally, we can simplify this further by creating a custom HTMl Helper: AutocompleteFor. The HTML Helpers are all extension methods, and the AutocompleteFor method is fairly simple as it just needs to obtain the URL and then call TextBoxFor:

public static class AutocompleteHelpers
{
    public static MvcHtmlString AutocompleteFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression, string actionName, string controllerName)
    {
        string autocompleteUrl = UrlHelper.GenerateUrl(null, actionName, controllerName,
                                                       null,
                                                       html.RouteCollection,
                                                       html.ViewContext.RequestContext,
                                                       includeImplicitMvcValues: true);
        return html.TextBoxFor(expression, new { data_autocomplete_url = autocompleteUrl});
    }
}

With this helper the view now reads a little more easily

@Html.LabelFor(m=>m.SomeValue)
@Html.AutocompleteFor(m=>m.SomeValue, "Autocomplete", "Home")

Note that for completeness, some overloads for AutoCompleteFor should be added to map to the various TextBoxFor overloads and Autocomplete helpers should be created to align to the TextBox helpers.

Wrapping up

Hopefully the helpers in this post are useful as-is, but the key principal can be applied in a range of scenarios. If you find script directly in your views, take a moment to consider whether there are benefits to extracting it into a reusable script. Once you have a separate script file you can also minify it, combine it and cache it!

AutoCompleteSample.zip