ASP.NET MVC Tip #41 – Create Cascading Dropdown Lists with Ajax
In this tip, I demonstrate three methods of creating cascading drop down lists in an MVC application. First, I demonstrate how to write JavaScript code to update one dropdown list when another dropdown list changes. Next, I show you how you can retrieve the data for the dropdown lists from either a controller action or a web service.
A reader of this blog emailed me recently and asked how he could create cascading dropdown lists in an MVC application. Why would you want to create cascading dropdown lists? Imagine that you want a user to select a car make and model. You display a dropdown list of car makes. Each time a user selects a new car make, a dropdown list displaying car models is populated (see Figure 1).
Figure 1 – Cascading DropDown Lists
You don’t want to post the form containing the two dropdown lists back to the server each and every time a user selects a new car make. That would create a really bad user experience. Instead, you want to update the list of car models after a new car make is selected without a form post.
The reader had attempted to use the AJAX Control Toolkit CascadingDropDown control , but he encountered difficulties in getting this control to work in the context of an MVC application.
In this situation, I would not recommend using the AJAX Control Toolkit. Instead, I would consider performing Ajax calls to get the data. I would use pure JavaScript to populate the HTML <select> elements in the view after retrieving the data from the server.
In this tip, I demonstrate three methods of creating cascading dropdown lists. First, I show you how to alter the list of options displayed by one dropdown list when an option in another dropdown list changes. Second, I show you how to expose the data for the dropdown lists through a controller action. Next, I show you how to grab the data for the dropdown lists from web services.
Updating DropDown Lists on the Client
Before you start making Ajax calls from the browser to the server to update the list of options displayed in a dropdown list, you should first consider whether these Ajax calls are really necessary. Do you really need to get the data from the server at all? In many situations, it makes more sense to create a static array of options on the page and use JavaScript to filter one dropdown list when another dropdown list changes.
In this section, I demonstrate how you can create an HTML helper that renders a cascading dropdown list. The dropdown list changes the list of items it displays when a new option is selected in a second dropdown list.
Let me start with the controller. The Home controller in Listing 1 adds two collections to ViewData. The first collection of items represents car makes. This collection is represented with the standard SelectList collection class included in the ASP.NET MVC framework. This first collection is used when rendering the dropdown list that displays car makes.
The second collection is used to represent car models. This collection is represented by a new type of collection that I created called a CascadingSelectList collection. Unlike a normal SelectList collection, every item in a CascadingSelectList collection has three properties: Key, Value, and Text. The CascadingDropDownList collection is used when rendering the cascading drop down list.
The new property, the Key property, is used to associate items in the second drop down list with items in the first drop down list. The Key property represents the foreign key relationship between the Models and Makes database tables.
Listing 1 – Controllers\HomeController.cs
using System.Linq; using System.Web.Mvc; using Tip41.Helpers; using Tip41.Models; namespace Tip41.Controllers { [HandleError] public class HomeController : Controller { private CarDataContext _dataContext; public HomeController() { _dataContext = new CarDataContext(); } public ActionResult Index() { // Create Makes view data var makeList = new SelectList(_dataContext.Makes.ToList(), "Id", "Name"); ViewData["Makes"] = makeList; // Create Models view data var modelList = new CascadingSelectList(_dataContext.Models.ToList(), "MakeId", "Id", "Name"); ViewData["Models"] = modelList; return View("Index"); } } }
The view in Listing 2 displays the dropdown lists for selecting a car make and car model. The first dropdown list is rendered with the standard DropDownList() helper. The second dropdown list is rendered with a new helper method named CascadingDropDownList().
Listing 2 – Views\Home\Index.aspx
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Index.aspx.cs" Inherits="Tip41.Views.Home.Index" %> <%@ Import Namespace="Tip41.Helpers" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server"> <title>Index</title> <script type="text/javascript" src="../../Content/MicrosoftAjax.js"></script> <script type="text/javascript" src="../../Content/CascadingDropDownList.js"></script> </head> <body> <div> <label for="Makes">Car Make:</label> <%= Html.DropDownList("--Select Make--", "Makes") %> <label for="Makes">Car Model:</label> <%= Html.CascadingDropDownList("Models", "Makes") %> </div> </body> </html>
The CascadingDropDownList() helper method expects two arguments: name and associatedDropDownList. The name argument is used in multiple ways. First, it becomes both the name and id of the <select> tag rendered by the CascadingDropDownList() helper. Furthermore, the name parameter is used to retrieve the CascadingSelectList collection from ViewData. If the ViewData dictionary does not contain an item that corresponds to the name argument, an exception is thrown.
Notice that the Index view includes references to two JavaScript libraries. The first JavaScript library is the standard ASP.NET AJAX Library. The second library contains the JavaScript code required for the cascading drop down list to work.
All of the code for the CascadingDropDownList() helper method is included with the project that you can download at the end of this blog entry. This helper method does something simple. It creates a JavaScript array that includes all of the possible options that could be displayed by the cascading dropdown list. When a new option is selected in the Makes dropdown list, the list of all possible options is filtered in the Models dropdown list.
The advantage of the approach taken in this section to building a cascading dropdown list is that no communication needs to happen between the browser and server. After the page gets rendered to the browser, all of the filtering happens in the browser. In other words, this approach is very fast and robust.
If you are only working with a few hundred options then you should take the approach to building a cascading dropdown list described in this section. However, if you need to work with thousands or millions of options then you’ll need to adopt one of the two approaches discussed in the following two sections.
Creating Cascading Dropdown Lists with Controller Actions
In this section, I explain how you can create a cascading dropdown list by retrieving options from a controller action. Selecting a new option from one dropdown list causes a second dropdown list to retrieve a new set of options by invoking a controller action on the server.
Let’s start by creating the controller. The Action controller is contained in Listing 3.
Listing 3 – Controllers\ActionController.cs
using System.Linq; using System.Web.Mvc; using Tip41.Models; namespace Tip41.Controllers { public class ActionController : Controller { private CarDataContext _dataContext; public ActionController() { _dataContext = new CarDataContext(); } public ActionResult Index() { var selectList = new SelectList(_dataContext.Makes.ToList(), "Id", "Name"); ViewData["Makes"] = selectList; return View("Index"); } public ActionResult Models(int id) { var models = from m in _dataContext.Models where m.MakeId == id select m; return Json(models.ToList()); } } }
The controller in Listing 3 exposes two actions named Index() and Models(). The Index() action returns a view and the Models() action returns a JSON (JavaScript Object Notation) array. The Models() action is responsible for returning matching car models when a new car make is selected in the view.
The view is contained in Listing 4. Notice that it contains a script include for the Microsoft AJAX Library.
Listing 4 – Views\Action\Index.aspx
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Index.aspx.cs" Inherits="Tip41.Views.Action.Index" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server"> <title>Index</title> <script type="text/javascript" src="../../Content/MicrosoftAjax.js"></script> <script type="text/javascript"> var ddlMakes; var ddlModels; function pageLoad() { ddlMakes = $get("Makes"); ddlModels = $get("Models"); $addHandler(ddlMakes, "change", bindOptions); bindOptions(); } function bindOptions() { ddlModels.options.length = 0; var makeId = ddlMakes.value; if (makeId) { var url = "/Action/Models/" + makeId; getContent(url, bindOptionResults); } } function bindOptionResults(data) { var newOption; for (var k = 0; k < data.length; k++) { newOption = new Option(data[k].Name, data[k].Id); ddlModels.options.add(newOption); } } /**** should be in library ***/ function getContent(url, callback) { var request = new Sys.Net.WebRequest(); request.set_url(url); request.set_httpVerb("GET"); var del = Function.createCallback(getContentResults, callback); request.add_completed(del); request.invoke(); } function getContentResults(executor, eventArgs, callback) { if (executor.get_responseAvailable()) { callback(eval("(" + executor.get_responseData() + ")")); } else { if (executor.get_timedOut()) alert("Timed Out"); else if (executor.get_aborted()) alert("Aborted"); } } </script> </head> <body> <div> <label for="Makes">Car Make:</label> <%= Html.DropDownList("--Select Make--", "Makes") %> <label for="Makes">Car Model:</label> <select name="Models" id="Models"></select> </div> </body> </html>
When the view in Listing 4 is displayed in a web browser, two dropdown lists are displayed (see Figure 2). The first dropdown list is rendered with the DropDownList() helper method. The options displayed in the second dropdown list is constructed with JavaScript code.
The pageLoad() method in Listing 4 executes when the document finishes loading. This method sets up a handler for the change event for the first dropdown list. When you select a new car make, the JavaScript bindOptions() method is executed. This method invokes the Models controller action to retrieve a list of matching car models for the select make. The matching models are added to the second dropdown list in the JavaScript bindOptionResults() method.
Using the approach described in this section for creating a cascading dropdown list makes sense when you have too many options to include in the page when the page is first rendered. For example, if you are working with a car parts database that contains millions of parts, then the approach described in this section makes perfect sense.
Creating Cascading Dropdown Lists with Web Services
In this final section, I demonstrate an alternative approach to creating a cascading dropdown list in an MVC view. Instead of invoking a controller action to retrieve a list of matching options, you can invoke a web service to retrieve the options.
Imagine that your application includes the web service in Listing 5. This service exposes one method named Models that returns all of the car models that match a particular car make.
Listing 5 – Services\CarService.asmx
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Services; using Tip41.Models; namespace Tip41.Services { [WebService(Namespace = "http://tempuri.org/")] [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)] [System.ComponentModel.ToolboxItem(false)] [System.Web.Script.Services.ScriptService] public class CarService : System.Web.Services.WebService { [WebMethod] public List<Model> Models(int makeId) { var dataContext = new CarDataContext(); var models = from m in dataContext.Models where m.MakeId == makeId select m; return models.ToList(); } } }
Notice that the web service is decorated with the ScriptService attribute. Using the ScriptService attribute is required when you want to be able to call a web method from the browser.
The view in Listing 6 displays the same two dropdown lists as the views in the previous two sections. However, the JavaScript code in this view invokes a web service instead of a controller action.
Listing 6 – Views\Service\Index.aspx
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Index.aspx.cs" Inherits="Tip41.Views.Service.Index" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server"> <title>Index</title> <script type="text/javascript" src="../../Content/MicrosoftAjax.js"></script> <script type="text/javascript"> var ddlMakes; var ddlModels; function pageLoad() { ddlModels = $get("Models"); ddlMakes = $get("Makes"); $addHandler(ddlMakes, "change", bindOptions); bindOptions(); } function bindOptions() { ddlModels.options.length = 0; var makeId = ddlMakes.value; if (makeId) { Sys.Net.WebServiceProxy.invoke ( "../Services/CarService.asmx", "Models", false, { makeId: makeId }, bindOptionResults ); } } function bindOptionResults(data) { var newOption; for (var k = 0; k < data.length; k++) { newOption = new Option(data[k].Name, data[k].Id); ddlModels.options.add(newOption); } } </script> </head> <body> <div> <label for="Makes">Car Make:</label> <%= Html.DropDownList("--Select Make--", "Makes") %> <label for="Makes">Car Model:</label> <select name="Models" id="Models"></select> </div> </body> </html>
The web service is invoked with the help of the Microsoft AJAX Library Sys.Net.WebServiceProxy.invoke() method. You can use this method to invoke a web service with any name from the client.
There is really no different between the approach to creating cascading dropdown lists described in this section and the approach described in the previous section. You can use either approach when you need to render cascading dropdown lists that might display thousands of items. Whether you choose the controller action or web service approach is entirely a matter of preference.
Summary
In this tip, I’ve discussed three approaches for creating cascading dropdown lists. If you are working with a relatively small number of dropdown list options (hundreds rather than thousands) than I recommend that you take the approach described in the first section. Use the CascadingDropDownList() helper method to render a static JavaScript array of all of the possible options. That way, you don’t need to communicate between the browser and server to update the options displayed by the dropdown list.
If, on the other hand, you need to support the possibility of displaying thousands of different options in a cascading dropdown list then I would take either the controller action or web service approach.