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