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

clip_image002

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") %>
    
    &nbsp;&nbsp;
   
    <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") %>
    
    &nbsp;&nbsp;

    <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.

clip_image002[1]

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") %>
    
    &nbsp;&nbsp;

    <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.

Download the Code

13 Comments

  • 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.

  • 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?

  • @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 tag in my View, BUT at the same time, I need to build the action of the form using the method which is not allowed to use in server side controls (with a runat attribute).

    At this point I started researching about the subject and discovered that I not only can't use server side controls with MVC and that they are against the MVC pattern.

  • 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

  • be careful copying the code, there's an error in the javascript.

    listing 4, line 31: newnewoption -> newoption
    listing 6, line 41: newnewoption -> newoption

  • The first option would be the best in my case, except that I cannot set default values for the CascadingDropDownList. SelectList has a parameter for this (selectedValue object). How could I implement something similar with CascadingDropDownList?

    For example, let say the user previously selected BMW -> 5 Series. I would like that the next time the user enters in the page, it shows BMW and all the options for BMW in the Car Model, with the 5 Series selected.

    Thanks,
    Jorge

  • This is really invaluable. I searched for some information regarding a straightforward way to use AJAX style calls to create such a dropdown ASP.NET using a Web Service but kept coming across the AJAX Control Toolkit. I don't having a Web Service that returns a CascadingDropDownItem array, that's pretty tightly coupled.

    Thanks for the article!

Comments have been disabled for this content.