Mac Reichelt Mac Reichelt - 1 month ago 17
C# Question

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

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.

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.

Comments