Search code examples
asp.net-mvcasp.net-mvc-4viewdata

Why is my ViewData list null? MVC 4


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>
          }

Solution

  • 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>
    }