1

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?

  • If so, why should I go with a Helper vs. a Template?
  • if not, what are any new ways to handle this? Bonus for providing code samples to compare with what I end up doing.

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>
RoLYroLLs
  • 3,113
  • 4
  • 38
  • 57
  • Its not clear why you think you would need to do this (and `DataAttributes` are unlikely to help). What do you mean _through javascript, I can repopulate the select based on the data values_? –  Feb 20 '18 at 05:32
  • Sorry, you're right. I updated the question, adding: *through javascript, I can repopulate the select for the `StateProvince` by filtering what `StateProvince` belongs to the selected `Country` based on the data values.* – RoLYroLLs Feb 20 '18 at 05:38
  • Then I consider your taking the wrong approach. I assume you trying to avoid making an ajax call to populate the 2nd dropdownlist based on the value of the first? –  Feb 20 '18 at 05:39
  • Yes, I do not want to use `ajax`. also I just added desired results – RoLYroLLs Feb 20 '18 at 05:40
  • OK (As I suspected). The explanation for why you should not be using either a template or a `HtmlHelper` extension method is complex (I might add some more detail about that in our chat later). I'll add an answer explaining how best to do it in in 30 min or so. –  Feb 20 '18 at 05:44

1 Answers1

2

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
}
  • Wow! This works perfectly! But it now leads into further questions, which I selectively left out of the question as to not complicate it, but I guess now requires attention. At the UI I intend the `StateProvince` dropdown to be hidden and show a free text field for countries where we do not a definition of the list of states or provinces. I can hard code that into the `javascript` but I was hoping to be able to have the script be more dynamic and hide.show elements based on `DataAttributes` or maybe some other way. I'll add it to the question as well. – RoLYroLLs Feb 20 '18 at 16:19
  • @RoLYroLLs, That is a different question, and I will not update this answer to cover that as well - you will turn this into a book chapter :). Ask a new question about your new requirements. –  Feb 20 '18 at 20:44