< Code />

Cascading dropdown lists with MVC4 and jQuery by dotnetjunkie, Monday, February 18, 2013.

We are huge fans of ASP.NET MVC, particularly MVC4. Although, this post isn't specific to MVC4. Microsoft gave people a bad taste in their mouth through the years. ASP.NET was a huge step and at the time was pretty amazing. But the garbled mess of HTML and huge viewstate payload really turned a lot of people off. Those people chose to use platforms such as Ruby on Rails for it's simplicity and clean markup.

However, when Microsoft released MVC, web development has changed and we now have super clean semantic HTML and nice separation of code. Many of you who are familiar with ASP.NET MVC, understand the concept of Views and Partial Views. Utliizing Partial Views and controller actions provides a nice separation and a really easy way to handle cascading dropdown lists.

Let's assume that we have a class that has an ID and a Name.


public class DropdownItem
{
    public int ItemID { get; set; }
    public string ItemName { get; set; }
}

Next we have a model that represents a collection of items as well as an ID to pass the selected item back to the server.


public class DropdownModel
{
    public int ItemID { get; set; }
    public List<DropdownItem> DropdownItems { get; set; }
}

We pass the model down to the view as follows.


public ActionResult Index()
{
    var dropdownItems = new List<DropdownItem>();

   dropdownItems.Add(new DropdownItem { ItemID = 1, ItemName = "Item 1" });
   dropdownItems.Add(new DropdownItem { ItemID = 2, ItemName = "Item 2" });
   dropdownItems.Add(new DropdownItem { ItemID = 3, ItemName = "Item 3" });

    return View(new DropdownModel { DropdownItems = dropdownItems });
}

In our view, we can easily bind the model collection to the dropdown using the @Html.DropdownListFor helper.


@Html.DropDownListFor(m => m.ItemID, new SelectList(Model.DropdownItems, "ItemID", "ItemName"))

No big deal right, nothing new here, just a strongly typed model being bound to a dropdown list.

But let's say that we want to have another dropdown list that is filtered by the selection of the first one. Doing that isn't very difficult either. But let's say that we know we will have multiple instances in our application that will have cascading dropdown lists. It would be nice to have a solution that is generic enough to where we can reuse our code.

Now to take that a step further, it would be even better to have a single event handler that was used for all dropdowns. Hmm, let's take a look at how we could accomplish that.

We'll first start off by dressing up our dropdown.


@Html.DropDownListFor(m => m.ItemID, 
                new SelectList(Model.DropdownItems, "ItemID", "Name")), 
                new { 
                        @class = "dropdown-list", 
                        data_filter_url = Url.Content("~/Home/GetNewDropdown/"), 
                        data_update_container = ".dropdown-container", 
                        data_update_id = "#SecondItemID" 
                })

In the definition above you'll notice a few things. The first is the class name "dropdown-list". It doesn't matter what it is called, I just chose to give it that name. However, this will be the class used for wiring up the jQuery event handler.

Next are the data- attributes. When adding them to an HTML helper through the htmlAttributes object, they must be separated with an underscore. However, they will be parsed and spit out with a dash. So data_filter_url will be data-filter-url.

The data-filter-url attribute will be used to indicate where to make the call to get the second dropdown.

The data-update-container will be used to indicate where to place the partial view once it is retrieved.

The data-update-id is used to indicate the name of the new dropdown so you can trigger a change event if needed.

So the idea is, we have a main controller action that fills in our first dropdown. We will have a second controller action that takes an ID from the first dropdown and returns a partial view containing the new dropdown.

So we'll create a new model so that our dropdown has a different ID name as well as a controller action to retrieve the dropdown.


public class SecondDropdownModel
{
    public int SecondItemID { get; set; }
    public List<DropdownItem> DropdownItems { get; set; }
}


public ActionResult GetNewDropdown(int id)
{
    // This would call a function that retrieves your list object
    var dropdownItems = repository.GetMyData(id);

    return PartialView(new SecondDropdownModel { DropdownItems = dropdownItems });
}

Next we'll create our partial view containing the definition for the second dropdown.


@model CascadingDropdownSample.Models.SecondDropdownModel

@Html.DropDownListFor(m => m.SecondItemID, new SelectList(Model.DropdownItems, "ItemID", "ItemName"))

Now we'll add the magic. We need an event handler that the will be used to retrieve the second dropdown when an item in the first is selected.


$('.dropdown-list').on('change', function (e) {
    
    // Retrieve the filter url from the data-filter-url attribute
    var filterUrl = $(this).attr('data-filter-url');

    // Grab the id of the selected item
    var selectedId = $(this).val();

    // Grab the container that should be updated
    var updateContainer = $(this).attr('data-update-container');

    // Grab the id of new dropdown
    var dataUpdateId = $(this).attr('data-update-id');

    var completeUrl = filterUrl + selectedId;

    $.get(completeUrl, function(r) {

        // Load Partial View using the URL from the data-filter-url attribute
        $(updateContainer).html(r);

        // This would be used if you wanted to trigger the new dropdown to filter another
        if (dataUpdateId != null) {
            // Trigger the change event after it is loaded
            $(dataUpdateId).trigger('change');
        }
    });    
});

