Sending/POSTing collection data from an HTML/Razor form to an MVC action argument model

I am working on a project to help students and advisers select the best courses for the next semester, using ASP.NET MVC 5. The first step is for the student to select the courses he has already taken from a list. The controller which displays the list is:

public ActionResult AddCourseVM (int? id)
    {
        Student student = db.Students.Find(id);
        // List<BaseCourse> potentialCourses = student.StudentConcentration.RequiredCourses.ToList();
        List<BaseCourse> potentialCourses = db.BaseCourses.ToList();
        AddCourseViewModel vModel = new AddCourseViewModel(student, potentialCourses);

        List<Course> listCourses = new List<Course>();

        foreach(BaseCourse baseC in potentialCourses)
        {
            Course c = new Course();
            c.BaseCourse = baseC;
            c.Student = student;
            listCourses.Add(c);
        }

        vModel.PossibleCourses = listCourses;

        return View("AddCourseVM", vModel);
    }

The ViewModel is:

public class AddCourseViewModel
{
    public Student Student { get; set; }
    public List<BaseCourse> AvailCourses { get; set; }
    public List<Course> PossibleCourses { get; set; }

    public AddCourseViewModel(Student s, List<BaseCourse> c)
    {
        Student = s;
        AvailCourses = c;
        PossibleCourses = new List<Course>();
    }

    public AddCourseViewModel()
    {
        Student = new Student();
        AvailCourses = new List<BaseCourse>();
        PossibleCourses = new List<Course>();
    }
}

The Course objects are specific instances of a course (for a given student, in a certain semester, etc), the BaseCourse objects are the individual courses from the course catalog.

I am displaying the possible courses in a list using this view:

    @model CMPSAdvising.ViewModels.AddCourseViewModel
@{
    ViewBag.Title = "AddCourseVM";
}

<h2>Add Courses</h2>

<div>
    <p>Name: @Model.Student.FirstName @Model.Student.LastName</p>
    <p>W#: @Model.Student.WNumber</p>
</div>

<div>
        <p>Select the Courses You Have Taken</p>
</div>
<div>
    @using (Html.BeginForm("AddCourseVM","Students"))
    {
        @Html.AntiForgeryToken();
        <div>
            <table class="table table-bordered">
                <tr>
                    <th>Course</th>
                    <th>Department</th>
                    <th>Number</th>
                    <th>Check if Taken</th>
                    <th>Semester</th>
                    <th>Grade</th>
                </tr>
                @foreach (var course in Model.PossibleCourses)
                {
                    <tr>
                        <td>@course.BaseCourse.Name</td>
                        <td>@course.BaseCourse.CourseNumber</td>
                        <td>@course.BaseCourse.CourseNumber</td>
                        <td>@Html.CheckBoxFor(s => course.Selected)</td>
                        <td>@Html.TextBoxFor(m => course.Semester)</td>
                        <td>@Html.TextBoxFor(g => course.Grade)</td>
                    </tr>
                }
            </table>
            <input type="submit" value="Save Classes Taken" class="btn btn-default" />
        </div>
    }
</div>

And finally the controller that receives the POST when the user hits the button:

[HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult AddCourseVM (AddCourseViewModel vModel)
    {
        Student stu = vModel.Student;
        foreach (Course c in vModel.PossibleCourses)
        {
            if (c.Selected)
            {
                stu.CoursesTaken.Add(c);
            }
        }

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

        return RedirectToAction("ListTakenCourses", new { id = stu.ID });
    }

My problem is that the AddCourseViewModel object (vModel) is coming back null. I would like to get the ViewModel back from the web page as an object, or at least get the list of courses that were checked and the student’s ID.

Answers:

Thank you for visiting the Q&A section on Magenaut. Please note that all the answers may not help you solve the issue immediately. So please treat them as advisements. If you found the post helpful (or not), leave a comment & I’ll get back to you as soon as possible.

Method 1

I believe there is an app for that called the BeginCollectionItem HtmlHelper. It is discussed briefly with references here, and is based on a blog article written by Steve Sanderson a few years ago.

The problem is that model binding with form collections is not quite the same as model binding with scalar inputs in MVC. Your collection needs an indexer, as discussed here. If it is not binding as expected, examine the name attributes of the form inputs that get rendered, and compare them to the property names and structures within the post model (action argument) class.

From the looks of it, HTML output that looked more like this should cause the action argument to be not null:

<tr>
    <td>HTTP 101</td>
    <td>HTP-101</td>
    <td>HTP-101</td>
    <td><input type="checkbox" name="PossibleCourses[0].Selected" /></td>
    <td><input type="text" name="PossibleCourses[0].Semester"></td>
    <td><input type="text" name="PossibleCourses[0].Grade"></td>
</tr>
<tr>
    <td>MVC 101</td>
    <td>MVC-101</td>
    <td>MVC-101</td>
    <td><input type="checkbox" name="PossibleCourses[1].Selected" /></td>
    <td><input type="text" name="PossibleCourses[1].Semester"></td>
    <td><input type="text" name="PossibleCourses[1].Grade"></td>
</tr>

…and so on, which the following razor should output:

@for (var i = 0; i <= Model.PossibleCourses.Count; i++)
{
    var course = Model.PossibleCourses[i];
    <tr>
        <td>@course.BaseCourse.Name</td>
        <td>@course.BaseCourse.CourseNumber</td>
        <td>@course.BaseCourse.CourseNumber</td>
        <td>@Html.CheckBox(string.Format("PossibleCourses[{0}].Selected", i),
            course.Selected)</td>
        <td>@Html.TextBox(string.Format("PossibleCourses[{0}].Semester", i),
            course.Semester)</td>
        <td>@Html.TextBox(string.Format("PossibleCourses[{0}].Grade", i),
            course.Grade)</td>
    </tr>
}

Note how the name attributes of the form input elements correspond to the name of the indexable (List<Course>) property in your action argument Model, and the indexed (Course) property names wrapped inside the collection. This is one way to help the model binder figure out how to populate the action argument class instance with data, by making the input name attributes match the method argument property names.

You could also use a GUID (or any string for that matter) to serve as an indexer, which is what BeginItemCollection does internally. The following should also help the model binder be able to populate the action argument so that it does not come in as null to the action method:

@foreach (var course in Model.PossibleCourses)
{
    var indexer = Guid.NewGuid(); // or possibly course.CourseId
    <tr>
        <td>@course.BaseCourse.Name</td>
        <td>@course.BaseCourse.CourseNumber</td>
        <td>@course.BaseCourse.CourseNumber</td>
        <td>@Html.Hidden("PossibleCourses.index", indexer)
            @Html.CheckBox(string.Format("PossibleCourses[{0}].Selected", indexer),
            course.Selected)</td>
        <td>@Html.TextBox(string.Format("PossibleCourses[{0}].Semester", indexer),
            course.Semester)</td>
        <td>@Html.TextBox(string.Format("PossibleCourses[{0}].Grade", indexer),
            course.Grade)</td>
    </tr>
}

All that matters is each group of form elements which correspond to a collection item in the action argument class must share the same indexer, and the indexer must be different from other groups of form elements that correspond to a different collection item in the action argument class. A sample of this solution can be understood by reading this question and its answer.


All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x