ajaxasp.net-mvcmodel-bindingeditorfor

How do I correctly use EditorFor to display checkbox selections from an AJAX call, and POST the selections?


My question is two-fold.

I have a View that gets data on the change of a drop down selection.

The data retrieved is a List property of a ViewModel class using an Ajax call.

This data is shown as a selection of check boxes for the user to select any number of them.

If I return a Partial View from an AJAX call, this is easy enough, but from what I have experienced, this doesn't work for POST'ing back to the controller. Nothing is bound correctly.

From what I have read the correct way is to use EditorFor, So firstly, I cannot figure out how to populate the EditorFor from the AJAX call.

Secondly, If I test by sending initial data from my GET, the EditorFor displays the correct checkbox options, but when I POST, the count of the items is 0.

View:

@model EngineeringAssistantMVC.Controllers.FirmwareController.FirmwareViewModel

@using (Html.BeginForm("Upload", "Firmware", FormMethod.Post, new { @id = "uploadFirmwareForm", @class = "form-horizontal" }))
{

    <!-- Device -->
    <div class="form-group">
        <div class="col-lg-1">
            @Html.LabelFor(x => x.Device, htmlAttributes: new { @class = "control-label" })
         </div>

         <div class="col-lg-2">
             @Html.DropDownListFor(x => x.Device, ViewBag.Devices as IEnumerable<SelectListItem>, new { @class = "form-control", @id = "Devices" })
         </div>

         <div class="col-lg-9">
             @Html.ValidationMessageFor(x => x.Device, "", new { @class = "text-danger" })
         </div>
     </div>

    @Html.EditorFor(x => x.SelectedModels, "SelectedModels", new { @id = "Models" })
    @Html.HiddenFor(x => x.SelectedModels)

}

And the AJAX call:

function GetModels() {
    $.ajax({
        type: "GET",
        url: '@Url.Action("GetModels", "Firmware", null)',
        data: { SelectedDevice: $('#Devices').val() },
        success: function (dataSet) {

            //$('#Models').html(dataSet);
            //$('#Models').data(dataSet);
            //$('#Models').val(dataSet);                

            // How do I populate the EditorFor from the dataSet returned?
        },
        error: function (err) {
            console.log("ERROR: " + err.responseText);
        },
    })
}

SelectedModels EditFor Template:

@model IEnumerable<EngineeringAssistantMVC.ViewModels.ModelViewModel>
@foreach (var item in Model)
{
    @Html.CheckBoxFor(x => item.IsSelected)
    @Html.Label(item.Description)

    @Html.HiddenFor(x => item.ModelId)
    @Html.HiddenFor(x => item.IsSelected)
    @Html.HiddenFor(x => item.Description)
}

Controller:

[HttpPost]
public ActionResult Upload(HttpPostedFileBase uploadFile, FirmwareViewModel firmwareViewModel)
{
    // firmwareViewModel.SelectedModels count is 0 here
}

ModelFirmware Class:

public class ModelFirmware
{
    public int ModelFirmwareId { get; set; }
    public int FirmwareId { get; set; }
    public int ModelId { get; set; }
} 

FirmwareViewModel:

public class FirmwareViewModel
{
    public int FirmwareViewModelId { get; set; }

    [Required]
    public string Device { get; set; }
    public ICollection<ModelViewModel> SelectedModels { get; set; }
}

I just can't get it to work correctly.

EDIT 1: - Add method that returns the models

[HttpGet]
    public ActionResult GetModels(string SelectedDevice)
    {
        var deviceAbbreviation = _dbContext.Radios.Where(x => x.RadioName == SelectedDevice).Select(x => x.ProjectAbbreviation).FirstOrDefault();
        var models = _dbContext.AnatomyModels.Where(x => x.SerialPrefix.StartsWith(deviceAbbreviation.Trim()) && x.ParentId == 0).ToList();

    List<ModelViewModel> mvms = models.Select(x => new ModelViewModel()
    {
        ModelId = x.AnatomyModelId,
        Description = x.SerialPrefix,
        IsSelected = false,
    }).ToList();

    return Json(mvms);
}

Solution

  • There are numerous issues with your code.

    First your not using the EditorTemplate correctly. Change its name to ModelViewModel.cshtml to match the name of the class, and locate it in the /Views/Shared/EditorTemplates (or /Views/YourControllerName/EditorTemplates) folder. The template is then based on a single object (note also the LabelFor() required to create a label associated with the checkbox, and you need to delete the hidden input for IsSelected)

    @model ModelViewModel
    
    @Html.CheckBoxFor(m => m.IsSelected)
    @Html.LabelFor(m => m.IsSelected, Model.Description)
    @Html.HiddenFor(m => m.ModelId)
    @Html.HiddenFor(m => m.Description)
    

    Refer also Post an HTML Table to ADO.NET DataTable to understand why your foreach loop would never have created the correct name attributes for model binding.

    Then in the main view use

    <div id="container">
        @Html.EditorFor(m => m.SelectedModels)
    </div>
    

    and remove the hidden input for SelectedModels (but before you do, inspect the html for that element to understand why its value would never bind). The EditorFor() method will correctly generate the html for each item in your collection.

    Next, change your GetModels() method to return a partial view based on FirmwareViewModel since that is what you will be posting back. Note that you could return a JsonResult, but that would mean generating a whole lot of html in the ajax call back that would not be strongly typed.

    [HttpGet]
    public PartialViewResult GetModels(string SelectedDevice)
    {
        var deviceAbbreviation = _dbContext.Radios.Where(x => x.RadioName == SelectedDevice).Select(x => x.ProjectAbbreviation).FirstOrDefault();
        var models = _dbContext.AnatomyModels.Where(x => x.SerialPrefix.StartsWith(deviceAbbreviation.Trim()) && x.ParentId == 0).ToList();
        List<ModelViewModel> mvms = models.Select(x => new ModelViewModel()
        {
            ModelId = x.AnatomyModelId,
            Description = x.SerialPrefix,
            IsSelected = false, // not really necessary since its the default
        }).ToList();
        FirmwareViewModel model = new FirmwareViewModel
        {
            SelectedModels = mvms
        };
        return PartialView(model);
    }
    

    and your GetModels.cshtml view will be

    @model FirmwareViewModel
    @Html.EditorFor(m => m.SelectedModels)
    

    Then, modify your ajax call to add the partial view in the success callback

    $.ajax({
        type: "GET",
        url: '@Url.Action("GetModels", "Firmware")', // do not need to add 3rd parameter
        data: { SelectedDevice: $('#Devices').val() },
        success: function (response) {
            $('#container').html(response);
        },
        error: function (err) {
            console.log("ERROR: " + err.responseText);
        },
    })
    

    The .html() function will replace any elements already existing in the <div id="container"> element

    Finally, since your using a view model, make use of it and do not use ViewBag. Your view model should contain a IEnumerable<SelectListItem> Devices property which you populate in the GET method (and use @Html.DropDownListFor(x => x.Device, Model.Devices, new { @class = "form-control" }) in the view (note also that the method generates id="Device"). It should also contain a HttpPostedFileBase property to avoid the additional parameter in the POST method, and allow you to add validation attributes.