Contoso University示例網站演示如何使用Entity Framework 5創建ASP.NET MVC 4應用程序。Entity Framework有三種處理數據的方式: Database First, Model First, and Code First. 本指南使用代碼優先。其它方式請查詢資料。示例程序是爲Contoso University建立一個網站。功能包括:學生管理、課程創建、教師分配。 本系列指南逐步講述如何實現這一網站程序。
如有問題,可在這些討論區提問: ASP.NET Entity Framework forum, the Entity Framework and LINQ to Entities forum, or StackOverflow.com.
上一節完成了相關聯數據的顯示,本節將學習如何更新關聯數據。大部分關聯關係可通過更新相應的外鍵來完成。對於多對多關係,EF沒有直接暴漏連接表,需要顯式的操作導航屬性(向其中添加、移除實體)來完成。
將要完成的效果如下:
定製課程的 Create 和Edit 頁面
課程實體創建後是和某個部門有關聯的。爲了展示這一點,自動生成的代碼生成了相應的控制器方法以及創建、編輯視圖,其中包括可選擇部門的下拉列表。下拉列表設置 Course.DepartmentID外鍵屬性,這樣EF就可以正確加載Department 導航屬性的對應實體。這裏只簡單修改代碼,增加錯誤處理和下拉列表排序功能。
在 CourseController.cs, Edit 和 Create 方法修改後代碼如下:
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 (DataException /* dex */) { //Log the error (uncomment dex variable name after DataException 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) { Course course = db.Courses.Find(id); PopulateDepartmentsDropDownList(course.DepartmentID); return View(course); } [HttpPost] [ValidateAntiForgeryToken] public ActionResult Edit( [Bind(Include = "CourseID,Title,Credits,DepartmentID")] Course course) { try { if (ModelState.IsValid) { db.Entry(course).State = EntityState.Modified; db.SaveChanges(); return RedirectToAction("Index"); } } catch (DataException /* dex */) { //Log the error (uncomment dex variable name after DataException 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); } 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 方法獲取按名排列的部門列表, 爲下拉列表構建一個SelectList 集合,使用 ViewBag 屬性將其傳遞到視圖.方法有一個可選參數selectedDepartment 以便設置下拉列表默認值. 視圖將把DepartmentID 傳遞給DropDownList 幫助器, 幫助器從 ViewBag 中尋找名爲DepartmentID的 SelectList.
HttpGet Create 調用 PopulateDepartmentsDropDownList 方法時不使用默認值,因爲此時還沒有創建新課程數據:
public ActionResult Create() { PopulateDepartmentsDropDownList(); return View(); }
HttpGet Edit 方法則設置默認值,因爲此時課程在編輯時有原始的部門信息:
public ActionResult Edit(int id) { Course course = db.Courses.Find(id); PopulateDepartmentsDropDownList(course.DepartmentID); return View(course); }
HttpPost 方法在捕獲異常之後再次顯示創建或編輯頁面時,初始化下拉列表默認值:
catch (DataException /* dex */) { //Log the error (uncomment dex variable name after DataException 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);
代碼確保如果發生異常返回頁面時,原有的操作數據還在.
在 Views\Course\Create.cshtml, 在 Title 域之前添加代碼,提供錄入課程編號的編輯域。之前曾經介紹過,自動生成代碼不會保護對主鍵的編輯域.
@model ContosoUniversity.Models.Course
@{
ViewBag.Title = "Create";
}
<h2>Create</h2>
@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
@Html.ValidationSummary(true)
<fieldset>
<legend>Course</legend>
<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>
<div class="editor-label">
@Html.LabelFor(model => model.Title)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Title)
@Html.ValidationMessageFor(model => model.Title)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.Credits)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Credits)
@Html.ValidationMessageFor(model => model.Credits)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.DepartmentID, "Department")
</div>
<div class="editor-field">
@Html.DropDownList("DepartmentID", String.Empty)
@Html.ValidationMessageFor(model => model.DepartmentID)
</div>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
}
<div>
@Html.ActionLink("Back to List", "Index")
</div>
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}
在 Views\Course\Edit.cshtml, Views\Course\Delete.cshtml, 和Views\Course\Details.cshtml, 添加如下代碼
<div class="editor-label"> @Html.LabelFor(model => model.CourseID) </div> <div class="editor-field"> @Html.DisplayFor(model => model.CourseID) </div>
運行 Create 頁面:
點擊Create. Index頁面將顯示新創建的課程信息。列表中的部門名稱來自於導航屬性,說明正確建立了關聯。
運行 Edit 頁面.
修改數據,點擊 Save. 索引頁面顯示更新後的數據.
爲 Instructors添加編輯頁面
編輯instructor 記錄時希望同時更新所在辦公室信息. Instructor 和 OfficeAssignment 實體存在一對零或一的關係, 也就是要處理以下場景:
- 如果之前有辦公室信息,編輯時將其去除,需要刪除OfficeAssignment 實體.
- 如果之前沒有辦公室信息,編輯時添加了,需要創建OfficeAssignment 實體.
- 如果編輯了辦公室信息,需要更新OfficeAssignment 實體.
在 InstructorController.cs , 自動生成的HttpGet Edit 方法代碼如下:
public ActionResult Edit(int id = 0) { Instructor instructor = db.Instructors.Find(id); if (instructor == null) { return HttpNotFound(); } 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) .Where(i => i.InstructorID == id) .Single(); return View(instructor); }
代碼通過貪婪加載方式獲取OfficeAssignment 實體。Find 方法無法使用貪婪加載,因此使用Where 和Single 方法。
將HttpPost Edit 方法替換爲如下代碼:
[HttpPost] [ValidateAntiForgeryToken] public ActionResult Edit(int id, FormCollection formCollection) { var instructorToUpdate = db.Instructors .Include(i => i.OfficeAssignment) .Where(i => i.InstructorID == id) .Single(); if (TryUpdateModel(instructorToUpdate, "", new string[] { "LastName", "FirstMidName", "HireDate", "OfficeAssignment" })) { try { if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment.Location)) { instructorToUpdate.OfficeAssignment = null; } db.Entry(instructorToUpdate).State = EntityState.Modified; db.SaveChanges(); return RedirectToAction("Index"); } catch (DataException /* dex */) { //Log the error (uncomment dex variable name after DataException 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."); } } ViewBag.InstructorID = new SelectList(db.OfficeAssignments, "InstructorID", "Location", id); return View(instructorToUpdate); }
這部分代碼的作用是:
-
從數據庫通過貪婪加載獲取Instructor 和 OfficeAssignment實體. 這是和 HttpGet Edit 方法一樣的.
-
使用模型綁定器數據更新 Instructor 實體. TryUpdateModel 更新白名單中的屬性值,關於白名單的介紹在指南的第二節..
if (TryUpdateModel(instructorToUpdate, "", new string[] { "LastName", "FirstMidName", "HireDate", "OfficeAssignment" }))
-
如果辦公室信息爲空,將 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>
運行,測試效果
在Instructor編輯頁面實現課程分配操作
教師可同時承擔多門課程,添加爲教師編輯頁面增加通過組選框設定承擔課程的功能:
Course 和 Instructor 實體之間是多對多關係,意味着無法之間訪問連接表。通過 Instructor.Courses導航屬性中添加或刪除關聯實體的方式來實現關係的維護.
功能通過組選框來實現。列出數據庫中所有課程,通過選擇框確定是否選擇,教師當前承擔的課程處於選中狀態。用戶通過選中或者取消選中的操作修改課程的分配情況. 如果課程數目很多,你可能希望使用別的顯示方法,但操作導航屬性來添加或刪除關係的方法是一樣的。
創建模型類以便爲視圖的組選框提供數據. 在ViewModels 文件夾創建 AssignedCourseData.cs,代碼如下:
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 導航屬性,調用PopulateAssignedCourseData 方法實現爲視圖提供AssignedCourseData 視圖模型的數據.
PopulateAssignedCourseData 方法讀取所有 Course 實體. 對每一個Courses 檢查是否已經存在於導航屬性. 爲了提高效率,將當前承擔課程的ID形成一個 HashSet 集合. 承擔課程的 Assigned 屬性將設爲 true . 視圖將使用此屬性決定哪些選擇框處於被選中狀態. 最後通過ViewBag 的一個屬性將列表傳遞到視圖.
下一步,完成保存代碼。
使用如下代碼替換 HttpPost Edit 方法的代碼, 調用一個新的方法更新Instructor 實體的 Courses 導航屬性.
[HttpPost] [ValidateAntiForgeryToken] 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, "", new string[] { "LastName", "FirstMidName", "HireDate", "OfficeAssignment" })) { 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 /* dex */) { //Log the error (uncomment dex variable name after DataException 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); } } } }
視圖不包含Course 實體集合, 因此模型綁定器不能直接更新Courses 導航屬性. 更新由 UpdateInstructorCourses 方法完成. 因此要把 Courses屬性從模型綁定器中排除出去. 這並不需要修改 TryUpdateModel 的代碼,因爲使用了白名單, Courses 不在名單之內.
如果沒有選中任何課程, UpdateInstructorCourses 將 Courses 導航屬性設爲一個空的列表:
if (selectedCourses == null) { instructorToUpdate.Courses = new List<Course>(); return; }
代碼執行循環檢查數據庫中的每一課程,若此課程被選中則判斷是否已經包含在相關數據中,如果沒有則添加到導航屬性。爲了提高效率,把選中課程Id和已有課程ID放在哈希表中。
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, 添加如下高亮代碼,在OfficeAssignment 之後增加選中 Courses 的組選框。
@model ContosoUniversity.Models.Instructor @{ ViewBag.Title = "Edit"; } <h2>Edit</h2> @using (Html.BeginForm()) { @Html.AntiForgeryToken() @Html.ValidationSummary(true) <fieldset> <legend>Instructor</legend> @Html.HiddenFor(model => model.InstructorID) <div class="editor-label"> @Html.LabelFor(model => model.LastName) </div> <div class="editor-field"> @Html.EditorFor(model => model.LastName) @Html.ValidationMessageFor(model => model.LastName) </div> <div class="editor-label"> @Html.LabelFor(model => model.FirstMidName) </div> <div class="editor-field"> @Html.EditorFor(model => model.FirstMidName) @Html.ValidationMessageFor(model => model.FirstMidName) </div> <div class="editor-label"> @Html.LabelFor(model => model.HireDate) </div> <div class="editor-field"> @Html.EditorFor(model => model.HireDate) @Html.ValidationMessageFor(model => model.HireDate) </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> <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> <p> <input type="submit" value="Save" /> </p> </fieldset> } <div> @Html.ActionLink("Back to List", "Index") </div> @section Scripts { @Scripts.Render("~/bundles/jqueryval") }
This code creates an HTML table that hasthree columns. In each column is a check box followed by a caption thatconsists of the course number and title. 創建了一個三列表格,每一列包含複選框、課程編號和名稱。所有複選框的名字都是一樣的("selectedCourses"), 模型綁定器由此得知將其作爲一組信息來處理. 單選框的 value 設爲對於課程的 CourseID. 當編輯提交之後,模型綁定器將被選中的複選框的值組合爲一個數組傳給控制器。
在 Views\Instructor\Index.cshtml, 在Office 列之後添加 Courses :
<tr> <th></th> <th>Last Name</th> <th>First Name</th> <th>Hire Date</th> <th>Office</th> <th>Courses</th> </tr>
修改視圖代碼:
@model ContosoUniversity.ViewModels.InstructorIndexData @{ ViewBag.Title = "Instructors"; } <h2>Instructors</h2> <p> @Html.ActionLink("Create New", "Create") </p> <table> <tr> <th></th> <th>Last Name</th> <th>First Name</th> <th>Hire Date</th> <th>Office</th> <th>Courses</th> </tr> @foreach (var item in Model.Instructors) { string selectedRow = ""; if (item.InstructorID == ViewBag.InstructorID) { selectedRow = "selectedrow"; } <tr class="@selectedRow" valign="top"> <td> @Html.ActionLink("Select", "Index", new { id = item.InstructorID }) | @Html.ActionLink("Edit", "Edit", new { id = item.InstructorID }) | @Html.ActionLink("Details", "Details", new { id = item.InstructorID }) | @Html.ActionLink("Delete", "Delete", new { id = item.InstructorID }) </td> <td> @item.LastName </td> <td> @item.FirstMidName </td> <td> @String.Format("{0:d}", item.HireDate) </td> <td> @if (item.OfficeAssignment != null) { @item.OfficeAssignment.Location } </td> <td> @{ foreach (var course in item.Courses) { @course.CourseID @: @course.Title <br /> } } </td> </tr> } </table> @if (Model.Courses != null) { <h3>Courses Taught by Selected Instructor</h3> <table> <tr> <th></th> <th>ID</th> <th>Title</th> <th>Department</th> </tr> @foreach (var item in Model.Courses) { string selectedRow = ""; if (item.CourseID == ViewBag.CourseID) { selectedRow = "selectedrow"; } <tr class="@selectedRow"> <td> @Html.ActionLink("Select", "Index", new { courseID = item.CourseID }) </td> <td> @item.CourseID </td> <td> @item.Title </td> <td> @item.Department.Name </td> </tr> } </table> } @if (Model.Enrollments != null) { <h3>Students Enrolled in Selected Course</h3> <table> <tr> <th>Name</th> <th>Grade</th> </tr> @foreach (var item in Model.Enrollments) { <tr> <td> @item.Student.FullName </td> <td> @Html.DisplayFor(modelItem => item.Grade) </td> </tr> } </table> }
</td>
運行 Instructor Index 查看效果:
點擊 Edit 查看Edit page.
修改一些課程的分配然後點擊 Save. 修改結果在Index頁面展示.
Note: 這種方式在課程數目不多時有效。如果課程數目很多需要修改顯示方式和更新方法。
更新 Delete 方法
修改代碼,當刪除教師時,爲其分配的辦公室信息隨之刪除:
[HttpPost, ActionName("Delete")] [ValidateAntiForgeryToken] public ActionResult DeleteConfirmed(int id) { Instructor instructor = db.Instructors .Include(i => i.OfficeAssignment) .Where(i => i.InstructorID == id) .Single(); instructor.OfficeAssignment = null; db.Instructors.Remove(instructor); db.SaveChanges(); return RedirectToAction("Index"); }
已經完成了完整的CRUD操作,但沒有處理同步問題。下一節將引入同步問題,介紹處理方法,爲CRUD操作添加同步處理。
Entity Framework 相關資源,可查看 the last tutorial in this series.