MVC How to pass a list of objects with List Items

2019-02-14 04:55发布

问题:

I want to post a List of items to controller from Razor view , but i am getting a List of objects as null My class structre is

Model:

List<Subjects> modelItem

class Subjects
{
    int SubId{get;set;}
    string Name{get;set;}
    List<Students> StudentEntires{get;set;}
}

class StudentEntires
{
    int StudId{get;set;}
    string Name{get;set;}
    int Mark{get;set;}
}

The model itself is a list of items and every items contain List of child items as well. Example model is a list of Subjects and every subject contains a List of Students, and i want to input mark for every student

My View is like

@model IList<Subjects>  
@{   
   Layout = "~/Views/Shared/_Layout.cshtml";
}
@using (Html.BeginForm())
{    
   @Html.ValidationSummary(true)
   if (Model.Count > 0)
   {
       @for (int item = 0; item < Model.Count(); item++)
       {
           <b>@Model[item].Name</b><br />
           @foreach (StudentEntires markItem in Model[item].StudentEntires)
           {
               @Html.TextBoxFor(modelItem => markItem.Mark)
           }
       }
       <p style="text-align:center">
           <input type="submit" class="btn btn-primary" value="Update" />
       </p>
    }
}

And in controller

    [HttpPost]
    public ActionResult OptionalMarks(int Id,ICollection<Subjects> model)
    {
        //BUt my model is null. Any idea about this?
    }

回答1:

You're finding this difficult because you're not utilising the full power of the MVC framework, so allow me to provide a working example.

First up, let's create a view model to encapsulate your view's data requirements:

public class SubjectGradesViewModel
{
    public SubjectGradesViewModel()
    {
        Subjects = new List<Subject>();
    }

    public List<Subject> Subjects { get; set; }
}

Next, create a class to represent your subject model:

public class Subject
{
    public int Id { get; set; }
    public string Name { get; set; }
    public List<Student> StudentEntries { get; set; }
}

Finally, a class to represent a student:

public class Student
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Grade { get; set; }
}

At this point, you have all the classes you need to represent your data. Now let's create two controller actions, including some sample data so you can see how this works:

public ActionResult Index()
{
    var model = new SubjectGradesViewModel();

    // This sample data would normally be fetched
    // from your database
    var compsci = new Subject
    {
        Id = 1,
        Name = "Computer Science",
        StudentEntries = new List<Student>()
        {
            new Student { Id = 1, Name = "CompSci 1" },
            new Student { Id = 2, Name = "CompSci 2" },
        }
    };

    var maths = new Subject
    {
        Id = 2,
        Name = "Mathematics",
        StudentEntries = new List<Student>()
        {
            new Student { Id = 3, Name = "Maths 1" },
            new Student { Id = 4, Name = "Maths 2" },
        }
    };

    model.Subjects.Add(compsci);
    model.Subjects.Add(maths);

    return View(model);
}

[HttpPost]
public ActionResult Index(SubjectGradesViewModel model)
{
    if (ModelState.IsValid)
    {
        return RedirectToAction("Success");
    }

    // There were validation errors
    // so redisplay the form
    return View(model);
}

Now it's time to construct the views, and this part is particularly important when it comes to sending data back to a controller. First up is the Index view:

@model SubjectGradesViewModel

@using (Html.BeginForm())
{
    @Html.ValidationSummary(true)

    @Html.EditorFor(m => m.Subjects) <br />
    <input type="submit" />
}

You'll notice I'm simply using Html.EditorFor, whilst passing Subjects as the parameter. The reason I'm doing this is because we're going to create an EditorTemplate to represent a Subject. I'll explain more later on. For now, just know that EditorTemplates and DisplayTemplates are special folder names in MVC, so their names, and locations, are important.

We're actually going to create two templates: one for Subject and one for Student. To do that, follow these steps:

  1. Create an EditorTemplates folder inside your view's current folder (e.g. if your view is Home\Index.cshtml, create the folder Home\EditorTemplates).
  2. Create a strongly-typed view in that directory with the name that matches your model (i.e. in this case you would make two views, which would be called Subject.cshtml and Student.cshtml, respectively (again, the naming is important)).

Subject.cshtml should look like this:

@model Subject

<b>@Model.Name</b><br />

@Html.HiddenFor(m => m.Id)
@Html.HiddenFor(m => m.Name)
@Html.EditorFor(m => m.StudentEntries)

Student.cshtml should look like this:

@model Student

@Html.HiddenFor(m => m.Id)
@Html.HiddenFor(m => m.Name)
@Html.DisplayFor(m => m.Name): @Html.EditorFor(m => m.Grade)
<br />

That's it. If you now build and run this application, putting a breakpoint on the POST index action, you'll see the model is correctly populated.

So, what are EditorTemplates, and their counterparts, DisplayTemplates? They allow you to create reusable portions of views, allowing you to organise your views a little more.

The great thing about them is the templated helpers, that is Html.EditorFor and Html.DisplayFor, are smart enough to know when they're dealing with a template for a collection. That means you no longer have to loop over the items, manually invoking a template each time. You also don't have to perform any null or Count() checking, because the helpers will handle that all for you. You're left with views which are clean and free of logic.

EditorTemplates also generate appropriate names when you want to POST collections to a controller action. That makes model binding to a list much, much simpler than generating those names yourself. There are times where you'd still have to do that, but this is not one of them.



回答2:

Change the action method signature to

public ActionResult OptionalMarks(ICollection<Subjects> model)

Since in your HTML, it does not look like there is anything named Id in there. This isn't your main issue though.

Next, do the following with the foor loop

@for(int idx = 0; idx < Model[item].StudentEntires.Count();idx++)
{
    @Html.TextBoxFor(_ => Model[item].StudentEntries[idx])
}

Possibly due to the use of a foreach loop for the StudentEntries, the model binder is having trouble piecing everything together, and thus a NULL is returned.

EDIT:

Here's an example:

Controller

public class HomeController : Controller
{
    public ActionResult Index()
    {
        var viewModel = new IndexViewModel();

        var subjects = new List<Subject>();
        var subject1 = new Subject();

        subject1.Name = "History";
        subject1.StudentEntires.Add(new Student { Mark = 50 });
        subjects.Add(subject1);

        viewModel.Subjects = subjects;

        return View(viewModel);
    }

    [HttpPost]
    public ActionResult Index(IndexViewModel viewModel)
    {
        return new EmptyResult();
    }
}

View

@model SOWorkbench.Controllers.IndexViewModel

@{
    ViewBag.Title = "Home Page";
}

@using (Html.BeginForm())
{
    @Html.ValidationSummary(true)
    if (Model.Subjects.Any())
    {
        int subjectsCount = Model.Subjects.Count();
        for (int item = 0; item < subjectsCount; item++)
        {
            <b>@Model.Subjects[item].Name</b><br />            
            int studentEntriesCount = Model.Subjects[item].StudentEntires.Count();

            for(int idx = 0;idx < studentEntriesCount;idx++)
            {
                @Html.TextBoxFor(_ => Model.Subjects[item].StudentEntires[idx].Mark);
            }
        }
        <p style="text-align:center">
            <input type="submit" class="btn btn-primary" value="Update" />
        </p>
    }
}

When you post the form, you should see the data come back in the viewModel object.