【EntityFramework系列教程六,翻译】在ASP.NET MVC程序中使用EntityFramework对相关数据进行更新
前一章你已经学会如何显示相关数据,在本章中你将学会如何更新相关数据。大部分情况下更新只需通过更新对应的外键字段即可完成,不过对“多对多”关系而言,由于EF不是直接暴露那个中间连接表,因此你不得不“显式”从对应的导航属性中增加或者删除实体得以完成。
以下一些截图是你今日要完成的任务:
【为Courses自定义“新增”和“编辑”页面】
当一个新课程创建之时,它总是隶属于某一个特定的系;为方便期间,自生成“创建”和“编辑”的代码架构中就包含了一个可供选择“系”的下拉列表。下拉框设置了Department的Id,这是所有EntityFramework实体都有的,为了把正确的Department加载到Course的Department导航属性中去。你只需对此代码做一些小小的变动(增加错误捕获以及对下拉列表框中的“系”排序)即可使用,代码如下:
public ActionResult Create() { PopulateDepartmentsDropDownList(); return View(); } [HttpPost] public ActionResult Create(Course course) { try { if (ModelState.IsValid) { db.Courses.Add(course); db.SaveChanges(); return RedirectToAction("Index"); } } catch (DataException) { //Log the error (add a variable name after DataException) 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) { Course course = db.Courses.Find(id); PopulateDepartmentsDropDownList(course.DepartmentID); return View(course); } [HttpPost] public ActionResult Edit(Course course) { try { if (ModelState.IsValid) { db.Entry(course).State = EntityState.Modified; db.SaveChanges(); return RedirectToAction("Index"); } } catch (DataException) { //Log the error (add a variable name after DataException) ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator."); } PopulateDepartmentsDropDownList(course.DepartmentID); return View(course); } 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); }
“PopulateDepartmentDropdownList”方法获取了所有根据名称排序的课程,并且创建了一个SelectList列表作为Dropdownlist的数据源赋值给它,最后把整个SelectList存入ViewBag中。此方法另外还接受一个可选参数,以便下拉列表回发后它可以被置初值。
HttpGet传递方式的Create方法没有调用带参数的PopulateDepartmentDropdownlist方法,原因在于当一个课程刚被创建之时,“系”尚未确定。
public ActionResult Create() { PopulateDepartmentsDropDownList(); return View(); }
HttpGet传递方式的Edit方法根据赋值给相关课程的Id号设置了对应的相关课程:
public ActionResult Edit(int id) { Course course = db.Courses.Find(id); PopulateDepartmentsDropDownList(course.DepartmentID); return View(course); }
对于以HttpPost方式传递的Create和Edit方法都包含了“在页面重现后,设定对应课程的”代码段,其位置位于错误捕获之后:
catch (DataException) { //Log the error (add a variable name after DataException) ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator."); } PopulateDepartmentsDropDownList(course.DepartmentID); return View(course);
此代码确保当页面回发后再次呈现之时,保留原先选择的课程不变,同时显示错误信息。
在“Views\Course\Create.cshtml”中在Title字段之前添加一个可供读者输入课程Id的字段。正如先前所说的一样,自生成的代码不会包含主键字段,但是这个字段在此处有用,因为这样可以输入课程的Id。
<div class="editor-label"> @Html.LabelFor(model => model.CourseID) </div> <div class="editor-field"> @Html.EditorFor(model => model.CourseID) @Html.ValidationMessageFor(model => model.CourseID) </div>
在“Views\Course\Edit.cshtml, Views\Course\Delete.cshtml”两个页面中Title字段之前添加一个新字段用以显示课程编号,考虑是主键所以只能显示,不能被修改。
<div class="editor-label"> @Html.LabelFor(model => model.CourseID) </div> <div class="editor-field"> @Html.DisplayFor(model => model.CourseID) </div>
运行Create页面(显示课程索引页,点击“Create New”),为创建新课程录入数据:
点击Create,随着新课程的加入Course索引页加载显示完毕。在索引页中“系”名称自导航属性而来,说明表关系已经正确创建。
运行“Edit”页面(显示课程索引页),点击Edit按钮:
在页面上更改一些数据并点击Save,课程索引页就显示已经更新的课程信息。
【为Instructors增加编辑页】
当你要编辑Instructor实体的时候,你应当可以更新OfficeAssignment。由于Instructor和OfficeAssignment之间是“一对一”或者“一对零”的关系,那么你必须要处理下列一些情况:
1)当一个用户清除了OfficeAssignment信息时,你应当移除此实体。
2)当一个用户添加了OfficeAssignment信息,且原来为空时,你应当为此创建一个实体。
3)当一个用户更改了OfficeAssignment信息时,你应当更新对应的实体。
打开InstructorController.cs文件看看HttpGet方式的“编辑”方法:
public ActionResult Edit(int id) { Instructor instructor = db.Instructors.Find(id); ViewBag.InstructorID = new SelectList(db.OfficeAssignments, "InstructorID", "Location", instructor.InstructorID); return View(instructor); }
自生成的代码并不是你所要的……它生成了一个下拉列表,但是你希望是一个可以输入数值的文本框。因此用以下代码进行替换:
public ActionResult Edit(int id) { Instructor instructor = db.Instructors .Include(i => i.OfficeAssignment) .Include(i => i.Courses) .Where(i => i.InstructorID == id) .Single(); return View(instructor); }
本代码没有使用ViewBag保存并回传数据,不过使用了“饥饿模式”对Course和OfficeAssignment”进行数据加载(目前暂且用不到Course,不过稍后会用到的)。“饥饿模式”状态下你不能用Find寻找数据,取而代之的是使用“Where”和“Single”。
用以下代码替换HttpPost方式的Edit方法,它用于专门处理OfficeAssignment的更新:
[HttpPost] public ActionResult Edit(int id, FormCollection formCollection) { var instructorToUpdate = db.Instructors .Include(i => i.OfficeAssignment) .Include(i => i.Courses) .Where(i => i.InstructorID == id) .Single(); if (TryUpdateModel(instructorToUpdate, "", null, new string[] { "Courses" })) { try { if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment.Location)) { instructorToUpdate.OfficeAssignment = null; } db.Entry(instructorToUpdate).State = EntityState.Modified; db.SaveChanges(); return RedirectToAction("Index"); } catch (DataException) { //Log the error (add a variable name after DataException) ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator."); return View(); } } return View(instructorToUpdate); }
此代码做了一些事情:
1)使用“饥饿模式”在获取当前的Instructor实体同时加载了OfficeAssignment和Course等信息,这和你在HttpGet模式中的Edit方法一样。
2)从模型绑定设置中更新已获取的Instructor实体,此不包括Courses导航属性。
If (TryUpdateModel(instructorToUpdate, "", null, new string[] { "Courses" }))
3)第二个和第三个参数分别表示了“属性名称上无前缀”以及“没有需要包含的属性列表”,如果验证失败,那么TryUpdateModel将返回false,代码最终也执行return View这块。如果Office的Location为空,那么把对应的OfficeAssignment设置为null,这样相关的数据自然就被删除了。
if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment.Location)) { instructorToUpdate.OfficeAssignment = null; }
4)保存更新的记录:在“”中,在Hire Date字段的div后,为编辑Office的Location添加新字段:
<div class="editor-label"> @Html.LabelFor(model => model.OfficeAssignment.Location) </div> <div class="editor-field"> @Html.EditorFor(model => model.OfficeAssignment.Location) @Html.ValidationMessageFor(model => model.OfficeAssignment.Location) </div>
运行该页面(选择Instrcutors选项卡,单击Edit):
变更Location中的数据,点击保存:
那么新的Location数据就在Index页面显示出来,在Server Explorer中你也确实可以看到OfficeAssignment表中发生了同样的改变:
返回到Edit页,清空OfficeAssignment中的内容点击Save,Index显示空白的Office的Location;与之对应数据表中也删除了该记录:
再次到Edit页,为Office的Location数据做一些变更,单击Save之后看到Index页中数据同样做了变更,数据表显示的记录也表示数据发生了更新:
【为Instructor页添加“赋予课程”功能】
教师们可能教授多门课程,现在你借助一组复选框为Instructor增加了选课的功能。如下所示:
Course和Instructor之间是“多对多”的关系,所以你不能简单地访问“连接中间表”或者是外键;你可以从Instructors.Courses中增加或者移除相关的课程。
使得你可以任意选择教授课程的用户界面其实是一组复选框——每一个课程在界面上对应一个复选框,如果老师教授某课程,则那个对应复选框则打勾;我们可以通过打勾或者取消打勾的方式任意为某个老师指定课程,如果课程过于多的话,你或许要考虑换个界面展示这些课程;不过为创建或者删除相关的课程,你还是使用和本示例同样的方法。
为了给这一堆复选框提供数据,你需要使用到视图模型类;在ViewModel文件夹中创建“AssignedCourseData.cs”,并用以下代码进行替换:
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; namespace ContosoUniversity.ViewModels { public class AssignedCourseData { public int CourseID { get; set; } public string Title { get; set; } public bool Assigned { get; set; } } }
在“InstructorController.cs”,HttpGet方式下的Edit方法中调用了一个用于提供复选框信息的方法,此方法使用了本视图模型类,见以下代码:
public ActionResult Edit(int id) { Instructor instructor = db.Instructors .Include(i => i.OfficeAssignment) .Include(i => i.Courses) .Where(i => i.InstructorID == id) .Single(); PopulateAssignedCourseData(instructor); 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; }
新方法中从数据库读取全部的课程信息,以便使用视图模型类加载此系列的课程信息;对于每个课程而言,代码将检测对应的Instructor中导航属性Courses是否包含该课程:为了提高效率寻找,某个Instructor所有的课程都被放入一个HashSet,并且那些已选择的课程“Assigned”属性被设置成true。视图将决定哪些复选框将是处于勾选状态的……同时,这些信息将被存储到ViewBag并且发送回页面视图中。
下一步,添加点击“Save”按钮时要执行的代码——使用以下代码替换HttpPost方式的Edit方法,它调用了一个新方法用来更新指定Instructor的导航属性Courses信息。
[HttpPost] public ActionResult Edit(int id, FormCollection formCollection, string[] selectedCourses) { var instructorToUpdate = db.Instructors .Include(i => i.OfficeAssignment) .Include(i => i.Courses) .Where(i => i.InstructorID == id) .Single(); if (TryUpdateModel(instructorToUpdate, "", null, new string[] { "Courses" })) { try { if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment.Location)) { instructorToUpdate.OfficeAssignment = null; } UpdateInstructorCourses(selectedCourses, instructorToUpdate); db.Entry(instructorToUpdate).State = EntityState.Modified; db.SaveChanges(); return RedirectToAction("Index"); } catch (DataException) { //Log the error (add a variable name after DataException) 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); } } } }
如果没有一个复选框选中,那么在“UpdateInstructorCourses
”中的代码使用一个空集合初始化Courses导航属性,比如:
if (selectedCourses == null) { instructorToUpdate.Courses = new List(); return; }
接着代码将循环遍历所有在数据表中有的课程,如果选中的课程不属于那个指定的Instructor,它将通过Courses导航属性自动加入此Instructor:
if (selectedCoursesHS.Contains(course.CourseID.ToString())) { if (!instructorCourses.Contains(course.CourseID)) { instructorToUpdate.Courses.Add(course); } }
如果一个课程尚未选择,但是它却属于这个指定的Instructor,那么该课程将从Courses导航属性中移除。
else { if (instructorCourses.Contains(course.CourseID)) { instructorToUpdate.Courses.Remove(course); } }
在“Views\Instructor\Edit.cshtml”中,在div元素后面为OfficeAssignment字段添加一系列生成复选框选课的代码:
<div class="editor-field"> <table> <tr> @{ int cnt = 0; List<ContosoUniversity.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>
此代码创建了有着3列的表,每一列中都有一个复选框,旁边附带课程Id和名称;这些复选框的名称都是一致的,这告知模型绑定机制——它们属于同一组!每个复选框的value属性绑定了一个课程Id,这样当页面提交时,只有打勾的id才会被上传。
当这些复选框回发之后,对于那些已经被赋给当前Instructor的所有课程自然处于打勾状态。
在对教师课程信息选择做出变更之后,当页面回到Index后你应当可以检测这些变化;因此你需要在那个表中额外增加一个字段——此情况下你不必使用ViewBag,因为这些信息都已被你传递给页面的模型实体Instructor中的Courses这个导航属性所包含了。
在“Views\Instructor\Index.cshtml”中,在“<th>Office</th>”之后加上“<th>Courses</th>”:
<tr> <th></th> <th>Last Name</th> <th>First Name</th> <th>Hire Date</th> <th>Office</th> <th>Courses</th> </tr>
随后紧挨着office location单元格下增加一个新的单元格,用于显示全部选定课程:
<td> @{ foreach (var course in item.Courses) { @course.CourseID @: @course.Title <br /> } } </td>
现在运行Instructor的Index页看看所有教员,以及他们任教的课程:
点击某个教员的“Edit”查看Edit页面信息:
对一些课程选择信息做一些改变,然后单击“Save”,变更信息将立即在Instructor的Index页面上反映出来。
目前为止你已经完成了处理相关数据的任务,并且直到本章节,连同先前所有教程在一起,它们教会你完成了整个增删改查的任务;不过你尚未处理“并发冲突”问题。下一章我们就要讨论此问题,并且为你的一个已经完成的实体类型增加此功能,并加以解释。
关于其它EntityFramework资源您可以本系列最后一篇末尾处找到。