Update many-to-many entity with ASP.NET Core MVC MultiSelectList

asp.net-core-1.0 asp.net-mvc c# entity-framework-core

Question

I have a form where I want to update an entity with a many-to-many relationship with another entity, and I'm trying to use the select tag to create the MultiSelectList control. I'm using ASP.NET Core 1.0 with Entity Framework.

Say I have the following entities:

public class Student
{
    public int Id { get; set; }
    public string Name { get; set; }
    public List<StudentCourse> Courses { get; set; }
}

public class Course
{
    public int Id { get; set; }
    public string Name { get; set; }
    public List<StudentCourse> Students { get; set; }
}

public class StudentCourse
{
    public int StudentId { get; set; }
    public Student Student { get; set; }

    public int CourseId { get; set; }
    public Course Course { get; set; }
}

The view model:

public class StudentVM
{
    public Student Student { get; set; }
    public IEnumerable<SelectListItem> CourseList { get; set; }
}

The view:

@model StudentVM
<form asp-action="Edit">
    <div class="form-horizontal>
        <div class="form-group">
            <select asp-for="CourseList" asp-items="Model.CourseList" />
        </div>
        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </div>
    </div>
</form>

The controller's action methods:

[HttpGet]
public async Task<IActionResult> Edit(Student student)
{
    var viewmodel = new StudentVM {
        Student = student,
        CourseList = new MultiSelectList(
            _db.Courses,
            "Id",
            "Name",
            student.Courses
            .Select(sc => sc.CourseId));
    };
    return View(viewmodel);
}

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(StudentVM viewmodel)
{
    if (viewmodel.Student == null)
        return NotFound();

    if (ModelState.IsValid)
    {
        Student student = viewmodel.Student;
        var newCourses = viewmodel.CourseList
                         .Where(i => i.Selected)
                         .Select(c => new StudentCourse {
                             StudentId = student.Id,
                             CourseId = Convert.ToInt32(c.Value)
                         });
        student.Courses.RemoveAll(sc => !newCourses.Contains(sc));
        student.Courses.AddRange(
            newCourses.Where(nc => !student.Courses.Contains(nc)));
        _db.Update(student);
        _db.SaveChanges();

        return RedirectToAction("Index");
    }
    return View(viewmodel);
}

Now come the questions...

Question 1

When StudentVM comes into the HttpPost variant of Edit, both the properties are null. Why do the properties in the view model get cleared before entering the Edit function? I've confirmed they are being set correctly in the HttpGet variant, and that the data displays correctly in the view.

Question 2

Since the data coming into Edit is invalid, I can't test my database updates. Is my method of getting the selected items from the SelectList and adding/removing items from Student.Courses correct? I haven't been able to find a lot of documentation regarding many-to-many relationships in EF Core.

I'm rather new to ASP.NET and Entity Framework, so any tips or criticisms would be greatly appreciated.

1
3
8/27/2016 2:56:36 AM

Accepted Answer

You cannot bind a <select> to a collection of complex objects (which is what MultiSelectList is. A <select multiple> element posts back an array of its selected option values (which in your case will be an array of the Courses Id values). Your model needs a property to bind to. Assuming he Id property of Course is int, then add the following property

public IEnumerable<int> SelectedCourses { get; set; }

Note also that setting the selectedValues property in the MultiSelectList constructor is ignored by the tag helper (internally the method builds a new IEnumerable<SelectListItem> based on the value of the property. The code in your GET method should be

var viewmodel = new StudentVM
{
    Student = student,
    CourseList = new SelectList(_db.Courses, "Id", "Name"),
    SelectedCourses = student.Courses.Select(sc => sc.CourseId)   
};
return View(viewmodel);

Then in the view, use

<select asp-for="SelectedCourses" asp-items="Model.CourseList"></select>

And in the POST method, the value of SelectedCourses will contain an array of Course Id values that you selected in the view.

1
11/2/2016 8:24:48 PM


Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow