Creating cascading dropdownlists using MVC 4 And JQuery

3 min read

I recently read a blog post by Bipin Joshi on Creating Cascading DropDownLists Using ASP.NET MVC 4 And JQuery and it struck me that I would have coded this quite differently to that proposed by Bipin. That's not to say Bipin is wrong, it is simply my preferred solution to the problem, so I thought I'd share

####See the live demo: /Demo/Jquery/cascading-dropdown-lists

Most of the changes are fairly minor but and some purely for readability, nevertheless, I thought it would be useful to create a post about how I would accomplish this same task.

First off, I created a basic MVC4 web application which gave me some default files out of the box.

In the home controller I have decided to use the collection initializer to build the list of countries but more importantly I felt the code was cleaner by using the ViewBag dynamic over ViewData.

    public ActionResult Index()
    {
        var countries = new List<string> {"USA", "UK", "India"};
        var countryOptions = new SelectList(countries);
        ViewBag.Countries = countryOptions;
        return View();
    }

Next is the GetStates() action method. Here I have made one change that enables me to retrieve the states over a HttpGet request. The reason for this is I believe that HttpGet is the best fit for this request as we are simply retrieving information form the server. If we were adding or updating states then a HttpPost request would be required.

    public JsonResult GetStates(string country)
    {
        var states = new List<string>();
        switch (country)
        {
            case "USA":
                states.Add("California");
                states.Add("Florida");
                states.Add("Ohio");
                break;
            case "UK":
                states.Add("London");
                states.Add("Essex");
                break;
            case "India":
                states.Add("Goa");
                states.Add("Punjab");
                break;
        }
        
        //Add JsonRequest behavior to allow retrieving states over http get
        return Json(states, JsonRequestBehavior.AllowGet);
    }

The second, and final, part of my solution is the Index.cshtml file. In this file I have the html for the form as well as the javascript required to retrieve the states from the server.

First lets look at the entire Index.cshtml file (displayed in the next 2 code blocks):

@using (Html.BeginForm())
{
    <div>Select country:</div>
    <div>@Html.DropDownList("country", 
                            ViewBag.Countries as SelectList, 
                            "Please select", 
                            new { id = "country" })
    </div>
    <div>Select state:</div>
    <div>
        <select id="state" disabled="disabled"></select>
    </div>
    <input type="submit" value="Submit"/>
}


@section scripts
{
    <script type="text/javascript">
        $(function() {
            $('#country').on('change', function() {
                var stateDropdown = $('#state');
                //disable state drop down
                stateDropdown.prop('disabled', 'disabled');
                //clear drop down of old states
                stateDropdown.empty();

                //retrieve selected country
                var country = $(this).val();
                if (country.length > 0) {
                    // retrieve data using a Url.Action() to construct url
                    $.getJSON('@Url.Action("GetStates")', {
                        country: country
                    })
                    .done(function (data) {
                        //re-enable state drop down
                        stateDropdown.removeProp('disabled');
                        //for each returned state
                        $.each(data, function (i, state) {
                            //Create new option
                            var option = $('<option />').html(state);
                            //append state states drop down
                            stateDropdown.append(option);
                        });
                    })
                    .fail(function (jqxhr, textStatus, error) {
                        var err = textStatus + ", " + error;
                        console.log("Request Failed: " + err);
                    });
                }
            });
        })
    </script>
}

Unlike Bipin, I have decided to set the states drop down list as disabled by default and also decided to use an overload of @Html.DropDownList() extension method to set my default option as well as the id for the select list.

Below the html (in the same file) I have utilised the @section keyword which enables me to define javascript which will automatically get rendered at the bottom of the <body> tag thanks to the _Layout.cshtml page.

The first thing my javascript does is to disable the states drop down regardless as soon as the value changes.

var stateDropdown = $('#state');
//disable state drop down
stateDropdown.prop('disabled', 'disabled');
//clear drop down of old states
stateDropdown.empty();

The main difference in my approach, however, is that I have decided to use jQuery's $.getJSON() method over $.ajax() as, in my opinion, it allows for more readable code and strips out some of the boilerplate code. It also negates the need to build an ajax options object.

$.getJSON('@Url.Action("GetStates")', {
    country: country
})

In order to get the url, I decided to use the @Url.Action() method in case of future routing or controller/action changes.

The second big change is that instead of setting success and error actions I have decided to use callback chaining in the form of .done() and .error(). Again this is personal preference and largely down to readability.

.done(function (data) {
    //re-enable state drop down
    stateDropdown.removeProp('disabled');
    //for each returned state
    $.each(data, function (i, state) {
        //Create new option
        var option = $('<option />').html(state);
        //append state states drop down
        stateDropdown.append(option);
    });
})
.fail(function (jqxhr, textStatus, error) {
    var err = textStatus + ", " + error;
    console.log("Request Failed: " + err);
});

As an additional, note. If the project catered for it I would probably implement the GetStates() method as a WebApi method. This would easily be done by moving the method into a new controller that inherited from ApiController. You would simply have to update the url of our $.getJSON call to the web api method.

####See the live demo: /Demo/Jquery/cascading-dropdown-lists

Well, that's it. I hope you enjoy my take on this fairly common task. Please feel free to comment or tweet me, it'd be great to hear your thoughts on the different approaches.

Here are some links you might find useful that relate to this post:


Comments
dj says:January 30, 2014

hiii,this code not working.....

dj says:January 30, 2014

plzzzz,suggest me any simple code...

dj says:January 30, 2014

using entity

John says:January 30, 2014

Hi DJ,

I have used the post above frequently (including in the live demo) so I am confident that it works. Without further information I am unable to suggest a fix. Please consider posting a complete question to stackoverflow.com and adding a link to the comments and I will be sure to take a look and hopefully help you out.

sddsf says:July 10, 2014

I'm new for mvc.. This code is not working i.e when select country the corresponding of states are not bind for state dropdown

Christian says:August 28, 2014

Hi, the code works fine, i guess its just a little mistake there. Where you have "var option = $('>option /<').html(state);" change it to "var option = $('<option />').html(state);". that's what i did and it worked for me. thanks to John

John says:September 11, 2014

Hi Christian,

Exactly right. That was an encoding typo on my part. I will update as soon as I am back at my laptop. Thanks for helping out

Andy says:March 14, 2016

This is a great post thank you. Works well for me. But now I need to pass two values to the states dropdown... State and ID. How? This is what I'm attempting in the controller...

        var bastyps = from bstps in db.BASES_TYPES
                      where bstps.BASE_ID == country
                      select new{bstps.BASE_TYPE,bstps.ID};

        List&lt;SelectListItem&gt; states = new List&lt;SelectListItem&gt;();
        foreach (var s in bastyps)
        {
            states.Add(new SelectListItem { Text = &quot;dynamic_state&quot;, Value = &quot;dynamic_id&quot; });
        } 

Obviously I'm tying the second dropdown to a database. And I'm stuck on two things... getting the dynamic db values to display instead of what's in the quotes, and getting the view to accept both values. Please help!

Andy says:March 14, 2016

Never mind got it figured out! Let me know if you want to see the code for both text and id getting passed to the view.