1

I have the current ViewModel:

public class ServiceSheetViewModel
{
    public List<ServiceSheetQuestionViewModel> questions { get; set; }
    public List<VehicleAction> actions { get; set; }
}

And, on the view, I'm generating the following:

@Html.EditorFor(modelItem => Model.questions[j], "ServiceSheetQuestionViewModel");
@Html.EditorFor(m => Model.actions[i], "VehicleAction");

On the controller:

[HttpPost]
public ActionResult CreateServiceSheet([Bind(Include = "questions,actions")]  ServiceSheetViewModel model, int vehicleId, int serviceSheetId)

When I submit the form, it is not binding.

On the post information, its saying its sending a List of ServiceSheetQuestionViewModel and a List of VehicleActions.

How is the proper way to make the form bind correctly?

EDIT 1 - View Code

@model  SupervisedSolutions.Models.ViewModels.ServiceSheetViewModel
....
@using (Html.BeginForm("CreateServiceSheet", "PerformService", Model))
{
    ....
    @foreach (var rootItem in Model.questions.Where(x => x.Question.IsGroup || (!x.Question.IsGroup && (x.Question.ParentQuestion == null || x.Question.ParentQuestion.ID == 0))).ToList())
    {
        var i = Model.questions.IndexOf(rootItem);
        if (Model.questions[i].Question.IsGroup)
        {
            @Html.HiddenFor(m => Model.questions[i].Question.ID)
            @Html.HiddenFor(m => Model.questions[i].Question.Title)
            @Html.HiddenFor(m => Model.questions[i].Question.Description)
            @Html.HiddenFor(m => Model.questions[i].Question.IsGroup)
            @Html.HiddenFor(m => Model.questions[i].Question.OrderIndex)
            @Html.HiddenFor(m => Model.questions[i].Question.ParentQuestion.ID)
            @Html.HiddenFor(m => Model.questions[i].Question.ParentQuestion.setDeleted)
            @Html.HiddenFor(m => Model.questions[i].Question.ParentQuestion.Title)
            @Html.HiddenFor(m => Model.questions[i].Question.ParentQuestion.Created)
            <h1>@Model.questions[i].Question.Title</h1>
            <p>@Model.questions[i].Question.Description</p>
            ....
            @foreach (var item in Model.questions.Where(x => !x.Question.IsGroup && (x.Question.ParentQuestion != null && x.Question.ParentQuestion.ID == Model.questions[i].Question.ID)).ToList())
            {
                var j = Model.questions.IndexOf(item);
                @Html.EditorFor(modelItem => Model.questions[j], "ServiceSheetQuestionViewModel");
            }
        }
        else
        {
            @Html.EditorFor(modelItem => Model.questions[i], "ServiceSheetQuestionViewModel");
        }
    }
    @foreach (var todo in Model.actions)
    {
        var i = Model.actions.IndexOf(todo);
        @Html.EditorFor(m => Model.actions[i], "VehicleAction");
    }
    <input type="submit" value="Start Run" class="btn btn-info" />
}

This is the EditorTemplate for ServiceSheetQuestionsViewModel:

@model SupervisedSolutions.Models.ViewModels.ServiceSheetQuestionViewModel
....
@Html.HiddenFor(m => m.Question.ID)
@Html.HiddenFor(m => m.Question.Title)
@Html.HiddenFor(m => m.Question.Description)
@Html.HiddenFor(m => m.Question.IsGroup)
@Html.HiddenFor(m => m.Question.OrderIndex)
@Html.HiddenFor(m => m.Question.ParentQuestion.ID)
@Html.HiddenFor(m => m.Question.ParentQuestion.setDeleted)
@Html.HiddenFor(m => m.Question.ParentQuestion.Title)
@Html.HiddenFor(m => m.Question.ParentQuestion.Created)
<b>@Html.DisplayFor(m => m.Question.Title)</b>
<label>
    @Html.RadioButtonFor(m => m.isFault, "Fault")Fault
</label>
<label>
    @Html.RadioButtonFor(m => m.isFault, "NoFault")No Fault
</label>
@Html.LabelFor(model => model.FaultDescription)
@Html.TextAreaFor(model => model.FaultDescription)
@Html.ValidationMessageFor(model => model.FaultDescription)
@Html.ValidationMessageFor(m => m.isFault)

And this is the EditorTemplate for Vehicle Action:

@model SupervisedSolutions.Models.VehicleAction

@Html.LabelFor(model => model.Description)
@Html.TextAreaFor(model => model.Description)
@Html.ValidationMessageFor(model => model.Description)

@Html.LabelFor(model => model.Priority)
@Html.TextAreaFor(model => model.Priority)
@Html.ValidationMessageFor(model => model.Description)

