Imagine that you want to create edit view for Company
entity which has two properties: Name
(type string)
and Boss
(type Person
). You want both properties to be editable. For Company.
Name
, simple text input is enough but for Company.Boss
, you want to use jQuery UI Autocomplete widget*. This widget has to meet the following requirements:
- Suggestions should appear when user starts typing person's last name or presses down arrow key
- Identifier of person selected as boss should be sent to the server
- Items in the list should provide additional information (first name and date of birth)
- User has to select one of the suggested items (arbitrary text is not acceptable)
- The
boss
property should be validated (with validation message and style set for appropriate input field)
The above requirements appear quite often in web applications. I've seen many over-complicated ways in which they were implemented. I want to show you how to do it quickly and cleanly... The assumption is that you have basic knowledge about jQuery UI Autocomplete and ASP.NET MVC. In this post, I will show only the code which is related to autocomplete functionality but you can download the full demo project here. It’s ASP.NET MVC 5/Entity Framework 6/jQuery UI 1.10.4 project created in Visual Studio 2013 Express for Web and tested in Chrome 34, FF 28 and IE 11 (in 11 and 8 mode).
So here are our domain classes:
public class Company
{
public int Id { get; set; }
[Required]
public string Name { get; set; }
[Required]
public Person Boss { get; set; }
}
public class Person
{
public int Id { get; set; }
[Required]
[DisplayName("First Name")]
public string FirstName { get; set; }
[Required]
[DisplayName("Last Name")]
public string LastName { get; set; }
[Required]
[DisplayName("Date of Birth")]
public DateTime DateOfBirth { get; set; }
public override string ToString()
{
return string.Format("{0}, {1} ({2})", LastName, FirstName, DateOfBirth.ToShortDateString());
}
}
Nothing fancy there, few properties with standard attributes for validation and good looking display. Person
class has ToString
override – the text from this method will be used in autocomplete suggestions list.
Edit view for Company
is based on this view model:
public class CompanyEditViewModel
{
public int Id { get; set; }
[Required]
public string Name { get; set; }
[Required]
public int BossId { get; set; }
[Required(ErrorMessage="Please select the boss")]
[DisplayName("Boss")]
public string BossLastName { get; set; }
}
Notice that there are two properties for Boss
related data.
Below is the part of edit view that is responsible for displaying input field with jQuery UI Autocomplete widget for Boss
property:
<div class="form-group">
@Html.LabelFor(model => model.BossLastName, new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.TextBoxFor(Model => Model.BossLastName,
new { @class = "autocomplete-with-hidden",
data_url = Url.Action("GetListForAutocomplete", "Person") })
@Html.HiddenFor(Model => Model.BossId)
@Html.ValidationMessageFor(model => model.BossLastName)
</div>
</div>
form-group
and col-md-10
classes belong to Bootstrap framework which is used in MVC 5 web project template – don’t bother with them. BossLastName
property is used for label, visible input field and validation message. There’s a hidden input field which stores the identifier of selected boss (Person
entity). @Html.TextBoxFor
helper which is responsible for rendering visible input field defines a class and a data attribute. autocomplete-with-hidden
class marks inputs that should obtain the widget. data-url
attribute value is used to inform about the address of action method that provides data for autocomplete. Using Url.Action
is better than hardcoding such address in JavaScript file because helper takes into account routing rules which might change.
This is HTML markup that is produced by the above Razor code:
<div class="form-group">
<label class="control-label col-md-2" for="BossLastName">Boss</label>
<div class="col-md-10">
<span class="ui-helper-hidden-accessible"
role="status" aria-live="polite"></span>
<input name="BossLastName"
class="autocomplete-with-hidden ui-autocomplete-input"
id="BossLastName" type="text" value="Kowalski"
data-val-required="Please select the boss"
data-val="true" data-url="/Person/GetListForAutocomplete"
autocomplete="off">
<input name="BossId" id="BossId" type="hidden"
value="4" data-val-required="The BossId field is required."
data-val-number="The field BossId must be a number." data-val="true">
<span class="field-validation-valid"
data-valmsg-replace="true" data-valmsg-for="BossLastName"></span>
</div>
</div>
This is JavaScript code responsible for installing jQuery UI Autocomplete widget:
$(function () {
$('.autocomplete-with-hidden').autocomplete({
minLength: 0,
source: function (request, response) {
var url = $(this.element).data('url');
$.getJSON(url, { term: request.term }, function (data) {
response(data);
})
},
select: function (event, ui) {
$(event.target).next('input[type=hidden]').val(ui.item.id);
},
change: function(event, ui) {
if (!ui.item) {
$(event.target).val('').next('input[type=hidden]').val('');
}
}
});
})
Widget’s source
option is set to a function. This function pulls data from the server by $.getJSON
call. URL is extracted from data-url
attribute. If you want to control caching or provide error handling, you may want to switch to $.ajax
function. The purpose of change
event handler is to ensure that values for BossId
and BossLastName
are set only if user selected an item from suggestions list.
This is the action method that provides data for autocomplete:
public JsonResult GetListForAutocomplete(string term)
{
Person[] matching = string.IsNullOrWhiteSpace(term) ?
db.Persons.ToArray() :
db.Persons.Where(p => p.LastName.ToUpper().StartsWith(term.ToUpper())).ToArray();
return Json(matching.Select(m => new { id = m.Id,
value = m.LastName, label = m.ToString() }), JsonRequestBehavior.AllowGet);
}
value
and label
are standard properties expected by the widget. label
determines the text which is shown in suggestion list, value
designates what data is presented in the input filed on which the widget is installed. id
is custom property for indicating which Person
entity was selected. It is used in select
event handler (notice the reference to ui.item.id
): Selected ui.item.id
is set as a value of hidden input field - this way it will be sent in HTTP request when user decides to save Company
data.
Finally, this is the controller method responsible for saving Company
data:
public ActionResult Edit([Bind(Include="Id,Name,BossId,BossLastName")] CompanyEditViewModel companyEdit)
{
if (ModelState.IsValid)
{
Company company = db.Companies.Find(companyEdit.Id);
if (company == null)
{
return HttpNotFound();
}
company.Name = companyEdit.Name;
Person boss = db.Persons.Find(companyEdit.BossId);
company.Boss = boss;
db.Entry(company).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
return View(companyEdit);
}
Pretty standard stuff. If you've ever used Entity Framework, the above method should be clear to you. If it's not, don't worry. For the purpose of this post, the important thing to notice is that we can use companyEdit.BossId
because it was properly filled by model binder thanks to our hidden input field.
That's it, all requirements are met! Easy, huh?
* You may be wondering why I want to use jQuery UI widget in Visual Studio 2013 project which by default uses Twitter Bootstrap. It's true that Bootstrap has some widgets and plugins but after a bit of experimentation, I've found that for some more complicated scenarios jQ UI does a better job. The set of controls is simply more mature...