jQuery Autocomplete in ASP.NET MVC

This time I'm going to write about using jQuery Autocomplete plugin in ASP.NET MVC. If we want to use the plugin, we should reference following JavaScript and CSS:
<link href="<%= Url.Content("~/Content/jquery.autocomplete.css") %>" rel="stylesheet" type="text/css" />
<script src="<%= Url.Content("~/Scripts/jquery-1.4.1.min.js") %>" type="text/javascript"></script>
<script src="<%= Url.Content("~/Scripts/jquery.autocomplete.min.js") %>" type="text/javascript"></script>
We will start by creating simple autocomplete with static data (below JavaScript assumes that there is already a input with type text and id region):
//Static data for autocomplete
var regions = ['Eastern', 'Western', 'Northern', 'Southern'];

$(document).ready(function () {
//Adding autocomplete to region input
$("#region").autocomplete(regions, {
//Minimum number of characters a user has to type before the autocompleter activates
minChars: 0,
//The number of items in the select box
max: 4,
//Fill the input while still selecting a value
autoFill: true,
//Only suggested values are valid
mustMatch: true,
//The comparison doesn't looks inside
matchContains: false
});
});
Now we will extend regions so it contains objects with regionId and regionDescription properties. We want to display the regionDescription in autocomplete and put selected regionId into input of type hidden. To achieve this, we will use autocomplete callbacks for formatting data and result. But first let's change markup:
<%: Html.Hidden("regionId", -1) %>
<label for="regionDescription">Region:</label> <%: Html.TextBox("regionDescription")%>
And here is modified JavaScript:
//Static data for autocomplete
var regions = [
{ regionId: 1, regionDescription: 'Eastern' },
{ regionId: 2, regionDescription: 'Western' },
{ regionId: 3, regionDescription: 'Northern' },
{ regionId: 4, regionDescription: 'Southern' }
];

$(document).ready(function () {
//Adding autocomplete to region input
$("#regionDescription").autocomplete(regions, {
//Minimum number of characters a user has to type before the autocompleter activates
minChars: 0,
//The number of items in the select box
max: 4,
//Fill the input while still selecting a value
autoFill: true,
//Only suggested values are valid
mustMatch: true,
//The comparison doesn't looks inside
matchContains: false,
//Callback which will format items for autocomplete list
formatItem: function (data, index, max) {
//Display only descriptions in list
return data.regionDescription;
},
//Callback which will format items for matching
formatMatch: function (data, index, max) {
//Match only descriptions
return data.regionDescription;
},
//Callback which will format result
formatResult: function (data, index, max) {
//Display only description as result
return data.regionDescription;
}
//Callback which will be called when user chooses value
}).result(function (event, data, formatted) {
//Set region id as hidden input value
if (data)
$("#regionId").val(data.regionId);
else
$("#regionId").val('-1');
});
});
What if we want to achieve the same behavior, but with remotely loaded data? We should start by adding markup for new inputs:
<%: Html.Hidden("territoryId", -1)%>
<label for="territoryDescription">Territory:</label> <%: Html.TextBox("territoryDescription")%>
The JavaScript isn't that much different, we just need to pass URL instead of array with data (you can also adjust local cache size):
//Adding autocomplete to territoryDescription input
$("#territoryDescription").autocomplete('<%: Url.Action("Territories", "Home") %>', {
//Don't fill the input while still selecting a value
autoFill: false,
//Only suggested values are valid
mustMatch: true,
//The comparison doesn't looks inside
matchContains: false,
//The number of query results to store in cache
cacheLength: 12,
//Callback wich will format items for autocomplete list
formatItem: function (data, index, max) {
//Display only descriptions in list
return data[1];
},
//Callback which will format items for matching
formatMatch: function (data, index, max) {
//Match only descriptions
return data[1];
},
//Callback which will format result
formatResult: function (data, index, max) {
//Display only description as result
return data[1];
}
//Callback which will be called when user chooses value
}).result(function (event, data, formatted) {
//Set territory id as hidden input value
if (data)
$("#territoryId").val(data[0]);
else
$("#territoryId").val('-1');
});
Controller action will receive three parameters: q - the string we should search for, limit - maximum number of items we should return (equal to max option) and timestamp. The plugin is expecting quite unusual response. It should be a string, where '\n' is a row delimeter and '|' is cells separator. So the action will look like this (I will skip database access code):
public ContentResult Territories(string q, int limit, Int64 timestamp)
{
StringBuilder responseContentBuilder = new StringBuilder();
IQueryable<Territory> territories = _repository.GetTerritories(q, limit);
foreach (Territory territory in territories)
responseContentBuilder.Append(String.Format("{0}|{1}\n", territory.TerritoryID, territory.TerritoryDescription));

return Content(responseContentBuilder.ToString());
}
So our autocomplete is now populated from server. Lets take it one step further, and make territory dependant on region. We will use extraParams for this:
//Adding autocomplete to territoryDescription input
$("#territoryDescription").autocomplete('<%: Url.Action("Territories", "Home") %>', {
//Don't fill the input while still selecting a value
autoFill: false,
//Only suggested values are valid
mustMatch: true,
//The comparison doesn't looks inside
matchContains: false,
//The number of query results to store in cache
cacheLength: 12,
//Extra parameters which will be put into query string
extraParams: {
regionId: function () { return $("#regionId").val(); }
},
//Callback which will format items for autocomplete list
formatItem: function (data, index, max) {
//Display only descriptions in list
return data[1];
},
//Callback which will format items for matching
formatMatch: function (data, index, max) {
//Match only descriptions
return data[1];
},
//Callback which will format result
formatResult: function (data, index, max) {
//Display only description as result
return data[1];
}
//Callback which will be called when user chooses value
}).result(function (event, data, formatted) {
//Set territory id as hidden input value
if (data)
$("#territoryId").val(data[0]);
else
$("#territoryId").val('-1')
});
After that change we will receive on more parameter in our action:
public ContentResult Territories(string q, int regionId, int limit, Int64 timestamp)
{
StringBuilder responseContentBuilder = new StringBuilder();
IQueryable<Territory> territories = _repository.GetTerritories(regionId, q, limit);
foreach (Territory territory in territories)
responseContentBuilder.Append(String.Format("{0}|{1}\n", territory.TerritoryID, territory.TerritoryDescription));

return Content(responseContentBuilder.ToString());
}
This way we have finished our simple sample. There are few more interesting options in this plugin (for example you can attach it to textarea and/or allow multiply selection). Our sample application looks like this:


You can download source code from here.