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 – ControllersHomeController.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 – ViewsHomeIndex.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 – ControllersActionController.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 – ViewsActionIndex.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 – ServicesCarService.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 – ViewsServiceIndex.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.
Isn’t we are going to classic ASP [but in object oriented] way by using the MVC fraemework
@kamil47 – Yes, I think this is close to the truth. Creating views in MVC is a similar experience to creating classic ASP pages. However, the ASP.NET framework adds a lot to the experience of building MVC applications: built-in caching support, authorization and authentication, pre-compilation for really good performance, and so on.
Excellent as ever Stephen. These posts really are indispensable. Have you seen the jQuery cascading drop down here?:
devlicio.us/…/…ascading-values-from-forms.aspx
From my limited knowledge of ajax, I think jQuery could be a better fit for MVC than the MS toolkit it it’s current form.
If you’re trying out the stackoverflow beta here’s the question that started it for me:
beta.stackoverflow.com/…/have-you-got-a-casca…
Keep them comming.
@IainMH — Thanks for the link! Really interesting article.
@Kamil47: apart from the arguments by Stephen, you can argue that the ASP code you have in a MVC app is just code to build your GUI. All the real (business) logic is in the controller and the objects that the controller uses. That’s good OOP I think.
Why not just use the AJAX Control Toolkit cascading drop down lists?
Nice one…
@GH: because they are server side controls which is not a thing to use with the MVC pattern. You should avoid them at all costs.
In fact I was the guy who asked Stephen about this (“A reader of this blog emailed bla bla bla”), and I was using the AJAX Toolkit before, but I started to notice how bad is to use server side controls when I was stuck with this:
To use AJAX Toolkit I need a
Is it possible to use this “control” in a partial view and have the javascript work? The ScriptManager class can’t be used to register the client script.
I found this old link on the ASP.NET forums: http://forums.asp.net/p/1200582/2088640.aspx. However, that’s pretty out-of-date info… I would think there must be something a little more clean now that basic AJAX is supported in MVC.
Great “tip”. I’d greatly appreciate a little help taking it a step farther. TIA!
Attempting to compile the download I get “The project type is not supported by this installation.” I have a straight 3.5 installation – what else might I need to download?
TIA,
Doug
I have the same problem as Doug
“The project type is not supported by this installation.”
I have tried installing several extra bits, please advise
I’ve extended your controls in 2 ways:
1) it now remembers the selected values after a postback
2) I added a way to add a blank option (–select a model–)
If anyone is interested you can contact me on my email address.
And my email address is:
[email protected]
I am new to MVC, i guess it will help me a lot, thanks, keep it up good work
Hi Stephen,
I’m new to Ajax and I find your examples extremely useful. However when I tried incorporating the methid 2 mentioned in your article, I get a JS error at this position.
callback(eval(“(” + executor.get_responseData() + “)”));
Could you kindly let me know any reasons for this?
Good Stuff Stephen…I’ve been a fan since 1994.
Hey, this looks great.
I’m trying to implement it in my application but something doesn’t seem right. At runtime, the javascript and jquery generated seem all correct (the allOptions is set to an array with the correct data), however nothing happens when I change the selection of the first dropdown list.
Any help on this?
Thanks
Oh, by the way, I tried to use the first method, since my lists are rather short.
me too, i have this:
Hi,
Is it possible to set the cascading drop-down box to a value stored in a database. For example on an edit page?
Not running on VS2008, update the code…
Update this code please?
It is not running well on the MVC1.0
Hello Stephen,
I read all your blogs and the books about asp.net unleashed 1st/2nd edition. They are very good books even in China.
I have lots of questions about aspnetmvc, :). This time I just ask the one about tip#41.
The CaseCading Dropdownlist is very usefull but how can I do when I wanna post the data in the second or the third dropdownlist like the CarModel in your demo?
Thanks for ur good blogs.
Please update the code. Cannot open on VS2008 MVC 1.0
Hi,
I have seen your post.
It is quite good. But can you tell me how to save the value of the dropdownlist on post back.
As i am binding the dropdown on client side and as i click on the submit button the value is not in dropdown
tell me the solution
Please update the code.It cannot open
I tried to build this example and it doesn’t appear to work with the current version of MVC. Any advice on what to change?
reborn.
I’m having problems recreating this solution, I get an error in the “CarDataContext” it seems i’m missing a using directive but they are the same as the example. Can someone help me?
Nice Post! Really cascading dropdown list is very useful This will help me a lot. Thanks.
I’m having some problems when trying to implement this solution.
I’m using ADO.net entity framework and i try to implement this example in my project, but an error occurs, in the file MicrosoftAjax.js more exactly in finally{if(a._xmlHttpRequest!=null) (sintax error), when it is called by the next function.
function getContentResults(executor, eventArgs, callback) {
alert(“getcontentresults”);
if (executor.get_responseAvailable()) {
callback(eval(“(” + executor.get_responseData() + “)”));
alert(“passou o callback”);
}
else {
if (executor.get_timedOut())
alert(“Timed Out”);
else if (executor.get_aborted())
alert(“Aborted”);
}
}
Some one have ideas how to resolve this problem, or what I’m doing wrong.
I have some issues with using this code.
I am using the second method where i am binding them straight to controller actions. I get the first dropdown to populate.
But, for some reason, i cannot hit the controller action while on onChange of the first dropdown list. I checked the javascript which doesnt seem to have any typos or errors in it.
Any reason why it will not post? I think the major reason is it is not posting, so it isnt hittting the controller action. I am using Ajax.Beginform()
Better examined some potential pitfalls on my blog:
http://www.ipodtouchtopc.com
new Thank you for sharing the this code.
Hi
I downloaded the sample code and tried to run it and I get the following error.
There is no ViewData item with the key ‘–Select Make–‘ of type ‘IEnumerable‘
Im new to ASP.net MVC
There is no ViewData item with the key ‘–Select Make–‘ of type ‘IEnumerable’Online education accreditation | Distance education accreditation
International Accreditation
Nice Post! Really cascading dropdown list is very useful This will help me a lot. Thanks.
f4323
I believe it is a promising (currently version 4.0). So I would stick with it.. thanks
Wow thanks a lot I was looking for it very long time Thnksssssssssss
Thanks!!!. It is a remarkable code
I’m having problems recreating this solution, I get an error in the “CarDataContext” it seems I’m missing a using directive but they are the same as the example…
classifieds |job listings |articles
Controller knowing how the data will be viewed (“SelectList” etc) in your “Index()” method, isn’t that a bad design in itself?