views:

924

answers:

5

I am making an application that deals with vehicles. I need two DropDownLists:

  • Makes: All Vehicle Makes
  • Models: Models that belong to the selected value of the Make DropDownList

How is this done in MVC2?

My Idea: Do I use an ajax call when my first list is selected and then pull back the Models to bind to the Model DDL? How would model binding come into play that way?

UPDATE I posted what I ended up doing as an answer. It is super simple and works great.

You can use a get too if you feel so inclined, but you have to specify that you want to like so... return Json(citiesList, JsonRequestBehavior.AllowGet);

+4  A: 

Here's a nice way to do it :

Suppose that we have two drop lists, country and city ,, the city drop down is disabled by default and when a country is selected the following happens :

1. city drop down list gets enabled.
2. An AJAX call is made to an action method with the selected country and a list of cities is returned.
3. the city drop down list is populated with the JSON data sent back.

Credits for the original code goes to King Wilder from MVC Central. This example is a simplified version that was extracted from his code in the Golf Tracker Series.

HTML

<select id="Country">
// a List of Countries Options Goes Here.
</select></div>

<select id="City" name="City" disabled="disabled">
// To be populated by an ajax call
</select>

JavaScript

// Change event handler to the first drop down ( Country List )
$("#Country").change(function() {
    var countryVal = $(this).val();
    var citySet = $("#City");

    // Country need to be selected for City to be enabled and populated.
    if (countryVal.length > 0) {
        citySet.attr("disabled", false);
        adjustCityDropDown();
    } else {
        citySet.attr("disabled", true);
        citySet.emptySelect();
    }
});

// Method used to populate the second drop down ( City List )   
function adjustCityDropDown() {
    var countryVal = $("#Country").val();
    var citySet = $("#City");
    if (countryVal.length > 0) {
        // 1. Retrieve Cities that are in country ...
        // 2. OnSelect - enable city drop down list and retrieve data
        $.getJSON("/City/GetCities/" + countryVal ,
        function(data) {
            // loadSelect - see Note 2 bellow
            citySet.loadSelect(data);
        });
    }
}

Action Method

[HttpGet]
public ActionResult GetCities(string country)
{
    Check.Require(!string.IsNullOrEmpty(country), "State is missing");

    var query  = // get the cities for the selected country.

    // Convert the results to a list of JsonSelectObjects to 
    // be used easily later in the loadSelect Javascript method.         
    List<JsonSelectObject> citiesList = new List<JsonSelectObject>();
        foreach (var item in query)
        {
            citiesList.Add(new JsonSelectObject { value = item.ID.ToString(),
                                                  caption = item.CityName });
        }        

    return Json(citiesList, JsonRequestBehavior.AllowGet);
}

Important Notes:

1. The JsonSelectObject help make things easier when converting the results to an option tag as it will be used in the javascript loadSelect method below. it's basically a class with the two properties value and caption :

public class JsonSelectObject
{
    public string value { get; set; }
    public string caption { get; set; }
}

2. The function loadSelect is a helper method that takes a list of json objects originally of typeJsonSelectObject , converts it to a list of options to be injected in calling drop down list. it's a cool trick from the "jQuery In Action" book as referenced in the original code, it's included in a jquery.jqia.selects.js file that you will need to reference. Here's the code in that js file :

(function($) {
    $.fn.emptySelect = function() {
        return this.each(function() {
            if (this.tagName == 'SELECT') this.options.length = 0;
        });
    }

    $.fn.loadSelect = function(optionsDataArray) {
        return this.emptySelect().each(function() {
            if (this.tagName == 'SELECT') {
                var selectElement = this;
                selectElement.add(new Option("[Select]", ""), null);
                $.each(optionsDataArray, function(index, optionData) {
                    var option = new Option(optionData.caption,
                                  optionData.value);
                    if ($.browser.msie) {
                        selectElement.add(option);
                    }
                    else {
                        selectElement.add(option, null);
                    }
                });
            }
        });
    }

})(jQuery);

This method might be complex ,,, but at the end you will have a clean & compact code that you can use everywhere else.

I hope this was helpful ,,,


Update

Using POST instead of GET in the AJAX call

You can replace the $.getJSON call with the following code to make the ajax call using POST instead of GET.

$.post("/City/GetCities/", { country: countryVal }, function(data) {
     citySet.loadSelect(data);
 });

also remember to change your Action method to accept POST requests by changing the [HttpGet] annotation with [HttpPost] and remove the JsonRequestBehavior.AllowGet when returning the result in the Action Method.

Important Note

Note that we are using the value of the selected item rather than the name. for example if the user selected the following option.

<option value="US">United States</option>

then "US" is sent to the Action method and not "United States"

Update 2: Accessing the Selected Values In The Controller

suppose that you have the following two properties in your Vehicle viewmodel:

public string Maker { get; set; }
public string Model { get; set; }

and you name your select elements with the same name as your ViewModel properties.

<select id="Maker" name="Maker">
// a List of Countries Options Goes Here.
</select></div>

<select id="Model" name="Model" disabled="disabled">
// To be populated by an ajax call
</select>

