By Tom Dykstra, Tom Dykstra is a Senior Programming Writer on Microsoft's Web Platform & Tools Content Team.
全文目錄:Contoso 大學 - 使用 EF Code First 創建 MVC 應用
在上一次的課程中,你已經學習了如何顯示關聯的數據,我們將要更新關聯的數據。大多數情況下,可能就是更新表的外鍵字段。對於多對多的關系來說,由於 EF 並沒有直接將表與表之間的連接關系暴露出來,你就必須通過顯式對相關的導航屬性進行添加或者刪除實體來完成。
下面的截圖展示了我們馬上要完成的工作。
6-1 定制課程的創建和編輯頁面
當新的課程實體創建的時候,必須包含相關的 Department。為了達到這個目的,腳手架創建的代碼,包括創建和編輯的控制器以及視圖,都包含了對 Department 的下拉列表的支持。下拉列表設置 Course.DepartmentId 外鍵屬性,這對於 EF 通過 Department 導航屬性來加載 Department 實體來說是必須的。下面將要對腳手架生成的代碼進行一些小的改動,增加錯誤的處理以及對列表內容進行排序。
打開 CourseController.cs, 刪除原來的 Edit 和 Create 方法,使用下面的代碼替換它們。
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); }
PopulateDepartmentsDropDownList 方法獲取經過對 Name 進行排序的 Department,然后創建用於下拉列表的 SelectList 集合,通過 ViewBag 傳遞到視圖中。這個方法包含一個參數,允許調用方可選地傳遞一個初始選中項目的值。
HttpGet Create 方法調用沒有設置選中項的 PopulateDepartmentsDropDownList 方法,因為對於新創建的課程來說,還沒有確定歸屬的系。
public ActionResult Create() { PopulateDepartmentsDropDownList(); return View(); }
HttpGet Edit 方法則設置了當前選中的項目,基於當前被編輯課程的 DepartmentId。
public ActionResult Edit(int id) { Course course = db.Courses.Find(id); PopulateDepartmentsDropDownList(course.DepartmentID); return View(course); }
對於 Create 和 Edit 的 HttpPost 方法來說,在錯誤信息處理之后,都包含了設置當前選中項目的代碼。
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 之前增加一個新的字段,允許用戶輸入課程編號。想之前演示的那樣,腳手架沒有生成主鍵字段,因為主鍵字段對用戶沒有意義。所以需要添加以便用戶能夠輸入這個值。
<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>
運行 Create 頁面 ( 在課程的 Index 頁面,點擊 Create New ),然后輸入新的課程。
點擊 Create,在 Index 中應該可以看到包含新創建課程的列表。系的名稱通過導航屬性獲取到,顯示的數據是正確的。
運行編輯頁面 ( 在課程的 Index 頁面中在某個課程上選擇 Edit )
修改一些數據,然后點擊 Save,可以看到更新之后的課程數據。
6-2 增加教師的編輯頁面
在修改教師信息的時候,我們希望也能夠修改教師的辦公室分配。教師實體 Instructor 和辦公室分配 OfficeAssignment存在一對一或者一對零的關系,這意味着你必須處理如下的狀態:
- 如果教師原來存在一個辦公室分配,但是用戶刪除了它,那么,你必須移除並且刪除這個辦公室分配 OfficeAssignment 實體。
- 如果教師原來沒有辦公室分配,但是用戶輸入了一個,你必須創建一個新的辦公室分配。
- 如果用戶修改了原來的辦公室分配,你必須修改當前的辦公分配 OfficeAssignment 實體。
打開 InstructorController.cs,看一下 HttpGet Edit 方法。
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 語句,增加了使用預先加載的相關 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); }
代碼中處理了如下的內容:
- 從數據庫中獲取了教師 Instructor 實體,並且預先加載了相關的的辦公室分配 OfficeAssignment 和課程 Course 導航屬性。如同在 HttpGet 的 Edit 方法一樣。
- 使用通過模型綁定獲取的數據,更新 Instructor 實體,除了課程 Course 導航屬性之外。
If (TryUpdateModel(instructorToUpdate, "", null, new string[] { "Courses" }))
( 第二和第三個參數在屬性名的前面沒有前綴,而且沒有被包含的屬性列表 ),如果驗證失敗, TryUpdateModel 方法返回 false,程序將直接轉到方法最后的 return View 語句。
- 如果辦公位置為空,設置 Instructor.OfficeAssignment 屬性為 null,在 OfficeAssignment 表相關的行將會被刪除。
if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment.Location)) { instructorToUpdate.OfficeAssignment = null; }
- 將修改保存到數據庫中。
在 Views\Instructor\Edit.cshtml, 在 Hire Date 字段的 div 元素之后,增加新的字段來編輯辦公位置。
<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>
運行頁面 ( 選中教師 Instructors ,點擊某個教師的 Edit 鏈接 )
修改辦公室位置的值,然后保存 Save。
新的辦公位置出現在 Index 頁面上,打開數據庫中的 OfficeAssgnment 表,可以看到表中的數據行。
運行編輯頁面,將辦公位置 Office Location 清除掉,然后保存 Save。在 Index 頁面上辦公位置將顯示為空白,在表中的行被刪除了。
6-3 在教師編輯頁面增加課程分配
教師可以教授任意數量的課程。現在你需要擴展教師的編輯頁面,增加通過一系列復選框分配可能的能力,如下所示。
在課程 Course 和教師 Instructor 之間存在多對多的關系,這意味着你不需要直接訪問表之間的關聯。而是通過增加或者刪除 Instructor. Course 實體來完成。
在 UI 界面上,與教師相關的課程被顯示為一組復選框,在數據庫中的每一門課程都有一個對應的復選框,教師當前教授的課程對應的復選框處於被選中狀態。用戶可以通過選中或者取消選中來改變教師教授的課程。如果課程的數量巨大,你可能需要采取其他的方式來顯示這些數據,但是你可以使用類似的方式來控制導航屬性以便創建和刪除關系。
為了為視圖提供復選框數據,你需要使用視圖模型 ViewModel,在 ViewModels 文件夾中創建 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; }
新創建的方法中,讀取所有的課程實體,加載到視圖模型中,對於每一個課程,檢查這個課程是否存在於教師實體的 Courses 導航集合屬性中。為了更加有效地遍歷教師講授的課程,將教師講授的課程通過 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); } } } }
如果沒有復選框被選中,在 UpdateInstructorCourse 方法中,使用一個空的集合來初始化 Courses 導航屬性。
if (selectedCourses == null) { instructorToUpdate.Courses = new List(); return; }
然后,代碼遍歷數據庫中所有的課程,如果課程的復選框被選中了,但是沒有包含在教師 Insturctor 的 Courses 集合中。這個課程將會被加入到導航屬性集合中。
if (selectedCoursesHS.Contains(course.CourseID.ToString())) { if (!instructorCourses.Contains(course.CourseID)) { instructorToUpdate.Courses.Add(course); } }
如果課程沒有被選中,但是在教師的導航屬性 Courses 集合中,就從集合屬性中刪除掉。
else { if (instructorCourses.Contains(course.CourseID)) { instructorToUpdate.Courses.Remove(course); } }
在 Views\Instructor\Edit.cshtml 中,在 OfficeAssignment 區域的 div 之后,增加一個 Courses 區域,通過一組復選框來顯示課程的狀態。
<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>
代碼創建一個 HTML 的三列表格,每一列中顯示課程的標題和編號,后面跟着一個復選框。復選框的名稱是相同的 ( “selectedCourses” ),這樣在模型綁定的時候就會被連接成一組。復選框的 value 屬性設置為 CourseID。當提交頁面的時候,選中的復選框代表的 CourseID 將以數組的形式傳遞給控制器。
在復選框被初始化的時候,選中課程對應的復選框的 checked 屬性被設置為選中狀態。
在修改了課程狀態之后,當回到 Index 頁面的時候,需要驗證這些修改。因此,需要在頁面的表格中增加一列,在這里不需要使用 ViewBag,因為需要的信息已經通過教師實體 Instructor 的導航屬性 Courses 傳遞到頁面了。
在 Views\Instuctor\Index.cshtml 中,在 <th>Office</th> 之后,增加一個 <th>Course</th> 的標題列,如下所示。
<tr> <th></th> <th>Last Name</th> <th>First Name</th> <th>Hire Date</th> <th>Office</th> <th>Courses</th> </tr>
然后,在辦公位置的單元格之后增加一個詳細內容的單元格。
<td> @{ foreach (var course in item.Courses) { @course.CourseID @: @course.Title <br /> } } </td>
運行 Instructor 的 Index 頁面,檢查教師的授課情況。
點擊 Edit 查看編輯頁面。
修改一些課程的授課情況,然后保存 Save,修改的結果在 Index 頁面中可以看到。
你已經完成了修改關聯數據的工作。通過目前的課程你已經可以完成增、刪、改、查所有的操作,但是還沒有考慮並發問題。下一次我們將探討並發問題,展示處理並發的方式,然后對已經完成的實體的增、刪、改、查的代碼增加並發處理。