Since what you really wanting to do here is avoid making ajax calls to populate the 2nd dropdownlist based on on the first, then neither an EditorTemplate
(where you would have to generate all the html for the <select>
and <option>
tags manually), or using a HtmlHelper
extension method are particularly good solutions because of the enormous amount of code you would have to write to simulate the what the DropDownListFor()
method is doing internally to ensure correct 2-way model binding, generating the correct data-val-*
attributes for client side validation etc.
Instead, you can just pass a collection of all StateProvince
to the view using your view model (your editing data, so always use a view model), convert it to a javascript array of objects, and then in the .change()
event of the first dropdownlist, filter the results based on the selected option and use the result to generate the options in the 2nd dropdownlist.
Your view models would look like
public class AddressVM
{
public int? Id { get; set; }
[Display(Name = "Country")]
[Required(ErrorMessage = "Please select a country")]
public int? SelectedCountry { get; set; }
[Display(Name = "State Province")]
[Required(ErrorMessage = "Please select a state province")]
public int? SelectedStateProvince { get; set; }
public IEnumerable<SelectListItem> CountryList { get; set; }
public IEnumerable<SelectListItem> StateProvinceList { get; set; }
public IEnumerable<StateProvinceVM> AllStateProvinces { get; set; }
}
public class StateProvinceVM
{
public int Id { get; set; }
public string Name { get; set; }
public int Country { get; set; }
}
The view would then be
@using (Html.BeginForm())
{
@Html.LabelFor(m => m.SelectedCountry)
@Html.DropDownListFor(m => m.SelectedCountry,Model.CountryList, "Please select", new { ... })
@Html.ValidationMessageFor(m => m.SelectedCountry)
@Html.LabelFor(m => m.SelectedStateProvince)
@Html.DropDownListFor(m => m.SelectedStateProvince,Model.StateProvinceList, "Please select", new { ... })
@Html.ValidationMessageFor(m => m.SelectedStateProvince)
....
}
and the script
// convert collection to javascript array
var allStateProvinces = @Html.Raw(Json.Encode(Model.AllStateProvinces))
var statesProvinces = $('#SelectedStateProvince');
$('#SelectedCountry').change(function() {
var selectedCountry = $(this).val();
// get the state provinces matching the selected country
var options = allStateProvinces.filter(function(item) {
return item.Country == selectedCountry;
});
// clear existing options and add label option
statesProvinces.empty();
statesProvinces.append($('<option></option>').val('').text('Please select'));
// add options based on selected country
$.each(options, function(index, item) {
statesProvinces.append($('<option></option>').val(item.Id).text(item.Name));
});
});
Finally, in the controller you need to populate the SelectLists and allow for returing the view when ModelState
is invalid, or for when your editong existing data (in both cases, both SelectLists need to be populated). To avoid repeating code, create a private helper method
private void ConfigureViewModel(AddressVM model)
{
IEnumerable<Country> countries = db.Countries;
IEnumerable<StateProvince> stateProvinces = db.StateProvinces;
model.AllStateProvinces = stateProvinces.Select(x => new StateProvinceVM
{
Id = x.Id,
Name = x.Name,
Country = x.CountryId
});
model.CountryList = new countries.Select(x => new SelectListItem
{
Value = x.Id.ToString(),
Text = x.Name
});
if (model.SelectedCountry.HasValue)
{
model.StateProvinceList = stateProvinces.Where(x => x.CountryId == model.SelectedCountry.Value).Select(x => new SelectListItem
{
Value = x.Id.ToString(),
Text = x.Name
});
}
else
{
model.StateProvinceList = new SelectList(Enumerable.Empty<SelectListItem>());
}
}
and then the controller methods will be (for a Create()
method)
public ActionResult Create()
{
AddressVM model = new AddressVM();
ConfigureViewModel(model);
return View(model);
}
[HttpPost]
public ActionResult Create(AddressVM model)
{
if (!ModelState.IsValid)
{
ConfigureViewModel(model);
return View(model);
}
.... // initailize an Address data model, map its properties from the view model
.... // save and redirect
}