MVC - Cascading Lists using Ajax and jQuery



It seems like every project I'm on has a cascading list requirement.  The problem is common enough: once a value in one list is selected, a second sub-list is populated based on selections from the first.  One of the most common scenarios (as seen above) is selecting a Country, which populates a list of States or Provinces (or Cities for my example).

In a recent project, an additional requirement of cascading multi-select lists was needed.  If the user selects multiple values in the first list, the second list should contain all of the sub-selections from all the selections in the first list.

Typically in the past, this has been done via post-back to the server to populate the second list.  This has the disadvantage of refreshing the page, which means the user is stuck waiting.  However, in MVC, it is just as simple to use Ajax and Jquery to seamlessly populate the list without interrupting the user experience.

First, the models.  This example is pretty simple.  We have a Country object, and a City object that contains a reference to the Country.

Code Snippet
  1. public class Country
  2. {
  3.     public int CountryId { getset; }
  4.     public string Name { getset; }
  5. }

Code Snippet
  1. public class City
  2. {
  3.     public int CityId { getset; }
  4.     public int CountryId { getset; }
  5.     public string Name { getset; }
  6. }


Next, we need a place to capture the selected CountryIds and CityIds, so our view model looks like so:


Code Snippet
  1. public class HomeModel
  2. {
  3.     public List<int> SelectedCountryIds { getset; }
  4.     public List<int> SelectedCityIds { getset; }
  5. }


On the controller, a ViewBag is populated with the Countries (generally retrieved from a repository).  This will allow us to bind to that list on the view.  Note: It's not necessary to populate the ViewBag with Cities; we will do that via Ajax as the user makes selections.


Code Snippet
  1. public ActionResult Index()
  2. {
  3.     //Populate a list of Countries.
  4.     ViewBag.Countries = FakeRepository.GetCountries();
  5.  
  6.     return View();
  7. }


On the view, we bind the two lists to the HomeModel.


Code Snippet
  1. @Html.ListBoxFor(x => x.SelectedCountryIds, new MultiSelectList(ViewBag.Countries, "CountryId""Name"))
  2. @Html.ListBoxFor(x => x.SelectedCityIds, new MultiSelectList(new List<City>(), "CountryId""Name"))


Note that our city list is populated with an empty List<City>().

That's the setup.  All that's left is to create a method on the controller to get a city list based on a list of countries (or countryIds in this case), and the jQuery to make the Ajax call.

I use POST to prevent access via URL to the GetCities(), but GET works as well.  Note that we pass the model to the Json() method.  This will translate the model into an object usable by jQuery.


Code Snippet
  1. [HttpPost]
  2. public ActionResult GetCities(List<int> countryIds)
  3. {
  4.     if (countryIds == null)
  5.         return Json(null);
  6.  
  7.     //Select cities from the list matching the selected counties
  8.     List<City> selectedCities =         FakeRepository.GetCitiesByCountryIds(countryIds);
  9.  
  10.     //Return a Json object to the Success fucntion of the Ajax call
  11.     return Json(selectedCities);
  12. }


Finally, we need some jQuery to put it all together.  We'll hook a function to the click() method of the Countries list.  We iterate over all of the selected countries and store the values in an array.  Finally, we send the array to the Controller using jQuery's $.ajax call.  In the success function, the city list is populated with the new options returned from the controller.



Code Snippet
  1. <script type="text/javascript">
  2.     $(function () {
  3.         //Change the City list each time the Country list is selected
  4.         $('#SelectedCountryIds').change(function () {
  5.             var selectedCountryIds = [];
  6.  
  7.             //Iterate over each selection in the Country list and add
  8.             //its CountryId to the array
  9.             $('#SelectedCountryIds option:selected').each(function () {
  10.                 selectedCountryIds.push($(this).val());
  11.             });
  12.  
  13.             //Ajax the CountryId list to the GetCities() method in the HomeController
  14.             $.ajax({
  15.                 url: '@Url.Content("~/Home/GetCities/")',
  16.                 dataType: 'json',
  17.                 type: 'POST',
  18.                 data: JSON.stringify(selectedCountryIds),
  19.                 contentType: 'application/json',
  20.                 success: function (cities) {
  21.                     //Empty the City List of previous values
  22.                     $('#SelectedCityIds').empty();
  23.  
  24.                     //Iterate over each city and append it to the City List
  25.                     $(cities).each(function () {
  26.                         $('#SelectedCityIds').append('<option value="' + this.CityId + '">' + this.Name + '</option>');
  27.                     });
  28.                 }
  29.             });
  30.         });
  31.     });
  32. </script>


Thats it.  Holding down CTRL while clicking the Country list will add those Cities to the City list, all without refreshing the page.

Just one little gotcha, it is important to specify the contentType option as 'application/json' in the Ajax call.  Without it, the controller won't interpret the data as Json, and the contryIds list will be null.

Comments

Popular posts from this blog

SQL Reporting Services - Viewer broken in Non-IE Browsers: FIX

Dynamics AX 2012 - Visual Studio Team Services Custom Work Item Fields Break Version Control (Error TF237121)

Dynamics AX SysFileDeployment Framework - Deploy files automatically (using resources) to the client on login