So I've searched around and haven't found any "new" answers. I'm not sure if it's because the answers are still correct, or no one has recently asked it.
I have the following classes (condensed for brevity):
public class Address {
public int Id { get; set; }
public int CountryId { get; set; }
public Country Country { get; set; }
public int StateProvinceId { get; set; }
public StateProvince StateProvince { get; set; }
}
public class Country {
public int Id { get; set; }
public string Name { get; set; }
public string Code { get; set; }
}
public class StateProvince {
public int Id { get; set; }
public string Name { get; set; }
public int CountryId { get; set; }
public Country Country { get; set; }
}
What I'm looking for is the simplest, but very customizable way to create an EditorFor
/DropDownList
for the list of Country
. Specifically, I want to add data attributes
to each option
of a select
so that, through javascript, I can repopulate the select
for the StateProvince
by filtering what StateProvince
belongs to the selected Country
based on the data
values.
I've looked through the following (and more, but these are the most notable):
The thing is all these answers look great, but are over 3-4 years old.
Are these methods still valid?
Helper
vs. a Template
?Desired Results
<select id="Country" name="Country">
<option value="1" data-country-code="US">United States</option>
<option value="2" data-country-code="CA">Canada</option>
...
</select>
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
}