目錄
Contoso 大學 - 使用 EF Code First 創建 MVC 應用
在上一個課程中,你已經創建了 MVC 應用,使用 EF 和 SQL Server Compact 保存和顯示數據。在這個課程中,你將要復習並定制 MVC 腳手架為你的控制器和視圖自動創建的 CRUD (創建、讀取、更新和刪除)代碼。注意:為了在你的控制器和數據訪問層之間進行抽象,通常的做法是實現倉儲模式。為了保持這個課程的簡潔,在這個系列的最后課程之前,我們不會實現倉儲模式。
在這個課程中,你將要創建如下的頁面。
2-1 創建詳細頁
腳手架創建的代碼遺留下了學生注冊屬性沒有處理,因為這個屬性是集合屬性。在詳細頁面中,你將要在 HTML 表格中顯示這個集合的內容。
在 Controllers\StudentController.cs 中,詳細頁面的 Action 方法類似如下的代碼:
public ViewResult Details(int id)
{
Student student = db.Students.Find(id);
return View(student);
}
代碼使用 Find 方法來獲取單個的 Student 實體,使用傳遞給方法的 id 關鍵字。Id 來自 Index 頁面中的超級鏈接提供的查詢字符串。
打開 Views\Student\Details.cshtml。每個字段使用 DisplayFor 助手方法進行顯示,類似如下所示:
<div class="display-label">LastName</div>
<div class="display-field">
@Html.DisplayFor(model => model.LastName)
</div>
為了顯示注冊課程列表,在注冊日期 EnrollmentDate
字段之后,fieldset 結束標記之前,增加如下的代碼。
<div class="display-label">
@Html.LabelFor(model => model.Enrollments)
</div>
<div class="display-field">
<table>
<tr>
<th>Course Title</th>
<th>Grade</th>
</tr>
@foreach (var item in Model.Enrollments)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Course.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.Grade)
</td>
</tr>
}
</table>
</div>
代碼遍歷導航屬性 Enrollments
中所有的實體,對於這個屬性中的每一個 Enrollment 實體,將顯示其中的課程和年級。課程標題通過 Enrollments
導航屬性保存的 Course
實體來獲得。在需要的時候,所有的數據從數據庫中獲取。( 從另外一個角度說,在這里使用了延遲加載,你沒有為 Courses 導航屬性指定餓漢模式加載,所以,在你第一次試圖訪問這個屬性的時候,將會向數據庫發出一次查詢來獲取數據,你可以在這個系列后面的 讀取相關數據 部分獲取更加詳細的說明 )
運行這個頁面,選擇 Students 選項卡,然后點擊 Details 超級鏈接,你就可以看到課程的列表。
2-2 建立創建頁面
在 Controllers\StudentController.cs,使用下面的代碼替換HttpPost
Create
這個 Action 方法,為腳手架創建的代碼增加 try-catch 塊。
[HttpPost]
public ActionResult Create(Student student)
{
try
{
if (ModelState.IsValid)
{
db.Students.Add(student);
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(student);
}
這些代碼將通過 ASP.NET MVC 模型綁定創建的實體對象加入到 Students 集合中,然后保存修改到數據庫中。( 模型綁定是 ASP.NET MVC 的一個功能用於簡化你獲取通過表單提交的數據,模型綁定轉換提交的表單數據到 .NET 中的數據類型,通過 Action 方法的參數傳遞進來,在這里,模型綁定通過表單數據為你實例化了一個 Student 的實體對象實例 )
這里的 try-catch 塊是這些代碼區別於腳手架創建的代碼的唯一不同之處,在保存數據的時候,如果派生自DataException 的異常被拋出,錯誤信息將會被顯示出來,這類錯誤典型的是由於一些內部錯誤,而不是程序錯誤,所以建議用戶再重新試一次。在 Views\Student\Create.cshtml 中的代碼與 Details.cshtml 中類似,除了將每個字段的 DisplayFor 代替為EditorFor
和 ValidationMessageFor 助手方法.下面的示例演示了相關的代碼。
<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>
在 Create.cshtml. 中不需要進行修改。
重新運行頁面,選擇 Students選項卡,點擊 Create New。
默認會進行數據驗證,輸入名字和一個錯誤的日期,然后點擊 Create,查看一下錯誤提示。
現在,你會看到通過 JavaScript 實現的客戶端驗證,但是,服務器端的驗證也已經實現了。即使客戶端驗證失敗了,壞的數據也會被捕獲到,在服務器端會拋出一個異常。
將日期修改為一個正確的日期,例如:9/1/2005,然后點擊 Create,會看到一個新的學生出現在 Index頁面上。
2-3 創建一個編輯頁面
在 Controllers\StudentController.cs 中,Http Edit 方法 ( 其中沒有使用 HttpPost標簽的那一個 ) 使用 Find 方法來獲取選中的學生實體,像你在 Details 方法中看到的一樣,不需要修改這個方法。
實際上,需要修改 HttpPost Edit 方法,使用下面的代碼為它增加 try-catch處理。
[HttpPost]
public ActionResult Edit(Student student)
{
try
{
if (ModelState.IsValid)
{
db.Entry(student).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(student);
}
這段代碼非常類似在 Create 中的代碼,實際上,除了將通過模型綁定創建的對象添加到實體集中,這段代碼還設置了實體的標志來表示這個實體已經被修改過了。當 SaveChanges方法被調用的時候,數據庫中行的所有字段都將會更新。包括用戶沒有修改過的字段,並發沖突被忽略掉 ( 你將會在這個系列的后面學習如何處理並發問題 )。
實體的狀態,連接以及 SaveChanges 方法
數據庫上下文對象維護內存中的對象與數據庫中數據行之間的同步。這些信息在調用 SaveChanges方法被調用的時候使用。例如,當使用 Add 方法傳遞一個新的實體對象時,實體的狀態被設置為 Added,在調用 SaveChanges方法的時候,數據庫上下文使用 SQL 命令 Insert來插入數據。
實體的狀態可能為如下之一:
Added. 實體在數據庫中不存在。SaveChanges 方法必須執行 Insert 命令
Unchanged. 在調用 SaveChanges 的時候不需要做任何事情,當從數據庫讀取數據的時候,實體處於此狀態。
Modified. 某些或者全部的實體屬性被修改過. SaveChanges方法需要執行 Update 命令。
Deleted. 實體標記為已刪除,SaveChanges 方法必須執行 Delete 命令。
Detached. 實體的狀態沒有被數據庫上下文管理。
在桌面應用中,實體的狀態改變典型地自動完成。在這種類型的應用中,你讀取一個實體,然后修改某些屬性的值,這使得實體的狀態被自動修改為 Modified。然后,在調用 SaveChanged 的時候,實體框架生成 Update 命令進行更新,只有你修改的實際屬性被更新。
但是,在 Web 應用程序中,這個處理序列不是連續的。因為數據庫上下文讀取的實體對象實例在頁面被呈現之后被丟棄了。當 HttpPost Edit 方法被調用的時候,導致一個新的請求和一個新的數據庫上下文對象被生成,所以,你必須手工設置實體的狀態為 Modified。然后調用 SaveChanges 方法,實體框架更新數據庫中數據行的所有列,因為數據庫上下文沒有辦法知道你修改了哪些屬性。
如果你希望 Update 語句僅僅更新你實際上修改的字段。你可以通過某種途徑保存原有的數據值 ( 例如通過隱藏域 ),以便在 HttpPost Edit 方法被調用的時候這些值存在。然后,可以使用原始的數據來創建一個 Student 實體,使用 Attach 方法調用連接含有原始值的實體對象,然后,使用新的值來更新實體對象,最后再調用 SaveChanges 方法,更多的詳細內容,可以查看 EF 團隊的博客: Add/Attach and Entity States和Local Data。
在 Views\Student\Edit.cshtml中的代碼類似於 Create.cshtml ,不需要修改。
運行頁面,選擇 Students 選項卡,然后點擊 Edit 超級鏈接。
修改一些數據,然后點擊 Save,可以在 Index 頁面看到修改后的數據。
2-4 創建刪除頁面
在 Controllers\StudentController.cs, 模板生成的 HttpGet Delete 方法使用 Find 方法來獲取 Student 實體,像在Details 和 Edit 方法中一樣。實際上,為了實現在調用 SaveChanges 方法失敗的時候使用的錯誤頁面,你需要為這個方法和相應的視圖增加一些功能。
像在更新和創建操作中一樣,刪除操作也需要兩個方法。GET 方法用於顯示一個視圖,使用戶可以允許或者取消刪除操作,如果用戶允許刪除操作,那么,將會發出一個 Post 請求,HttpPost Delete 方法將會被調用,然后執行實際的刪除操作。
你需要為 HttpPost Delete 方法增加一個 try-catch 塊來捕獲數據庫更新過程中的任何異常。如果錯誤出現,HttpPost Delete 方法調用 HttpGet Delete 方法,傳遞一個參數表示錯誤發生了。HttpGet Delete 方法就會根據錯誤信息重新顯示確認頁面,使用戶可以取消或者重試。
使用下面的代碼替換 HttpGet Delete 方法中的代碼,這里會管理錯誤報告。
public ActionResult Delete(int id, bool? saveChangesError)
{
if (saveChangesError.GetValueOrDefault())
{
ViewBag.ErrorMessage = "Unable to save changes. Try again, and if the problem persists see your system administrator.";
}
return View(db.Students.Find(id));
}
這段代碼接收一個可選的 bool 類型參數,這個參數用來表示是在更新失敗之后調用這個方法。在頁面請求中被調用的時候,這個參數為 null ( false ),當通過 HttpPost Delete 方法更新數據庫失敗后,被調用的時候,參數被設置為 true,錯誤信息被傳遞到視圖中。
將 HttpPost Delete 方法 ( 名為 DeleteConfirmed 方法 )的代碼替換成下面的代碼,這將會執行實際的刪除操作,並捕獲任何數據庫更新錯誤。
[HttpPost, ActionName("Delete")]
public ActionResult DeleteConfirmed(int id)
{
try
{
Student student = db.Students.Find(id);
db.Students.Remove(student);
db.SaveChanges();
}
catch (DataException)
{
//Log the error (add a variable name after DataException)
return RedirectToAction("Delete",
new System.Web.Routing.RouteValueDictionary {
{ "id", id },
{ "saveChangesError", true } });
}
return RedirectToAction("Index");
}
這段代碼獲取選中的實體,然后調用 Remove 方法將實體的狀態設置為 Deleted。當調用 SaveChanged 的時候,SQL 命令 Delete 被生成並執行。
如果性能是應用的高優先級目標,你可以省略掉不需要的 SQL 查詢處理,使用下面的代碼行調用 Find 和 Remove 方法。
Student studentToDelete = new Student() { StudentID = id };
db.Entry(studentToDelete).State = EntityState.Deleted;
這段代碼實例化了一個 Student 實體,僅僅設置了主鍵的值,然后將實體的狀態設置為 Deleted。EF 刪除實體僅僅需要這些信息。
需要注意的是,HttpGet Delete 方法並不真正刪除數據,在 GET 請求處理中進行刪除存在着安全風險 ( 同樣在進行編輯,創建,或者任何修改數據的處理中 ),更多的資料,參見:ASP.NET MVC Tip #46 — Don't use Delete Links because they create Security Holes
在 Views\Student\Delete.cshtml 中,在 h2 和 h3 之間增加下面的代碼:
<p class="error">@ViewBag.ErrorMessage</p>
運行頁面,選擇 Students 選項卡,點擊 Delete 超級鏈接。
點擊 Delete,Index 頁面中就不會再顯示被刪除的學生了。( 在處理並發的部分可以看到錯誤處理 )
2-5 確認數據庫連接沒有忘記關閉
為了確認數據庫連接被正確關閉,以及資源被正確釋放,你需要釋放數據庫上下文,這就是為什么在 StudentController 代碼的最后會看到 Dispose 方法的原因,在 StudentController.cs, 如下所示:
protected override void Dispose(bool disposing)
{
db.Dispose();
base.Dispose(disposing);
}
基類 Controller 已經實現了接口 IDisposable,所以這段代碼簡單地重寫了 Dispose ( bool ) 方法來顯式釋放上下文對象。
你現在已經有了一套完整的頁面對 Student 進行增、刪、改、查處理。在下一課程中,將會為 Index 頁面增加排序和分頁功能。