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
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!
Comments
- Anonymous
August 30, 2012
Awesome! Just what I needed! I have to mention, though, that when working on this I noticed that there are a couple of extra things to do:
- On the view, add @using YourHelper.Namespace (I tried adding it under <pages><namespaces> and it didn't work)
- In the helper class file, reference both System.Web.Mvc and System.Web.Mvc.Html Thanks!!!
Anonymous
September 08, 2013
ExcellentAnonymous
January 30, 2014
suppose ,If i need to join some item with autocomplete means, how can i do this ?Anonymous
February 03, 2014
Missing: if (searchStr == null) searchStr = "";Anonymous
February 24, 2014
Great work. Nice explanations. Thanks for sharing this information. visit: Dream DestinationsAnonymous
May 28, 2014
Great job. Very useful. Thanks for sharing.Anonymous
June 17, 2014
Excellent Job !!!!!Anonymous
June 17, 2014
Great Job !!!!Anonymous
December 26, 2014
Great!! Simple and works beautifully Thanks a ton.Anonymous
April 03, 2015
I followed this approach, but the "model" parameter is always null ! How am I supposed to populate that parameter with the user's input? @Html.TextBoxFor(m => m.Model, new { data_autocomplete_url = Url.Action("GetAllETKDModelsAutoComplete") }) actually invokes the corresponding method when I type something in the textbox, but model == null How can public ActionResult AutoComplete(string model) { var allModels = _repository.GetAllModels(); var filteredItems = model != null ? allModels.Where(item => item.IndexOf(model, StringComparison.InvariantCultureIgnoreCase) >= 0) : new List<string>(); return Json(filteredItems, JsonRequestBehavior.AllowGet); }Anonymous
December 10, 2015
Well,thus to my English so bad, I mostly can't understand anything ! T_T。