Now, all you need is a placeholder on the main view where the dropdown should be inserted. Simply create a div with a class name of dropdown-container and you're set.

And that's pretty much it. By using the dropdown to store information about how to retrieve the next dropdown, we can cascade the dropdowns and use the same jQuery event for each one. If you plan on having more than the second level, you'll have to use jQuery's live instead of on for the event declaration. However, in the recent version of jQuery, they have deprecated live. Enjoy!

Comments

Felipe Freitas, Thursday, August 8, 2013 7:29 PM. reply

Thanks, it was pretty easy :D

Stephen, Friday, August 23, 2013 9:13 AM. reply

You say in the last paragraph: "If you plan on having more than the second level, you'll have to use jQuery's live instead of on for the event declaration" Why can't you use "on"?

Jake, Friday, September 13, 2013 3:37 PM. reply

What I should have said was that you would just need to make sure you hook in the event handler.

jhon, Friday, August 23, 2013 12:12 PM. reply

why the var dropdownItems = repository.GetMyData(id); repository turns into red and visual cant find anything?

Jake, Friday, September 13, 2013 3:35 PM. reply

That line is an example of calling a function that returns your data.

John Smith, Monday, September 9, 2013 6:54 AM. reply

Seems like, you've published code with errors. Would it be difficult to check if it works and re-publish it?

Jake, Friday, September 13, 2013 3:34 PM. reply

Hi John, there shouldn't be any errors. I build the sample, ran it, and copied the code. Can you give me an example of where an error exists?

Frank, Tuesday, September 17, 2013 3:15 AM. reply

Really helpful and very simple solution. Thanks very much!

Craig, Friday, October 4, 2013 7:59 PM. reply

Hey Jake, I am getting an error on this line... @class = "dropdown-list", ...where "class" is a reserved word, and must be wrapped by @() to make it an explicit expression. (Which also doesn't work) Error = Parser Error Message: "class" is a reserved word and cannot be used in implicit expressions. An explicit expression ("@()") must be used. Any Help is appreciated!

David, Friday, January 17, 2014 2:49 PM. reply

reposiry? i not run

Damian, Thursday, October 31, 2013 11:34 PM. reply

Same here. I'm also getting the error "@class = "dropdown-list", ...where "class" is a reserved word,..."

Jake, Friday, November 1, 2013 3:45 PM. reply

My apologies, the issue is the line above. new SelectList(Model.DropdownItems, "ItemID", "Name")) remove the last paren. new SelectList(Model.DropdownItems, "ItemID", "Name") and you should be good to go. Thanks

Rolla, Thursday, November 28, 2013 9:55 AM. reply

For some reason, I can not get the expected results. Is it possible to get the source code?

Ana Pinheiro, Friday, November 29, 2013 5:46 PM. reply

I can obtain the expected results when I run the page for the first time. But if I refreshed the page the program is not able to enter in GetNewDropdown function. So if I insert a new field in the table of DB and then run the page with the dropdown lists, the second one does not include the new field because the program enters only once in the GetNewDropdown function and it is not able to repopulate the second dropdown list.

Ana Pinheiro, Wednesday, December 4, 2013 10:06 AM. reply

I found one possible solution to force the program to enter always in GetNewDropdown function. The question is when we enter once in this function the results the list content is saved in browser cache. So the solution is clean the cache at the beginning of this function: [OutputCache(Duration=1, VaryByParam = "id")] public ActionResult GetNewDropdown(int id) { ... }

Jake, Thursday, December 5, 2013 4:33 PM. reply

Hi Ana, the issue you are seeing is specific to Internet Explorer. By default, IE will cache the content. So you're solution is correct for IE.

Amittava, Tuesday, December 10, 2013 10:26 AM. reply

to which class the object repository belong . Please provide the source code

crazyhorse, Wednesday, December 11, 2013 9:46 AM. reply

Thanks very much man. Your solution is pretty much clear and simple and is working for me. All the best in future work! Great job!

muxa, Saturday, August 2, 2014 7:59 PM. reply

Thanks for great article. About the multiple cascading menus: Instead of using .live , which is deprecated, that worked for me with .on ========= $(document).on('change', '.dropdown-list', {}, function (e) { } ================= Source: http://blog.revathskumar.com/2013/10/jquery-on-avoid-losing-event-binding-for-ajaxed-contents.html

it-it, Sunday, August 24, 2014 9:46 PM. reply

Any article that does not provide source code download link as verification that things DO work usually means that it will not work.

John G, Monday, August 25, 2014 3:23 AM. reply

Anyone who can't follow directions and needs source code for something as simple as this is clearly a novice!

Gwasshoppa, Wednesday, June 17, 2015 10:21 PM. reply

Don't knock being a novice... we all had to start somewhere.

Gwasshoppa, Wednesday, June 17, 2015 10:21 PM. reply

Don't knock being a novice... we all had to start somewhere.

Gwasshoppa, Wednesday, June 17, 2015 10:22 PM. reply

Don't knock being a novice... we all had to start somewhere.

Add Comment

Captcha