Then the selected values will be automatically Bound to your ViewModel and you can access them directly in you Action Method.

This will work if the page is Strongly Typed to that ViewModel.


Note: For the first list ( The Makes List ) you can create a MakersList of type SelectList in your ViewModel and use the HTML helper to automatically populate your Makers Drop Down with the list in your ViewModel. the code will look something like this :

<%= Html.DropDownListFor(model => model.Maker, Model.MakersList) %>

In that case the generated name for this select will be also "Maker" (the name of the property in the ViewModel).

I hope this is the answer you are looking for.

Manaf Abu.Rous
Whoah, i was just about to post an answer, but it wouldnt have been as comprehensive as this one. Nice work (+1). Also, try to use POST instead of GET for json web services - more secure. But besides that, great answer.
RPM1984
I don't get how model binding works with this. Any help?
Blankasaurus
I'm note sure if i understand your question, could you be more specific about the model that you want to bind to ?
Manaf Abu.Rous
I guess my question is, how do I access the selected value of those dropdownlists in my controller? Is there a way to bind it to my viewmodel?
Blankasaurus
I get your question now, I'll update the post with the answer in a min.
Manaf Abu.Rous
Cool - Thanks. This is really freaking complicated to update a DDL. =/
Blankasaurus
I just found a duplicate of you question http://stackoverflow.com/questions/2174334/populate-a-dropdown-select-based-on-the-value-chosen-on-another-dropdown ,,,, There's a link to a detailed example. You might want to check it if this is too complicated for you :)
Manaf Abu.Rous
This is officially the longest answer I've ever posted :)
Manaf Abu.Rous
Just seems after the amount of planning and careful thought put into MVC2 and .NET updating a DDL wouldn't take 100 lines of code. =D
Blankasaurus
i agree some tasks are more complex now in MVC than WebForms but MVC gives you a better control over your application which i think is more important.
Manaf Abu.Rous
Check the first comment in the duplicate i posted. it looks like this question is a duplicate of a duplicate. and the answer to the later is more compact and clear.
Manaf Abu.Rous
+2  A: 

The simpliest way is to use jQuery "cascade" plugin. http://plugins.jquery.com/project/cascade (look at the demo page there).

If you want to resolve value using ajax, it also helps you and it eliminates a lot of code from the previous answer, so you can concentrate on your logic :)

You can find a lot of examples in google, but you end up with just the following script:

$('#myChildSelect').cascade('#myParentSelect', 
{
    ajax: '/my/url/action',
    template: function(item) {
        return "<option value='" + item.value + "'>" + item.text + "</option>"; },
    match: function(selectedValue) { return this.when == selectedValue; }    
});
Patrol02
Ill look into it. Thanks +1
Blankasaurus
+5  A: 

This is what I ended up doing... Didn't need additional plugins / 1000 Lines of code...

The Html

//The first DDL is being fed from a List in my ViewModel, You can change this...
<%: Html.DropDownList("MakeList", new SelectList(Model.Makes, "ID", "Name")) %>
<select id="ModelID" name="ModelID" disabled="disabled"></select>

The JQuery

    $(document).ready(function () {
        $('#MakeList').change(function () {
            $.ajaxSetup({ cache: false });
            var selectedItem = $(this).val();
            if (selectedItem == "" || selectedItem == 0) {
                //Do nothing or hide...?
            } else {
                $.post('<%: ResolveUrl("~/Sell/GetModelsByMake/")%>' + $("#MakeList > option:selected").attr("value"), function (data) {
                    var items = "";
                    $.each(data, function (i, data) {
                        items += "<option value='" + data.ID + "'>" + data.Name + "</option>";
                    });
                    $("#ModelID").html(items);
                    $("#ModelID").removeAttr('disabled');
                });
            }
        });
    });

The Action Method

    [HttpPost]
    public ActionResult GetModelsByMake(int id)
    {
        Models.TheDataContext db = new Models.TheDataContext();
        List<Models.Model> models = db.Models.Where(p=>p.MakeID == id).ToList();

        return Json(models);
    }
Blankasaurus
Thanks for sharing your solution +1
Manaf Abu.Rous
It would be better thought to move the logic of Ajax call and populating the select to a new method in a separate file. that way it's more reusable.
Manaf Abu.Rous
True - I'll probably do that. Didn't wanna get too extensible until I got it working... ;)
Blankasaurus
A: 

This will come in very handy in the future, but to expand on the question, what if a user has scripts turned off?

How would you deal with this?

Would you have to create a new controller/view that accepted the Make dropdown value, then displayed a new view?

Thanks

Tim B James
You show them a giant message that says "Turn your scripts on or you don't get to use my site." =D You would have to do a Post I imagine and populate the box in that controller.
Blankasaurus
Personally I would show this message or something ruder :) , also if they are using IE6 then I would just display some sort of message. However for work within a company there are many voices and I am sure one will say that we need to cater for these situations.
Tim B James
A: 

Thanks for sharing your code. Just used it as a template and it worked fine. Saved me a lot of time.

Regards, kzmp

kzmp