vLr
vLr

Reputation: 75

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

Upvotes: 2

Views: 3795

Answers (4)

vLr
vLr

Reputation: 75

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.

Upvotes: 0

AlexC
AlexC

Reputation: 10756

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

Upvotes: 3

Tomasz Jaskuλa
Tomasz Jaskuλa

Reputation: 16013

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

Upvotes: 1

Aravind Sivam
Aravind Sivam

Reputation: 1099

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

Upvotes: 0

Related Questions