1

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);
}
Neill
  • 711
  • 2
  • 13
  • 32
  • _this doesn't work for POST'ing back to the controller_ - that is because your code is wrong - but you have not shown that, or your ajax call or your `GetModels()` method, or what that returns, or how its being added to the DOM. –  Feb 14 '18 at 22:26
  • And what little you have shown is incorrect. The `EditorTempate` needs to be named `ModelViewModel.cshtml` (the same as the model, and located in the `/Views/Shared/EditorTemplates` folder, and in the view its just `@Html.EditorFor(x => x.SelectedModels)`. And you need to remove the `@Html.HiddenFor(x => x.SelectedModels)` –  Feb 14 '18 at 22:28
  • And then the code in the `EditorTemplate` is `@model ModelViewModel` without the loop (the `EditorFor()` method correctly generates the html for each item in the collection, including the correct name attributes) –  Feb 14 '18 at 22:31
  • And since your using a view model. get rid of `ViewBag` and add a `IEnumerable` property for the dropdownlist, and a `HttpPostedFileBase` property for the uploaded file so that you can strongly bind to your model. –  Feb 14 '18 at 22:33
  • I have added the GetModel code. The ajax call code is shown above. That's one of my problems, I can't figure out how to add it to the DOM from the returned AJAX call. Will look at the other advice given so long. Thanks, – Neill Feb 15 '18 at 06:12
  • Why are your returning `json`? You should be returning a partial view based on `@model FirmwareViewModel` (but will only contain `EditorFor(m => m.SelectedModels)` and then you add the partial view in the ajax success callback - e.g. `$(someElement).html(dataSet);` –  Feb 15 '18 at 06:15
  • And you will also need to re-parse the validator if you want client side validation (refer [this answer](https://stackoverflow.com/questions/31768946/required-field-validations-not-working-in-jquery-popup-mvc-4/31769058#31769058) –  Feb 15 '18 at 06:17
  • I am returning json because I tried returning a partial view which I couldn't get working and after much reading on Stack Overflow I thought I came to the conclusion that it couldn't be done that way? I guess that was an incorrect conclusion? – Neill Feb 15 '18 at 06:24
  • Give me 15 min and I'll add an answer explaining your issues and how to do this. –  Feb 15 '18 at 06:24

1 Answers1

1

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.