2

I have two models, question and answer. I want to insert a list of answers thru ViewModel to a question but it seems in my post method my list is getting null. That might be a bad implementation as well, because I am returning a model of my question back when I post something and I guess my List is just getting null. How could I fix this?

Edit: I remade the controller and the view based on comments you gave me: Thats how it looks now, but seems my Answer List to be Empty again.

ViewModel:

 public class ViewModel
{
    public IEnumerable<Answer> Answers { get; set; }
    public Question Question { get; set; }
}

Controller:

[Authorize]
        public ActionResult Create()
        {
            ViewModel vm = new ViewModel();
            ViewBag.BelongToTest = new SelectList(db.Tests, "TestId" , "TestTitle").FirstOrDefault();
            vm.Question =  new Question { Question_Text = String.Empty };
            vm.Answers = new List<Answer> { new Answer { CorrectOrNot = false, AnswerText = "", OpenAnswerText = "" } };
            return View(vm);
        }

        //
        // POST: /Question/Create

        [HttpPost]
        [Authorize]
        public ActionResult Create(ViewModel vm)
        {

                if (ModelState.IsValid)
                {

                   vm.Question.BelongToTest = (from t in db.Tests
                                             join m in db.Members on t.AddedByUser equals m.MemberId
                                             where m.UserID == WebSecurity.CurrentUserId &&
                                             t.AddedByUser == m.MemberId
                                             orderby t.TestId descending
                                             select t.TestId).FirstOrDefault();

                    db.Questions.Add(vm.Question);
                    db.SaveChanges();

                    if (vm.Answers != null)
                    {
                        foreach (var i in vm.Answers)
                        {
                            i.BelongToQuestion = vm.Question.QuestionId;

                            db.Answers.Add(i);
                        }
                    }

                    db.SaveChanges();
                    ViewBag.Message = "Data successfully saved!";
                    ModelState.Clear();

                }

                ViewBag.BelongToTest = new SelectList(db.Tests, "TestId", "TestTitle", vm.Question.BelongToTest);
                vm.Question = new Question { Question_Text = String.Empty };
                vm.Answers = new List<Answer> { new Answer { CorrectOrNot = false, AnswerText = "", OpenAnswerText = "" } };
                return View("Create" , vm);

        }

View:

@model MvcTestApplication.Models.ViewModel
@using MvcTestApplication.Models

<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>

@{
    ViewBag.Title = "Create";
}

@using (Html.BeginForm("Create", "Question", FormMethod.Post)) {

<h2>Create</h2>

<table>
    <tr>
        <th>Question Name</th>
    </tr>

        <tr>
            <td>@Html.EditorFor(model=>model.Question.Question_Text)</td>
        </tr>

</table>

<table id="dataTable">
    <tr>
        <th>Correct?</th>
        <th>Answer text</th>
        <th>Open Answer</th>
    </tr>
   @foreach(var i in Model.Answers)
{
    <tr>
         <td>@Html.CheckBoxFor(model=>i.CorrectOrNot)</td>
         <td>@Html.EditorFor(model=>i.AnswerText)</td>
         <td>@Html.EditorFor(model=>i.OpenAnswerText)</td>
    </tr>
}
</table>

<input type="button" id="addNew" value="Add Answer"/>
<input type="submit" value="Create" />

}

