3

I have a page that contains multiple forms to edit questions for a single quiz, each question has its own list of answers. So for each question inside this quiz there is a form for which a user can edit the question (and answers), See below:

@model OLTINT.Areas.admin.ViewModels.OldQuizQAViewModel
<h1>Edit @Model.QuizTitle quiz</h1>
<hr />
<p class="breadcrumb">
    @Html.ActionLink(HttpUtility.HtmlDecode("&#9668;") + " Back to List", "Quizzes", new { id = Model.CourseID }, new { @class = "" })
</p>
@for (int j = 0; j < Model.OldQuizQuestions.Count(); j++)
{
    using (Ajax.BeginForm("EditQuiz", "Course", null, new AjaxOptions
    {
        HttpMethod = "POST",
        InsertionMode = InsertionMode.Replace,
        UpdateTargetId = "button"
    }))
    {
        @Html.AntiForgeryToken()
        @Html.HiddenFor(model => model.QuizID)
        @Html.HiddenFor(model => model.OldQuizQuestions[j].QuizQuestionID)

        <p class="form_title">Question number @Model.OldQuizQuestions[j].Order</p>
        <div class="resize_input">@Html.EditorFor(model => model.OldQuizQuestions[j].Question)</div>
        <p class="form_title">@Html.LabelFor(model => model.OldQuizQuestions[j].Type)</p>
        <div class="resize_input">@Html.DropDownListFor(model => model.OldQuizQuestions[j].Type, ViewBag.types, "Please choose...", new { @class = "chosen-select" })</div>

        <p class="form_title">Choose correct answers</p>
        Char x = 'a';
        for (int i = 0; i < Model.OldQuizQuestions[j].OldQuizAnswers.Count(); i++)
        {
            x++;
            if (i == 0)
            {
                x = 'a';
            }
            <div style="display:table; width:100%;">
                <div class="divTableCell" style="padding:0 10px 10px 0; vertical-align:middle; min-width:6%;">
                    @Html.CheckBoxFor(model => model.OldQuizQuestions[j].OldQuizAnswers[i].Correct, new { style = "" })
                    @Html.LabelFor(model => model.OldQuizQuestions[j].OldQuizAnswers[i].Correct, "["+ x +"]")
                </div>
                <div class="divTableCell quiz_input">
                    @Html.HiddenFor(model => model.OldQuizQuestions[j].OldQuizAnswers[i].QuizAnsID)
                    @Html.EditorFor(model => model.OldQuizQuestions[j].OldQuizAnswers[i].Answer)
                </div>
            </div>
        }
        <div class="button_container">
            <p id="button"></p>
            @Html.ActionLink("Delete this question", "DeleteQuestion", new { id = Model.OldQuizQuestions[j].QuizQuestionID }, new { @class = "button button_red button_not_full_width" })
            <input type="submit" value="Save" class="button button_orange button_not_full_width" />
        </div>
        <hr />
    }
}

OldQuizQAViewModel:

public class OldQuizQAViewModel
{
    public int CourseID { get; set; }
    public int? QuizID { get; set; }
    public string QuizTitle { get; set; }
    public IList<OldQuizQuestions> OldQuizQuestions { get; set; }
}

OldQuizQuestions:

public class OldQuizQuestions
{
    [Key]
    public int QuizQuestionID { get; set; }
    public int OldQuizID { get; set; }
    [Required]
    public string Question { get; set; }
    [Required]
    public int Order { get; set; }
    [Required]
    public int Type { get; set; }

    public virtual IList<OldQuizAnswers> OldQuizAnswers { get; set; }
    public virtual OldQuiz OldQuiz { get; set; }

}

OldQuizAnswers:

public class OldQuizAnswers
{
    [Key]
    public int QuizAnsID { get; set; }
    public int QuizQuestionID { get; set; }
    public string Answer { get; set; }
    public int Order { get; set; }
    public bool Correct { get; set; }
    public bool Chosen { get; set; }

    public virtual OldQuizQuestions OldQuizQuestions { get; set; }
}

