One of the common tasks that comes up when developing web applications is working with dependent data. If you have a form in your application that asks the user to select the Make and Model of their car, then you need to refresh the list of Models every time the Make changes. In ASP.net Web Forms this was commonly done by setting the AutoPostBack property of the DropDownList control and creating a SelectedIndexChanged event handler. The problem with this approach is that the page does a full postback, which can often be inefficient and can appear slow to the user. Fortunately, a better method exists in ASP.net MVC that is easy to accomplish with jQuery.
In order to show you how to create a cascading dropdown I am going to create a very simple web application. It will have a single page that will have two dropdowns, one for a list of States and one for a list of Cities. The list of Cities will change whenever the State changes. Here is the main controller for our application:
public class HomeController : Controller
{
private ILocationRepository locationRepository = new LocationRepository();
public ActionResult Index()
{
var model = new IndexViewModel();
model.AvailableStates = new SelectList(locationRepository.GetStates(), "Abbreviation", "Name");
model.AvailableCities = new SelectList(locationRepository.GetCities(), "Id", "Name");
return View(model);
}
}
public class IndexViewModel
{
public IndexViewModel()
{
AvailableStates = new SelectList(Enumerable.Empty<State>(), "Abbreviation", "Name");
AvailableCities = new SelectList(Enumerable.Empty<City>(), "Id", "Name");
}
public string State { get; set; }
public int City { get; set; }
public SelectList AvailableStates { get; set; }
public SelectList AvailableCities { get; set; }
}
The view for our page will be extremely simple. It will consist of the two dropdown lists needed to display the data and a bit of javascript used to retrieve the data.
@model CascadingDropdowns.Models.IndexViewModel
@using (Html.BeginForm())
{
<div class="editor-label">
@Html.LabelFor(m => m.State)
</div>
<div class="editor-field">
@Html.DropDownListFor(m => m.State, Model.AvailableStates, new { style = "width: 150px" })
</div>
<div class="editor-label">
@Html.LabelFor(m => m.City)
</div>
<div class="editor-field">
@Html.DropDownListFor(m => m.City, Model.AvailableCities, new { style = "width: 150px" })
</div>
<p>
<input type="submit" value="Submit" />
</p>
}
I am holding off on posting the javascript code until I post the code to retrieve the locations. The view is pretty simple. It defines a label and a dropdown for both the State and City property of our model. It uses the AvailableStates and AvailableCities properties as the data source for the dropdowns. It also contains a Submit button to POST the form, but this example won't be doing anything when the form is posted.
Here is the controller we will use to retrieve our location data. It has two action methods that both return JSON formatted data. The first method will return a list of all states. The other method will retrieve all of the cities based on the state abbreviation that is passed in.
public class LocationsController : Controller
{
private ILocationRepository locationRepository = new LocationRepository();
[HttpPost]
public ActionResult States()
{
var states = locationRepository.GetStates();
return Json(new SelectList(state, "Id", "Name"));
}
[HttpPost]
public ActionResult Cities(string abbreviation)
{
var cities = locationRepository.GetCities(abbreviation);
return Json(new SelectList(cities, "Abbreviation", "Name"));
}
}
Here is the corresponding repository class that will retrieve the data. I am going to hard code the data to simplify the example, although it would be easy to hook up this code to a database using your favorite data access tool such as Entity Framework or LINQ to SQL.
public class LocationRepository : ILocationRepository
{
public IQueryable<State> GetStates()
{
return new List<State>
{
new State { Abbreviation = "NE", Name = "Nebraska" },
new State { Abbreviation = "NC", Name = "North Carolina" }
}.AsQueryable();
}
public IQueryable<City> GetCities(string abbreviation)
{
var cities = new List<City>();
if (abbreviation == "NE")
{
cities.AddRange(new List<City> {
new City { Id = 1, Name = "Omaha" },
new City { Id = 2, Name = "Lincoln" }
});
}
else if (abbreviation == "NC")
{
cities.AddRange(new List<City> {
new City { Id = 3, Name = "Charlotte" },
new City { Id = 4, Name = "Raleigh" }
});
}
return cities.AsQueryable();
}
}
public interface ILocationRepository
{
IQueryable<State> GetStates();
IQueryable<City> GetCities(string abbreviation);
}
Now that we have a controller action that will return the data we need, it is time to wrap it all up with our jQuery code. You might have noticed that our controller actions have been marked with the [HttpPost] attribute. This will prevent the browser from caching the data, and MVC doesn't allow JSON GET requests by default. Here is the jQuery code:
<script type="text/javascript" src="/Scripts/jquery-1.5.1.min.js"></script>
<script type="text/javascript">
function getCities(abbr) {
$.ajax({
url: "@Url.Action("Cities", "Locations")",
data: {abbreviation: abbr},
dataType: "json",
type: "POST",
error: function() {
alert("An error occurred.");
},
success: function(data) {
var items = "";
$.each(data, function(i, item) {
items += "<option value=\"" + item.Value + "\">" + item.Text + "</option>";
});
$("#City").html(items);
}
});
}
$(document).ready(function(){
$("#State").change(function() {
var abbr = $("#State").val();
getCities(abbr);
});
});
</script>
That is all the code that is required to get a cascading dropdown. The jQuery code sets up an event handler that will run whenever the selected item is changed, making an AJAX call that is expecting JSON formatted data. When the AJAX call successfully completes it iterates over the collection of objects building an <option> element for each one and sets the HTML of the dropdown to the new list of items. It would be trivial to replace the code in the repository to retrieve data from a database as well, but I will leave that as an exercise for the reader