次の方法で共有


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

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:
  1. On the view, add @using YourHelper.Namespace (I tried adding it under <pages><namespaces> and it didn't work)
  2. In the helper class file, reference both System.Web.Mvc and System.Web.Mvc.Html Thanks!!!
  • Anonymous
    September 08, 2013
    Excellent

  • Anonymous
    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 Destinations

  • Anonymous
    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。