Controller:

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult EditQuiz(OldQuizQAViewModel model)
    {
        var questiondata = model.OldQuizQuestions.Single();

        if (ModelState.IsValid)
        {
            OldQuizQuestions updatequestion = db.OldQuizQuestions
                .SingleOrDefault(x => x.QuizQuestionID == questiondata.QuizQuestionID);

            updatequestion.Question = questiondata.Question;
            updatequestion.Type = questiondata.Type;

            db.Entry(updatequestion).State = EntityState.Modified;
            db.SaveChanges();

            foreach (var answer in questiondata.OldQuizAnswers)
            {
                var updateanswer = updatequestion.OldQuizAnswers
                    .First(x => x.QuizAnsID == answer.QuizAnsID);

                updateanswer.Answer = answer.Answer;
                updateanswer.Correct = answer.Correct;

                db.Entry(updateanswer).State = EntityState.Modified;
                db.SaveChanges();
            }

            return Content("<span style='font-weight:300; font-size:1.2em; color: green; '>Saved!</span>");
        }
        return Content("<span class='errortext'>Please correct the marked fields!</span>");
    }

Now this works fine if I want to edit the first question but when I edit anything else my controller just says null but when I check the data that's being posted everything is there (for example when i try to edit question 2):

enter image description here

I've had a look around on here at the many queries about model binding to a list but none have helped. Can anyone see where i'm going wrong with this?

Tieson T.
  • 20,774
  • 6
  • 77
  • 92
CornJ
  • 97
  • 8
  • What is the point of multiple forms. You can only submit one form at a time. Just have one form (with the loops inside it) and this will all work correctly. By default, the `DefaultModelBinder` will only bind collections with indexers that start and zero and are consecutive, so the only forms which meets that is the first one. –  Apr 27 '17 at 04:01
  • @StephenMuecke The point is I didn't want potentially 10 questions worth of data being submitted each time a user edits something but thanks. – CornJ Apr 27 '17 at 08:39
  • Then have links to and edit page to edit one question at a time. You current implementation can never work, and its a confusing UI and is poor performance –  Apr 27 '17 at 08:42
  • @StephenMuecke This UI was specifically requested. – CornJ Apr 27 '17 at 08:45
  • So the poor user edits one question, then another and hits a submit button only to find that only one of the edits is submitted and they would be none the wiser (assuming you make the changes to make this work) –  Apr 27 '17 at 08:47
  • @StephenMuecke I think you're misunderstanding slightly, the changes won't be made, the UI will just have to be changed (like you said) whether the person who requested it to be this way likes it or not. I simply asked my question to see whether there was a way for this implementation to ever work. – CornJ Apr 27 '17 at 09:08
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/142810/discussion-between-stephen-muecke-and-corrinejw). –  Apr 27 '17 at 09:09

2 Answers2

1

The issue you are facing is caused by a misunderstanding of how asp.net model binding works in relation to lists. For example looking at the view model used in your controller action EditQuiz.

public class OldQuizQAViewModel
{
    public int CourseID { get; set; }
    public int? QuizID { get; set; }
    public string QuizTitle { get; set; }
    public IList<OldQuizQuestions> OldQuizQuestions { get; set; }
}

In order for the model binding to work with a IList or any other collection, asp-net expects the form data you post to have sequential indexes. The form data you are sending over POST already has a working example of model binding with collections implemented. Looking at the form data:

Form Data

The highlighted properties show how to correctly bind to a collection, in that you set the values for the property Correct in the OldQuizAnswers model for each index of IList in OldQuizQAViewModel and pass these all at once in a single request.

Whereas in the same request you only pass the data for OldQuizQuestions of specific index you wish these values to be bound to in the IList collection.

This is why the fist time you post, the model binding works successfully as you are referencing the first index ([0]), whereas on the second POST you reference the second index (1) but not the first, causing the model binding to fail.

See herefor more information on how model binding works.

User7007
  • 331
  • 3
  • 14
0

Have you tried adding a @Html.HiddenFor(model => model.OldQuizQuestions[j]) ?

I read this "It creates a hidden input on the form for the field (from your model) that you pass it.

It is useful for fields in your Model/ViewModel that you need to persist on the page and have passed back when another call is made but shouldn't be seen by the user."

Answer comes from https://stackoverflow.com/a/3866720/8404545

Might be the problem here

Max
  • 794
  • 3
  • 7