Search code examples
asp.net-mvcasp.net-mvc-5html-helpermvc-editor-templates

Mvc EditorTemplate vs HtmlHelper for Adding Data Attributes


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>

Solution

  • 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
    }