上一篇链接:EF学习笔记(七):读取关联数据
本篇原文链接:Updating Related Data
本篇主要考虑对于有关联的数据进行新增、删除、更新操作;比如Course 、Instructor;
对于Course来说,新增时候必须定义属于哪个Department,所以在新增、更新操作的时候,必须要用户选择Department;
MVC5在选择基础控制器及视图框架的时候,如果选择EF的操作框架,则会自动带一部分基础代码,比如Course的Create\Edit直接就带上了Department的下拉列表选择框;
但是对于一些错误情况处理不够以及没有做显示排序,原文做了些调整优化;
public ActionResult Create() { PopulateDepartmentsDropDownList(); return View(); } [HttpPost] [ValidateAntiForgeryToken] public ActionResult Create([Bind(Include = "CourseID,Title,Credits,DepartmentID")]Course course) { try { if (ModelState.IsValid) { db.Courses.Add(course); db.SaveChanges(); return RedirectToAction("Index"); } } catch (RetryLimitExceededException /* dex */) { //Log the error (uncomment dex variable name and add a line here to write a log.) ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator."); } PopulateDepartmentsDropDownList(course.DepartmentID); return View(course); } public ActionResult Edit(int? id) { if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } Course course = db.Courses.Find(id); if (course == null) { return HttpNotFound(); } PopulateDepartmentsDropDownList(course.DepartmentID); return View(course); } [HttpPost, ActionName("Edit")] [ValidateAntiForgeryToken] public ActionResult EditPost(int? id) { if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } var courseToUpdate = db.Courses.Find(id); if (TryUpdateModel(courseToUpdate, "", new string[] { "Title", "Credits", "DepartmentID" })) { try { db.SaveChanges(); return RedirectToAction("Index"); } catch (RetryLimitExceededException /* dex */) { //Log the error (uncomment dex variable name and add a line here to write a log. ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator."); } } PopulateDepartmentsDropDownList(courseToUpdate.DepartmentID); return View(courseToUpdate); } private void PopulateDepartmentsDropDownList(object selectedDepartment = null) { var departmentsQuery = from d in db.Departments orderby d.Name select d; ViewBag.DepartmentID = new SelectList(departmentsQuery, "DepartmentID", "Name", selectedDepartment); }
PopulateDepartmentsDropDownList
方法给视图传递了一个用Name排序过的Department列表;
并且处理了新建的时候 选中的Department为空的情况;
Create、Edit的Post请求中都对异常情况做了处理,并同时在下拉框还保持原先的选择;
另外,在Create、Edit的视图中,对于Department的标题也需要从DepartmentID改为Department :
<div class="form-group"> <label class="control-label col-md-2" for="DepartmentID">Department</label> @*@Html.LabelFor(model => model.DepartmentID, "DepartmentID", htmlAttributes: new { @class = "control-label col-md-2" })*@ <div class="col-md-10"> @Html.DropDownList("DepartmentID", null, htmlAttributes: new { @class = "form-control" }) @Html.ValidationMessageFor(model => model.DepartmentID, "", new { @class = "text-danger" }) </div>
</div>
另外,对于Create视图,EF默认框架建立的视图会根据主关键字段是不是自动列来采用不同的方式;
如果定义为自动增加列,则不需要用户输入,由数据库自增长;
如果定义为非自动增长列,则需要用户手动,则会提供一个输入框;
但对于Edit视图,EF默认框架建立的视图不会明文显示主关键字段,因为它不知道这个字段到底有没有显示意义,它只用一个隐藏字段来包含主关键字段数据;
所以要手动增加显示主关键字段,但是就算自己手动增加显示主关键字段,也不可以删除那个隐藏字段!
@Html.ValidationSummary(true, "", new { @class = "text-danger" }) @Html.HiddenFor(model => model.CourseID) <div class="form-group"> @Html.LabelFor(model => model.CourseID, new { @class = "control-label col-md-2" }) <div class="col-md-10"> @Html.DisplayFor(model => model.CourseID) </div> </div> <div class="form-group"> @Html.LabelFor(model => model.Title, htmlAttributes: new { @class = "control-label col-md-2" }) <div class="col-md-10"> @Html.EditorFor(model => model.Title, new { htmlAttributes = new { @class = "form-control" } }) @Html.ValidationMessageFor(model => model.Title, "", new { @class = "text-danger" }) </div> </div>
同样,在Delete、Detail视图中,需要加入CourseID的显示:
<dt> @Html.DisplayNameFor(model => model.Department.Name)
</dt>
<dd> @Html.DisplayFor(model => model.Department.Name) </dd> <dt> @Html.DisplayNameFor(model => model.CourseID) </dt> <dd> @Html.DisplayFor(model => model.CourseID) </dd> <dt> @Html.DisplayNameFor(model => model.Title) </dt> <dd> @Html.DisplayFor(model => model.Title) </dd>
Course 修改效果:
Instructors 更新
对于Instructors,当进行编辑的时候,需要对Instructors指定Office,同时Instructors对于Office是 1对0或1 的关系,即有可能有Office,有可能没有 Office;
所以对于Instructors,编辑的时候,会碰到3种情况:
1、如果清除了Office,那么需要删除 Instructors和Office的对应关系;
2、如果原来是没有Office的,现在输入了Office,则需要增加 Instructors和Office的对应关系;
3、如果原来就有Office,现在输入了新的Office,则需要变更 Instructors和Office的对应关系;(变更OfficeAssginment实体)
EF框架自动生成的Edit Get请求肯定是不能满足以上要求的;(EF框架给搞了一个下拉框,这根本就不对)
所以把Edit Get请求修改为以下方式:
public ActionResult Edit(int? id) { if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } Instructor instructor = db.Instructors .Include(i => i.OfficeAssignment) .Where(i => i.ID == id) .Single(); if (instructor == null) { return HttpNotFound(); } return View(instructor); }
直接把Office数据从数据库里查出来;然后准备放在一个编辑框里显示出来;
通过原来的代码中的 find方法是没办法直接把Office数据一起查询出来的,所以需要用Where+Single来完成;
把Edit Post 方法改为如下代码:
[HttpPost, ActionName("Edit")] [ValidateAntiForgeryToken] public ActionResult EditPost(int? id) { if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } var instructorToUpdate = db.Instructors .Include(i => i.OfficeAssignment) .Where(i => i.ID == id) .Single(); if (TryUpdateModel(instructorToUpdate, "", new string[] { "LastName", "FirstMidName", "HireDate", "OfficeAssignment" })) { try { if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment.Location)) { instructorToUpdate.OfficeAssignment = null; } db.SaveChanges(); return RedirectToAction("Index"); } catch (RetryLimitExceededException /* dex */) { //Log the error (uncomment dex variable name and add a line here to write a log. ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator."); } } return View(instructorToUpdate); }
先用和Get请求中一样的代码,把原instructor从数据库里取出;
然后用 TryUpdateModel 来通过设置的列名白名单来更新实体数据;
如果Office输入为空,则设置Instructor的OfficeAssignment导航属性为空,则EF会自动在保存的时候,删除Instructors和Office的对应表的数据行;
在Edit 视图中,增加Office的编辑框:
<div class="form-group"> @Html.LabelFor(model => model.OfficeAssignment.Location, new { @class = "control-label col-md-2" }) <div class="col-md-10"> @Html.EditorFor(model => model.OfficeAssignment.Location) @Html.ValidationMessageFor(model => model.OfficeAssignment.Location) </div> </div>
下面就是准备把Instructor和Course的对应关系在编辑Instructor的时候一并建立;
Instructor对应Course是1对0或多,那么只能通过多个CheckBox来选取;原文用的方式确实比较惊奇,在视图中进行拼接显示视图的方式;
首先,先定义个显示Model用来支持Course显示:(CourseID用来定义ID,Title用来显示Course标题,Assigned用来对应CheckBox是否选中)
namespace EFTest.ViewModels { public class AssignedCourseData { public int CourseID { get; set; } public string Title { get; set; } public bool Assigned { get; set; } } }
第2步;编辑Instructor Edit Get请求:把Instructor的Course显示Model取出来传给视图;
public ActionResult Edit(int? id) { if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } Instructor instructor = db.Instructors .Include(i => i.OfficeAssignment) .Include(i => i.Courses) .Where(i => i.ID == id) .Single(); PopulateAssignedCourseData(instructor); if (instructor == null) { return HttpNotFound(); } return View(instructor); } private void PopulateAssignedCourseData(Instructor instructor) { var allCourses = db.Courses; var instructorCourses = new HashSet<int>(instructor.Courses.Select(c => c.CourseID)); var viewModel = new List<AssignedCourseData>(); foreach (var course in allCourses) { viewModel.Add(new AssignedCourseData { CourseID = course.CourseID, Title = course.Title, Assigned = instructorCourses.Contains(course.CourseID) }); } ViewBag.Courses = viewModel; }
第3步,编辑Instructor Edit Post请求,来进行Instructor更新,在传回InstructorID的同时,传回选中的CourseID列表:
[HttpPost] [ValidateAntiForgeryToken] public ActionResult Edit(int? id, string[] selectedCourses) { if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } var instructorToUpdate = db.Instructors .Include(i => i.OfficeAssignment) .Include(i => i.Courses) .Where(i => i.ID == id) .Single(); if (TryUpdateModel(instructorToUpdate, "", new string[] { "LastName", "FirstMidName", "HireDate", "OfficeAssignment" })) { try { if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment.Location)) { instructorToUpdate.OfficeAssignment = null; } UpdateInstructorCourses(selectedCourses, instructorToUpdate); db.SaveChanges(); return RedirectToAction("Index"); } catch (RetryLimitExceededException /* dex */) { //Log the error (uncomment dex variable name and add a line here to write a log. ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator."); } } PopulateAssignedCourseData(instructorToUpdate); return View(instructorToUpdate); } private void UpdateInstructorCourses(string[] selectedCourses, Instructor instructorToUpdate) { if (selectedCourses == null) { instructorToUpdate.Courses = new List<Course>(); return; } var selectedCoursesHS = new HashSet<string>(selectedCourses); var instructorCourses = new HashSet<int> (instructorToUpdate.Courses.Select(c => c.CourseID)); foreach (var course in db.Courses) { if (selectedCoursesHS.Contains(course.CourseID.ToString())) { if (!instructorCourses.Contains(course.CourseID)) { instructorToUpdate.Courses.Add(course); } } else { if (instructorCourses.Contains(course.CourseID)) { instructorToUpdate.Courses.Remove(course); } } } }
原文的意思是,视图没有Courses
的实体集合,所以模型绑定不能自动把instructor的course属性自动绑定上去,只能手动后台进行绑定;
而且,如果没有选择任何course ,也需要初始化一个空的Courses
的实体集合;
后面就是做原有的Courses
的实体集合和新选择的courseID列表的对比,新增的就Add,去除的就Remove;
第4步,就是要改视图:
在Office 那个<div>下面增加一段:
<div class="form-group"> <div class="col-md-offset-2 col-md-10"> <table> <tr> @{ int cnt = 0; List<EFTest.ViewModels.AssignedCourseData> courses = ViewBag.Courses; foreach (var course in courses) { if (cnt++ % 3 == 0) { @:</tr><tr> } @:<td> <input type="checkbox" name="selectedCourses" value="@course.CourseID" @(Html.Raw(course.Assigned ? "checked=\"checked\"" : "")) /> @course.CourseID @: @course.Title @:</td> } @:</tr> } </table> </div> </div>
代码确实比较神奇,不过就是直接粘贴上去后,需要调整调整格式,不然格式是乱的。。。刚开始看的时候,确实比较头疼。。。
看看样式结果:
对于Create也一样,需要修改Create Get 请求、Post请求:
public ActionResult Create() { var instructor = new Instructor(); instructor.Courses = new List<Course>(); PopulateAssignedCourseData(instructor); return View(); } [HttpPost] [ValidateAntiForgeryToken] public ActionResult Create([Bind(Include = "LastName,FirstMidName,HireDate,OfficeAssignment" )]Instructor instructor, string[] selectedCourses) { if (selectedCourses != null) { instructor.Courses = new List<Course>(); foreach (var course in selectedCourses) { var courseToAdd = db.Courses.Find(int.Parse(course)); instructor.Courses.Add(courseToAdd); } } if (ModelState.IsValid) { db.Instructors.Add(instructor); db.SaveChanges(); return RedirectToAction("Index"); } PopulateAssignedCourseData(instructor); return View(instructor); }
这里,原文还提到一个优化操作;对于新增加的Instructor,一开始是没有Course列表的,但是要视图正确显示,需要手动new一个Course实例集合;
那么可以考虑把这个动作放在取Course实例集合的时候统一做一次New操作:(在Instructor的Courses的导航属性的get方法中,如果courses为空,则new一个空集合;)
private ICollection<Course> _courses; public virtual ICollection<Course> Courses { get { return _courses ?? (_courses = new List<Course>()); } set { _courses = value; } }
有了这个修改,在控制器里的,手动new Course实例集合的一行可以去掉。
最后把Create对应的视图也增加CheckBox来选择Course:
<div class="form-group"> @Html.LabelFor(model => model.OfficeAssignment.Location, new { @class = "control-label col-md-2" }) <div class="col-md-10"> @Html.EditorFor(model => model.OfficeAssignment.Location) @Html.ValidationMessageFor(model => model.OfficeAssignment.Location) </div> </div> <div class="form-group"> <div class="col-md-offset-2 col-md-10"> <table> <tr> @{ int cnt = 0; List<EFTest.ViewModels.AssignedCourseData> courses = ViewBag.Courses; foreach (var course in courses) { if (cnt++ % 3 == 0) { @:</tr><tr> } @:<td> <input type="checkbox" name="selectedCourses" value="@course.CourseID" @(Html.Raw(course.Assigned ? "checked=\"checked\"" : "")) /> @course.CourseID @: @course.Title @:</td> } @:</tr> } </table> </div> </div>
最后Create的视图显示结果:
后面来修改Index视图,在Index 视图里直接多行显示出Instructor的Course;
先在标题列增加 Courses标题:
<tr>
<th>Last Name</th>
<th>First Name</th>
<th>Hire Date</th>
<th>Office</th>
<th>Courses</th>
<th></th>
</tr>
然后在Office的后面循环行显示Course:
<td> @if (item.OfficeAssignment != null) { @item.OfficeAssignment.Location } </td> <td> @{ foreach (var course in item.Courses) { @course.CourseID @: @course.Title <br /> } } </td> <td> @Html.ActionLink("Select", "Index", new { id = item.ID }) | @Html.ActionLink("Edit", "Edit", new { id = item.ID }) | @Html.ActionLink("Details", "Details", new { id = item.ID }) | @Html.ActionLink("Delete", "Delete", new { id = item.ID }) </td>
最后显示结果:
最后,对于DeleteComfirm 请求也需要修改:
[HttpPost, ActionName("Delete")] [ValidateAntiForgeryToken] public ActionResult DeleteConfirmed(int id) { Instructor instructor = db.Instructors .Include(i => i.OfficeAssignment) .Where(i => i.ID == id) .Single(); db.Instructors.Remove(instructor); var department = db.Departments .Where(d => d.InstructorID == id) .SingleOrDefault(); if (department != null) { department.InstructorID = null; } db.SaveChanges(); return RedirectToAction("Index"); }
这里的修改主要是对删除instructor后对Department的影响做处理;
不然,直接删除instructor,如果有Department有这个instructor的外键,就会引发异常,需要清除这个Department 对应的instructor .
最后,EF框架默认采用事务的方式处理数据库请求;即要么全部成功,要么全部不成功。