@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")

    <script lang="javascript">
        $(document).ready(function () {

            //1. Add new row
            $("#addNew").click(function (e) {
                e.preventDefault();
                var $tableBody = $("#dataTable");
                var $trLast = $tableBody.find("tr:last");
                var $trNew = $trLast.clone();

                var suffix = $trNew.find(':input:first').attr('name').match(/\d+/);
                $trNew.find("td:last").html('<a href="#" class="remove">Remove</a>');
                $.each($trNew.find(':input'), function (i, val) {
                    // Replaced Name
                    var oldN = $(this).attr('name');
                    var newN = oldN.replace('[' + suffix + ']', '[' + (parseInt(suffix) + 1) + ']');
                    $(this).attr('name', newN);
                    //Replaced value
                    var type = $(this).attr('type');
                    if (type.toLowerCase() == "text") {
                        $(this).attr('value', '');
                    }

                    // If you have another Type then replace with default value
                    $(this).removeClass("input-validation-error");

                });
                $trLast.after($trNew);

                // Re-assign Validation 
                var form = $("form")
                    .removeData("validator")
                    .removeData("unobtrusiveValidation");
                $.validator.unobtrusive.parse(form);
            });

            // 2. Remove 
            $('a.remove').live("click", function (e) {
                e.preventDefault();
                $(this).parent().parent().remove();
            });

        });
                </script>
          }
vLr
  • 75
  • 1
  • 11
  • What's `return RedirectToAction("Create", "Question", question);` redirecting to? Neither of your controllers take in a `Question` as a single parameter. Also, generally speaking, you should create a ViewModel for your page with the SelectList in it instead of storing the SelectList in a ViewBag. – alex Mar 31 '15 at 13:09
  • Regarding you edit, your are still not creating your controls correctly - you need to use a `for` loop - `for (int i = 0; i < Model.Answers.Count; i++) { @Html.CheckBoxFor(m => m[i].CorrectOrNot) ...}` –  Mar 31 '15 at 23:12

4 Answers4

3

For the ModelBinder to bind to a List the HTML form must be sequentially indexed.

Your

<td>@Html.CheckBoxFor(model=>a.CorrectOrNot)</td>
<td>@Html.EditorFor(model=>a.AnswerText)</td>
<td>@Html.EditorFor(model=>a.OpenAnswerText)</td>

is creating something that will be bound to an individual answer. You need to render HTML that will be bound to a List, something like

@for (int i = 0; i < ((List<Answer>)ViewData["Answers"]).Count; i++)
{
    <tr>
         <td>@Html.CheckBoxFor(model=>((List<Answer>)ViewData["Answers"])[i].CorrectOrNot)</td>
         <td>@Html.EditorFor(model=>((List<Answer>)ViewData["Answers"])[i].AnswerText)</td>
         <td>@Html.EditorFor(model=>((List<Answer>)ViewData["Answers"])[i].OpenAnswerText)</td>
    </tr>
}

Also, this looks pretty awful casting ViewData all over the place. It would generally be better, if you plan to keep this approach creating a real view model. You could pass that model to the view and it could wrapper both question and answer collections.

EDIT:

You still need to have a sequential index against your list which your edited implementation is not supplying. Something like

@for (int i = 0; i < Model.Answers.Count; i++)
{
  <tr>
     <td>@Html.CheckBoxFor(model=> Model.Answers[i].CorrectOrNot)</td>
     <td>@Html.EditorFor(model=> Model.Answers[i].AnswerText)</td>
     <td>@Html.EditorFor(model=> Model.Answers[i].OpenAnswerText)</td>
  </tr>
}
AlexC
  • 10,676
  • 4
  • 37
  • 55
  • This article also explains how you could achieve this with non-sequential indices as well as sequential http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx/ – mheptinstall Apr 01 '15 at 08:01
1

ViewData is relevant when going from the controller to the view. It won't post back.

You should relay on the (model / parameter) binding that will take care of passing List<Answer> answerList for you

Tomasz Jaskuλa
  • 15,723
  • 5
  • 46
  • 73
0

ViewData is only to transfer the data between the view and controller. You can use session to transfer the data between the controller

Aravind Sivam
  • 1,089
  • 8
  • 23
0

Thanks for the comments. They really helped me out. It was all correct that you say but there was something that was missing. My IEnumerable in the ViewModel simply does not allow me to index my values, instead using IList helped me out to index everything as it is supposed to be and everything works.

vLr
  • 75
  • 1
  • 11