@Html.LabelFor(m => m.DueDate>
@Html.EditorFor(model => model.DueDate)
@Html.ValidationMessageFor(m => m.DueDate)

EDIT 2 - Here is the information on the post data:

vehicleId:50
serviceSheetId:1
ID:0
questions[1].Question.ID:3
questions[1].Question.Title:Testing
questions[1].Question.Description:AASDASD
questions[1].Question.IsGroup:True
questions[1].Question.OrderIndex:1
questions[1].Question.ParentQuestion.ID:
questions[1].Question.ParentQuestion.setDeleted:
questions[1].Question.ParentQuestion.Title:
questions[1].Question.ParentQuestion.Created:
questions[0].Question.ID:1
questions[0].Question.Title:Test Question
questions[0].Question.Description:Habba
questions[0].Question.IsGroup:False
questions[0].Question.OrderIndex:0
questions[0].Question.ParentQuestion.ID:3
questions[0].Question.ParentQuestion.setDeleted:False
questions[0].Question.ParentQuestion.Title:Testing
questions[0].Question.ParentQuestion.Created:28/11/2016 7:35:12 PM
questions[0].isFault:NoFault
questions[0].FaultDescription:Teste
questions[3].Question.ID:5
questions[3].Question.Title:Group 2
questions[3].Question.Description:Groupin 2
questions[3].Question.IsGroup:True
questions[3].Question.OrderIndex:0
questions[3].Question.ParentQuestion.ID:
questions[3].Question.ParentQuestion.setDeleted:
questions[3].Question.ParentQuestion.Title:
questions[3].Question.ParentQuestion.Created:
questions[2].Question.ID:4
questions[2].Question.Title:Test Question 23
questions[2].Question.Description:TEEEE
questions[2].Question.IsGroup:False
questions[2].Question.OrderIndex:0
questions[2].Question.ParentQuestion.ID:5
questions[2].Question.ParentQuestion.setDeleted:False
questions[2].Question.ParentQuestion.Title:Group 2
questions[2].Question.ParentQuestion.Created:1/12/2016 4:19:49 PM
questions[2].isFault:NoFault
questions[2].FaultDescription:Teste
actions[0].Description:Erro2
actions[0].Priority:1
actions[0].DueDate:2010-01-01
Bruno Xavier
  • 377
  • 1
  • 16
  • Show you view (the loop you generating the form controls in and the EditorTemplates your using for `ServiceSheetQuestionViewModel` and `VehicleAction`) –  Dec 05 '16 at 20:51
  • @StephenMuecke I added the full HTML for the views. – Bruno Xavier Dec 07 '16 at 13:34

1 Answers1

1

First remove your [Bind] attribute (that attribute is not required when using view models).

To start with the easy one, replace the @foreach (var todo in Model.actions){ ... } loop for actions with just

@Html.EditorFor(m => m.actions)

and ensure your EditorTemplate is located in the /Views/Shared/EditorTemplates (or /Views/youControllerName/EditorTemplates) folder, and is named VehicleAction.cshtml. The EditorFor() method accepts IEnumerable<T> and will correctly generate the html for each item in the collection.

The issue with questions not binding is that the DefaultModelBinder (by default) requires that collection indexers start at zero and be consecutive. Your logic in the @foreach (var rootItem in Model.questions.Where..... and @foreach (var item in Model.questions.Where... and in particular the if (Model.questions[i].Question.IsGroup){ ... } else { ... } block means that you are not generating consecutive indexers (your request shows that they are in order [1], [0], [3], [2]).

One way to solve this is to include a hidden input for the collection indexer to override the default behavior of the DefaultModelBinder, but that means you cannot use an EditorTemplate and you will need for loops to generate the controls. Refer the first code snippet in this answer for an example of the necessary hidden input.

However, that kind of logic does not belong in a view, especially since you already using view models. Your ServiceSheetViewModel should have 2 collection properties for ServiceSheetQuestionViewModel, say

public List<ServiceSheetQuestionViewModel> GroupedQuestions { get; set; }
public List<ServiceSheetQuestionViewModel> UngroupedQuestions { get; set; }

and you populate those collections in the controller based on the value of IsGroup. Then in the view you can simply use

@Html.EditorFor(m => m.GroupedQuestions)
@Html.EditorFor(m => m.UngroupedQuestions)

to handle the if/else block that your currently using in the view.

You have not provided enough information to understand what the logic in foreach loops are doing, but it appears you have a hierarchical structure based on your ParentQuestion properties, which means that your view models should also have a hierarchical structure, i.e. a Question needs to contains a List<Question> SubQuestions property so that you can use nested EditorTemplates or for loops.

As a side note, rendering all those hidden inputs is unnecessary and bad practice (a malicious user can easily post back bad data and you just degrading performance). Just generate a hidden input for the questions ID property and when you submit, get the original from the database based on the ID and update only those properties you edit in the view based on the view model.

Community
  • 1
  • 1