為ASP.NET MVC應用程式讀取相關資料
這是微軟官方教程Getting Started with Entity Framework 6 Code First using MVC 5 系列的翻譯,這裡是第八篇:為ASP.NET MVC應用程式更新相關資料
原文: Updating Related Data with the Entity Framework in an ASP.NET MVC Application
在之前的教程中您已經成功顯示了相關資料。在本教程中你將學習如何對相關資料進行更新。對於大多數關係,可以從主鍵或者導覽屬性來進行更新。對於多對多關係,Entity Framework不會直接公開串連表,所以你可以從相應的導覽屬性添加和移除實體。
為課程自訂建立和編輯頁
當建立新的課程實體時,他必須擁有一個和已存在系的關係。為此,腳手架代碼建立的控制器方法及建立和編輯檢視種豆包含了用於選擇系的下拉式清單。下拉式清單用來設定Course.DepartmentID外鍵屬性,這對於Entity Framework通過Department導覽屬性來載入Department實體是必須的。你將使用腳手架代碼,但需要對其做一些小的改動來增加錯誤處理和對列表內容進行排序。
在Coursecontroller.cs中,刪除之前的Create和Edit方法,並添加下面的代碼:
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);}
在檔案的開頭增加以下引用:
using System.Data.Entity.Infrastructure;
PopulateDepartmentsDropDownList方法擷取所有的系列表並按照名稱進行排序來建立一個下拉式清單。並通過ViewBag屬性傳遞到視圖上。該方法接收一個選擇性參數selectedDepartment,在下拉式清單渲染時允許調用代碼指定被選擇的項目。視圖將傳遞DepartmentID名稱給下拉式清單協助器,然後協助器知道應當使用DepartmentID名來在ViewBag中對象進行下拉式清單的尋找。
HttpGet Create方法調用PopulateDepartmentsDropDownList方法,但並不設定已選項目,因為對於一個新的課程來說,尚未確定其所屬的系。
public ActionResult Create(){ PopulateDepartmentsDropDownList(); return View();}
HttpGetEdit方法設定所選的項目,基於已經分配給正在編輯的課程的系ID:
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);}
Create和Edit的HttpPost方法還包括當出現了錯誤後,重新顯示頁面時要再設定一次所選項目的代碼:
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);
這段代碼確保當頁面重新顯示錯誤資訊時,已經被選擇的系保持被選擇狀態。
Course視圖已經基於系欄位來使用腳手架構建了一個下拉式清單。但你並不想使用系ID來作為標題,所以在Views\Course\Create.cshtml中進行以下高亮部分的更改:
@model ContosoUniversity.Models.Course@{ ViewBag.Title = "Create";}<h2>Create</h2>@using (Html.BeginForm()) { @Html.AntiForgeryToken() <div class="form-horizontal"> <h4>Course</h4> <hr /> @Html.ValidationSummary(true) <div class="form-group"> @Html.LabelFor(model => model.CourseID, new { @class = "control-label col-md-2" }) <div class="col-md-10"> @Html.EditorFor(model => model.CourseID) @Html.ValidationMessageFor(model => model.CourseID) </div> </div> <div class="form-group"> @Html.LabelFor(model => model.Title, new { @class = "control-label col-md-2" }) <div class="col-md-10"> @Html.EditorFor(model => model.Title) @Html.ValidationMessageFor(model => model.Title) </div> </div> <div class="form-group"> @Html.LabelFor(model => model.Credits, new { @class = "control-label col-md-2" }) <div class="col-md-10"> @Html.EditorFor(model => model.Credits) @Html.ValidationMessageFor(model => model.Credits) </div> </div> <div class="form-group"> <label class="control-label col-md-2" for="DepartmentID">Department</label> <div class="col-md-10"> @Html.DropDownList("DepartmentID", String.Empty) @Html.ValidationMessageFor(model => model.DepartmentID) </div> </div> <div class="form-group"> <div class="col-md-offset-2 col-md-10"> <input type="submit" value="Create" class="btn btn-default" /> </div> </div> </div>}<div> @Html.ActionLink("Back to List", "Index")</div>@section Scripts { @Scripts.Render("~/bundles/jqueryval")}
之後在Edit視圖中進行相同的更改。
通常腳手架不會使用主鍵來產生欄位,因為主索引值是由資料庫產生的,無法更改且對使用者顯示也沒有意義。對於課程實體腳手架程式碼封裝含了一個用於CourseID的文字框,因為DatabaseGeneratedOption.None特性意味著使用者應當可以輸入主索引值。但它並不明白因為該號碼只有在你想要讓其顯示在某些特定視圖中才是有意義的。所以您需要手動添加它。
在Edit視圖中,在標題欄位之前添加課程編號欄位。
<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>
Edit視圖中已經有一個課程編號的隱藏欄位(Html.HiddenFor協助器)。為隱藏欄位添加一個Html.LabelFor協助器是沒必要的。因為它不會導致當使用者點擊儲存時將課程編號包含在要發送的資料中。
在Delete和Details視圖中,更改系名稱的標題從”Name”到”Department”並在標題欄位之前添加一個課程編號欄位。
<dt> Department</dt><dd> @Html.DisplayFor(model => model.Department.Name)</dd><dt> @Html.DisplayNameFor(model => model.CourseID)</dt><dd> @Html.DisplayFor(model => model.CourseID)</dd>
運行應用程式,開啟課程的建立頁面(顯示課程索引頁面並單擊建立新的)並輸入新課程的資料:
單擊建立,課程索引頁會顯示你剛才建立的課程。同時索引頁面的洗名稱是來自導覽屬性的,表示關係已經正確建立。
點擊編輯超連結來運行編輯頁。
為講師添加編輯頁面
當您編輯一名講師的記錄時,你希望能夠更新講師的辦公室分配情況。講師實體和辦公室分配實體之間有一個一到零或一的關係。這意味著您必須處理下列情況: 如果使用者清除了辦公室分配情況並且講師原來擁有一個,您必須移除並刪除這個OfficeAssignment實體。 如果使用者輸入了一個辦公室並且原來講師並沒有分配,您必須建立一個OfficeAssignment實體。 如果使用者更改辦公室分配值,你必須更改已經存在的OfficeAssignment實體。
開啟InstructorController.cs,檢查Edit的HttpGet 方法:
{ if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } Instructor instructor = db.Instructors.Find(id); if (instructor == null) { return HttpNotFound(); } ViewBag.ID = new SelectList(db.OfficeAssignments, "InstructorID", "Location", instructor.ID); return View(instructor);}
腳手架產生的程式碼並不是你想要的。它設定了一個下拉式清單,但你需要一個文半框。使用下面的代碼替換原來的:
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);}
這段代碼刪除了ViewBag語句並針對關聯的OfficeAssignment實體添加了積極式載入的。你不能在Find方法上使用積極式載入。所以這裡使用了Where和Single方法來選擇講師。
下面的代碼替換HttpPost的Edit方法。用來處理辦公室分配更新:
[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);}
然後添加下列引用:
using System.Data.Entity.Infrastructure;
這段代碼執行了以下操作: 將方法名稱變更為EditPost因為簽名現在和HttpGet方法的一樣。(依然使用ActionName特性指定的Edit URL) 使用消極式載入來從資料庫中擷取當前講師實體的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;}
將所做的更改儲存到資料庫中。
在Edit視圖中,在僱傭日期欄位的div元素之後,添加一個新的欄位來編輯辦公室地址:
<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>
運行該頁面(選擇教師選項卡,然後點擊編輯講師),更改辦公室位置並儲存。
為教師編輯頁面添加課程分配
教師能夠教授任意數量的課程。現在您會通過使用一組複選框來添加更改課程分配的功能,如下所示:
Course和Instructor實體之間的關係是多對多,這意味著您不需要直接存取串連表中的外鍵屬性。相反,你可以從Istructor.Courses導覽屬性中添加和移除實體。
UI使您能夠更改使用一組複選框來表示哪些課程是已經分配給教師的。在資料庫中的每一門課程都使用一個複選框來顯示,包括哪些已經分配給教師的。使用者可以通過選擇或清除複選框來更改課程分配。如果課程數目太多,你可能想要在視圖中使用不同的顯示資料的方法,但你會用同樣的方法來操作導覽屬性以建立或刪除關係。
為了給視圖提供複選框的列表,您會使用ViewModel類,在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){ 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;}
該代碼對Courses導覽屬性進行了積極式載入,並且調用了一個新的PopulateAssignedCourseData方法使用AssignedCourseData視圖模型類來為複選框數組提供資訊。
PopulateAssignedCourse方法中的代碼通過讀取所有Course實體並使用模型視圖類以載入列表。在每個課程中,代碼檢查講師的Courses導覽屬性中是否存在該課程。為了建立一個高效的檢查一個課程是否指派給教師,已經分配的課程被放入一個HashSet集合。當課程已指派時,Assigned屬性為True。視圖會使用該屬性來確定哪些複選框應當顯示為已選定。最後,該列表作為ViewBag屬性被傳遞到視圖上。
下一步,添加使用者單擊儲存時應當執行的代碼。調用一個新方法來更新Instructor實體的Courses導覽屬性,使用下面的代碼替換EditPost方法,高亮部分是你進行的更改:
[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); } } }}
由於現在方法簽名和HttpGet的Edit方法不同,所以該方法的名稱也從EditPost返回到Edit。
由於視圖沒有課程實體的集合,所以模型繫結器不能自動更新Courses導覽屬性。不同於使用模型繫結器來更新Course導覽屬性,你將在UpdateInstructorCourses方法中進行更新。因此,您需要將Course屬性從模型繫結器中排除。這不需要更改任何代碼,因為你正在使用的白名單重載列表中沒有包含Courses。
如果沒有複選框被選中,UpdateInstructorCourses中的代碼使用一個空集合來初始化Courses導覽屬性。
if (selectedCourses == null){ instructorToUpdate.Courses = new List<Course>(); return;}
該代碼通過迴圈資料庫中的所有課程,檢查哪些課程是分配給教師的來決定是否在視圖中應當選中它們。為了進行高效尋找,它們都儲存在HashSet對象中。
如果某個課程的複選框被選中但該課程並不在Instructor.Courses導覽屬性中,課程將被添加到導覽屬性的集合。
if (selectedCoursesHS.Contains(course