Monday, December 03, 2012

MVC Cascaded Drop Down List


MVC have helpers for dropdownlist and they work great, and you can build the template for them too.
But for cascaded drop downlist its different because I think, there is a lot of ways to implement it.
So I will try to build cascade dropdownlist template that can be used more than once in a form with an AutoPostBack ability too.

First of all we need model for the dropdownlist and a model for Cascade dropdownlist as below:

   1:      public class DropDownListViewModel
   2:      {
   3:          public string Value { get; set; }
   4:          public bool AutoPostBack { get; set; }
   5:          public IEnumerable<SelectListItem> Items { get; set; }
   6:      }
So value is for the selected value for the drop dow, autopostback is a flag to indicate autopostback to the form.

And for the cascaddropdown model as below:

   1:    public class CascadDropDownListViewModel
   2:      {
   3:          public string Value { get; set; }
   4:          public bool AutoPostBack { get; set; }
   5:          public string ParentDropDownName { get; set; }
   6:          public string Action { get; set; }
   7:          public IEnumerable Items { get; set; }
   8:      }
The parentdropdownname, is the name of the parent control so that it can update when it changes, the action property is the name of the action used to fill the cascade dropdownlist, it will be a json action
Lets add two editor templates that will work with these models
For dropdownlist

   1:  @if (Model.AutoPostBack)
   2:  {
   3:      @Html.DropDownList("Value",Model.Items, new { onchange="this.form.submit();" })
   4:  }
   5:  else
   6:  {
   7:      @Html.DropDownList("Value",Model.Items)
   8:  }
For the cascade dropdownlist

   1:  @model WebAdvEx.Portal.Models.CascadDropDownListViewModel
   2:   
   3:  @if (Model.AutoPostBack)
   4:  {
   5:      @Html.DropDownList("Value",Model.Items, new { action=Model.Action, cascad="yes", parent=Model.ParentDropDownName+"_Value" ,onchange="this.form.submit();" })
   6:  }
   7:  else
   8:  {
   9:      @Html.DropDownList("Value", Model.Items, new { action=Model.Action, cascad="yes", parent=Model.ParentDropDownName+"_Value" })
  10:  }
We need some Javascript to let the controls be alive,

   1:  <script type="text/javascript">
   2:          $(document).ready(function () {
   3:              $('select[cascad=yes]').each(function () {
   4:                  var action = $(this).attr('action');
   5:                  var drop = $(this);
   6:                  var parent = '#' + $(this).attr('parent');
   7:   
   8:                  $(parent).change(function () {
   9:                          $.getJSON(action, { id: $(parent).val() },
  10:                              function (items) {
  11:                               
  12:                                  drop.empty();
  13:                                  $.each(items, function (index, item) {
  14:                                      drop.append($('<option/>', { value: item.Value, text: item.Text }));
  15:                                  });
  16:                              });
  17:                  });
  18:              });
  19:          });
  20:  </script>
So in summary, looking for any cascade, getting the parent for the control, assigning a new change event, calling the action to get the data in Jason and update the cascade control
A sample of the control code and the controller view
In my controller I have a model with dropdownlist and cascade dropdownlist and im filling them as below:

   1:    model.Country = new DropDownListViewModel() { 
   2:                  Value = model.CountryId.ToString(), 
   3:                  Items = model.GetCountryList(),
   4:               AutoPostBack=false};
   5:   
   6:              model.City = new CascadDropDownListViewModel()
   7:              {
   8:                  Value = model.CityId.ToString(),
   9:                  Items = model.GetCityList(model.CountryId),
  10:                  AutoPostBack=false,
  11:                  ParentDropDownName="Country",
  12:                  Action = Url.Action("UpdateChildList")
  13:   
  14:              };
  15:   
  16:              return View(model);

the function used to fill the cascade after the parent is changed is as below:

   1:   public JsonResult UpdateChildList(int id)
   2:          {
   3:              var indexViewModel = new IndexViewModel();
   4:              return Json(indexViewModel.GetCityList(id),JsonRequestBehavior.AllowGet);
   5:          }
Im using sample data, so indexviewmodel is only a temp thing for testing and bloging, normally we build in the correct location, the idea is to return some sample data as below:

   1:   public IEnumerable<SelectListItem> GetCityList(int CountryId)
   2:          {
   3:              var result = new List<SelectListItem>();
   4:              switch (CountryId)
   5:              {
   6:                  case 1:
   7:                      result.Add(new SelectListItem() { Value = "1", Text = "Abu Dhabi" });
   8:                      result.Add(new SelectListItem() { Value = "2", Text = "Dubai" });
   9:                      result.Add(new SelectListItem() { Value = "3", Text = "Sharja" });
  10:                      break;
  11:                  case 2:
  12:                      result.Add(new SelectListItem() { Value = "4", Text = "USA City 1" });
  13:                      result.Add(new SelectListItem() { Value = "5", Text = "USA City 2" });
  14:                      result.Add(new SelectListItem() { Value = "6", Text = "USA City 3" });
  15:                      break;
  16:                  default:
  17:                      result.Add(new SelectListItem() { Value = "7", Text = "Germany City 1" });
  18:                      result.Add(new SelectListItem() { Value = "8", Text = "Germany City 2" });
  19:                      result.Add(new SelectListItem() { Value = "9", Text = "Germany City 3" });
  20:                      break;
  21:              }
  22:   
  23:   
  24:              return result;
  25:          }

Mission done, cascade dropdownlist are linked and working fine.