共用方式為


在 ASP.NET MVC 應用程式中實作 Entity Framework 的基本 CRUD 功能, (2/10)

By Tom Dykstra

Contoso University 範例 Web 應用程式示範如何使用 Entity Framework 5 Code First 和 Visual Studio 2012 建立 ASP.NET MVC 4 應用程式。 如需教學課程系列的資訊,請參閱本系列的第一個教學課程

注意

如果您遇到無法解決的問題, 請下載已完成的章節 ,並嘗試重現您的問題。 一般而言,您可以將程式碼與已完成的程式碼進行比較,以找出問題的解決方案。 如需一些常見的錯誤以及如何解決這些問題,請參閱 錯誤和因應措施。

在上一個教學課程中,您已建立 MVC 應用程式,以使用 Entity Framework 和 SQL Server LocalDB 來儲存和顯示資料。 在本教學課程中,您將檢閱並自訂 CRUD (建立、讀取、更新、刪除) 程式碼,MVC Scaffolding 會自動為您在控制器和檢視中建立的程式碼。

注意

實作儲存機制模式,以在您的控制器及資料存取層之間建立抽象層是一種非常常見的做法。 若要讓這些教學課程保持簡單,在本系列稍後的教學課程之前,您將不會實作存放庫。

在本教學課程中,您將建立下列網頁:

顯示 Contoso University 學生詳細資料頁面的螢幕擷取畫面。

顯示 Contoso University Student Edit 頁面的螢幕擷取畫面。

顯示 Contoso University Student Create 頁面的螢幕擷取畫面。

顯示 [學生刪除] 頁面的螢幕擷取畫面。

建立詳細資料頁面

Students Index 頁面的 Scaffold 程式碼會留下 Enrollments 屬性,因為該屬性會保存集合。 在 Details 頁面中,您會在 HTML 資料表中顯示集合的內容。

Controllers\StudentController.cs中,檢視的 Details 動作方法會使用 Find 方法來擷取單 Student 一實體。

public ActionResult Details(int id = 0)
{
    Student student = db.Students.Find(id);
    if (student == null)
    {
        return HttpNotFound();
    }
    return View(student);
}

索引鍵值會當做 參數傳遞至 方法, id 並來自 [索引] 頁面上 [ 詳細 資料] 超連結中的路由資料。

  1. 開啟 Views\Student\Details.cshtml。 每個欄位都會使用 DisplayFor 協助程式來顯示,如下列範例所示:

    <div class="display-label">
             @Html.DisplayNameFor(model => model.LastName)
        </div>
        <div class="display-field">
            @Html.DisplayFor(model => model.LastName)
        </div>
    
  2. 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>
    </fieldset>
    <p>
        @Html.ActionLink("Edit", "Edit", new { id=Model.StudentID }) |
        @Html.ActionLink("Back to List", "Index")
    </p>
    

    此程式碼會以迴圈逐一巡覽 Enrollments 導覽屬性中的實體。 Enrollment針對 屬性中的每個實體,它會顯示課程標題和成績。 課程標題是從實體的導覽屬性 EnrollmentsCourse 儲存的實體擷取 Course 。 當需要時,所有資料都會從資料庫自動擷取。 (換句話說,您在這裡使用延遲載入。您未指定導覽屬性的 積極式載入Courses ,因此第一次嘗試存取該屬性時,系統會將查詢傳送至資料庫以擷取資料。您可以在本系列稍後的 閱讀相關資料 教學課程中深入瞭解延遲載入和積極式載入。)

  3. 選取 [學生] 索引標籤,然後按一下 [專案 詳細 資料] 連結,以執行頁面。 您會看到選取學生的課程及成績清單:

    Student_Details_page

更新建立頁面

  1. Controllers\StudentController.csHttpPost``Create ,以下列程式碼取代 action 方法,將區塊和Bind 屬性新增 try-catch 至 Scaffolded 方法:

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Create(
       [Bind(Include = "LastName, FirstMidName, EnrollmentDate")]
       Student student)
    {
       try
       {
          if (ModelState.IsValid)
          {
             db.Students.Add(student);
             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.");
       }
       return View(student);
    }
    

    此程式碼會將 Student ASP.NET MVC 模型系結器所建立的實體新增至 Students 實體集,然後將變更儲存至資料庫。 (模型系結器是指 ASP.NET MVC 功能,可讓您更輕鬆地處理表單所提交的資料;模型系結器會將張貼的表單值轉換成 CLR 類型,並將其傳遞至參數中的動作方法。在此情況下,模型系結器會使用 collection.) 的 Form 屬性值,為您具現化 Student 實體

    屬性 ValidateAntiForgeryToken 有助於防止 跨網站要求偽造 攻擊。

> [!WARNING]
    > Security - The `Bind` attribute is added to protect against *over-posting*. For example, suppose the `Student` entity includes a `Secret` property that you don't want this web page to update.
    > 
    > [!code-csharp[Main](implementing-basic-crud-functionality-with-the-entity-framework-in-asp-net-mvc-application/samples/sample5.cs?highlight=7)]
    > 
    > Even if you don't have a `Secret` field on the web page, a hacker could use a tool such as [fiddler](http://fiddler2.com/home), or write some JavaScript, to post a `Secret` form value. Without the [Bind](https://msdn.microsoft.com/library/system.web.mvc.bindattribute(v=vs.108).aspx) attribute limiting the fields that the model binder uses when it creates a `Student` instance*,* the model binder would pick up that `Secret` form value and use it to update the `Student` entity instance. Then whatever value the hacker specified for the `Secret` form field would be updated in your database. The following image shows the fiddler tool adding the `Secret` field (with the value "OverPost") to the posted form values.
    > 
    > ![](implementing-basic-crud-functionality-with-the-entity-framework-in-asp-net-mvc-application/_static/image6.png)  
    > 
    > The value "OverPost" would then be successfully added to the `Secret` property of the inserted row, although you never intended that the web page be able to update that property.
    > 
    > It's a security best practice to use the `Include` parameter with the `Bind` attribute to *allowed attributes* fields. It's also possible to use the `Exclude` parameter to *blocked attributes* fields you want to exclude. The reason `Include` is more secure is that when you add a new property to the entity, the new field is not automatically protected by an `Exclude` list.
    > 
    > Another alternative approach, and one preferred by many, is to use only view models with model binding. The view model contains only the properties you want to bind. Once the MVC model binder has finished, you copy the view model properties to the entity instance.

    Other than the `Bind` attribute, the `try-catch` block is the only change you've made to the scaffolded code. If an exception that derives from [DataException](https://msdn.microsoft.com/library/system.data.dataexception.aspx) is caught while the changes are being saved, a generic error message is displayed. [DataException](https://msdn.microsoft.com/library/system.data.dataexception.aspx) exceptions are sometimes caused by something external to the application rather than a programming error, so the user is advised to try again. Although not implemented in this sample, a production quality application would log the exception (and non-null inner exceptions ) with a logging mechanism such as [ELMAH](https://code.google.com/p/elmah/).

    The code in *Views\Student\Create.cshtml* is similar to what you saw in *Details.cshtml*, except that `EditorFor` and `ValidationMessageFor` helpers are used for each field instead of `DisplayFor`. The following example shows the relevant code:

    [!code-cshtml[Main](implementing-basic-crud-functionality-with-the-entity-framework-in-asp-net-mvc-application/samples/sample6.cshtml)]

    *Create.cshtml* also includes `@Html.AntiForgeryToken()`, which works with the `ValidateAntiForgeryToken` attribute in the controller to help prevent [cross-site request forgery](../../security/xsrfcsrf-prevention-in-aspnet-mvc-and-web-pages.md) attacks.

    No changes are required in *Create.cshtml*.
  1. 選取 [學生] 索引標籤並按一下 [ 新建],以執行頁面。

    Student_Create_page

    根據預設,某些資料驗證的運作方式為 。 輸入名稱和不正確日期,然後按一下 [ 建立 ] 以查看錯誤訊息。

    Students_Create_page_error_message

    下列醒目提示的程式碼會顯示模型驗證檢查。

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Create(Student student)
    {
        if (ModelState.IsValid)
        {
            db.Students.Add(student);
            db.SaveChanges();
            return RedirectToAction("Index");
        }
    
        return View(student);
    }
    

    將日期變更為有效的值,例如 2005/9/1,然後按一下 [ 建立 ] 以查看新學生出現在 [索引 ] 頁面中。

    Students_Index_page_with_new_student

更新編輯 POST 頁面

Controllers\StudentController.csHttpGetEdit ,方法 (沒有 HttpPost 屬性的方法) 使用 Find 方法來擷取選取 StudentDetails 實體,如您在 方法中所見。 您不需要變更這個方法。

不過,將 HttpPostEdit action 方法取代為下列程式碼,以新增 try-catch 區塊和Bind 屬性

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(
   [Bind(Include = "StudentID, LastName, FirstMidName, EnrollmentDate")]
   Student student)
{
   try
   {
      if (ModelState.IsValid)
      {
         db.Entry(student).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.");
   }
   return View(student);
}

此程式碼類似于您在 方法中看到的內容 HttpPostCreate 。 不過,此程式碼不會將模型系結器所建立的實體新增至實體集,而是在實體上設定旗標,指出它已變更。 呼叫 SaveChanges 方法時, Modified 旗標會導致 Entity Framework 建立 SQL 語句來更新資料庫資料列。 將會更新資料庫資料列的所有資料行,包括使用者未變更的資料行,以及忽略並行衝突。 (您將瞭解如何在此系列稍後的教學課程中處理並行處理。)

實體狀態和 Attach 和 SaveChanges 方法

資料庫內容會追蹤實體在記憶體中是否與其在資料庫中相對應的資料列保持同步,並且此項資訊會決定當您呼叫 SaveChanges 方法時會發生什麼事情。 例如,當您將新的實體傳遞至 Add 方法時,該實體的狀態會設定為 Added 。 然後當您呼叫 SaveChanges 方法時,資料庫內容會發出 SQL INSERT 命令。

實體可能處於下列其中一種狀態

  • Added. 實體尚未存在於資料庫中。 方法 SaveChanges 必須發出 INSERT 語句。
  • Unchanged. SaveChanges 方法針對這個實體不需要進行任何動作。 當您從資料庫讀取一個實體時,實體便會以此狀態開始。
  • Modified. 實體中一部分或全部的屬性值已經過修改。 方法 SaveChanges 必須發出 UPDATE 語句。
  • Deleted. 實體已遭標示刪除。 方法 SaveChanges 必須發出 DELETE 語句。
  • Detached. 實體未獲得資料庫內容追蹤。

在桌面應用程式中,狀態變更通常會自動進行設定。 在應用程式的桌面類型中,您會讀取實體,並對其某些屬性值進行變更。 這會使得其實體狀態自動變更為 Modified。 然後當您呼叫 SaveChanges 時,Entity Framework 會產生 UPDATE SQL 語句,只更新您變更的實際屬性。

Web 應用程式的中斷連線本質不允許此連續序列。 讀取實體的 DbCoNtext 會在轉譯頁面之後處置。 HttpPostEdit 呼叫動作方法時,會提出新的要求,而且您有新的DbCoNtext實例,因此您必須在呼叫 SaveChanges 時手動將實體狀態設定為 Modified. Then,Entity Framework 會更新資料庫資料列的所有資料行,因為內容無法知道您變更的屬性。

如果您希望 SQL Update 語句只更新使用者實際變更的欄位,則可以以某種方式儲存原始值 (,例如隱藏欄位) ,以便在呼叫 方法時 HttpPostEdit 使用它們。 然後,您可以使用原始值建立 Student 實體、使用該原始版本的實體呼叫 Attach 方法、將實體的值更新為新的值,然後呼叫 SaveChanges. 如需詳細資訊,請參閱 MSDN Data Developer Center 中的 實體狀態和 SaveChangesLocal Data

Views\Student\Edit.cshtml中的程式碼與您在 Create.cshtml中看到的程式碼類似,不需要變更。

選取 [學生] 索引標籤,然後按一下 [ 編輯 ] 超連結,以執行頁面。

Student_Edit_page

變更一部分的資料,然後按一下 [儲存]。 您會在 [索引] 頁面中看到已變更的資料。

Students_Index_page_after_edit

更新刪除頁面

Controllers\StudentController.cs中,方法的 HttpGetDelete 範本程式碼會使用 Find 方法來擷取選取 Student 的實體,如 您在 和 Edit 方法中所 Details 見。 然而,若要在呼叫 SaveChanges 失敗時實作自訂錯誤訊息,您需要將一些功能新增至此方法及其對應的檢視。

如同您在更新及建立作業中所看到的,刪除作業需要兩個動作方法。 回應 GET 要求時呼叫的方法會顯示檢視,讓使用者有機會核准或取消刪除作業。 若使用者核准,則便會建立 POST 要求。 發生這種情況時, HttpPostDelete 會呼叫 方法,然後該方法實際上會執行刪除作業。

您會將 區塊新增 try-catch 至 方法, HttpPostDelete 以處理資料庫更新時可能發生的任何錯誤。 如果發生錯誤,方法會 HttpPostDelete 呼叫 HttpGetDelete 方法,並傳遞參數,指出發生錯誤。 方法 HttpGet Delete 接著會重新顯示確認頁面以及錯誤訊息,讓使用者有機會取消或再試一次。

  1. HttpGetDelete 動作方法取代為下列程式碼,以管理錯誤報表:

    public ActionResult Delete(bool? saveChangesError=false, int id = 0)
    {
        if (saveChangesError.GetValueOrDefault())
        {
            ViewBag.ErrorMessage = "Delete failed. Try again, and if the problem persists see your system administrator.";
        }
        Student student = db.Students.Find(id);
        if (student == null)
        {
            return HttpNotFound();
        }
        return View(student);
    }
    

    此程式碼接受 選擇性 的 Boolean 參數,指出它是否在儲存變更失敗之後呼叫。 這個參數是在 falseHttpGetDelete 呼叫 方法時,而不會發生先前失敗。 HttpPostDelete 當 方法呼叫以回應資料庫更新錯誤時,參數為 true ,並將錯誤訊息傳遞至檢視。

  2. HttpPostDelete 下列程式碼取代名為 DeleteConfirmed) 的動作方法 (,以執行實際的刪除作業並攔截任何資料庫更新錯誤。

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Delete(int id)
    {
        try
        {
            Student student = db.Students.Find(id);
            db.Students.Remove(student);
            db.SaveChanges();
        }
        catch (DataException/* dex */)
        {
            // uncomment dex and log error. 
            return RedirectToAction("Delete", new { id = id, saveChangesError = true });
        }
        return RedirectToAction("Index");
    }
    

    此程式碼會擷取選取的實體,然後呼叫 Remove 方法,將實體的狀態設定為 Deleted 。 呼叫 時 SaveChanges ,會產生 SQL DELETE 命令。 您也將動作方法的名稱從 DeleteConfirmed 變更為 Delete。 名為 方法 DeleteConfirmed 的 Scaffold 程式碼, HttpPostDelete 為方法提供 HttpPost 唯一的簽章。 ( CLR 需要多載的方法具有不同的方法參數。) 現在簽章是唯一的,您可以繼續使用 MVC 慣例,並針對 HttpPostHttpGet delete 方法使用相同的名稱。

    如果改善大量應用程式中的效能是優先順序,您可以避免不必要的 SQL 查詢,藉由以下列程式碼取代呼叫 FindRemove 方法的程式程式碼,如黃色醒目提示所示:擷取資料列:

    Student studentToDelete = new Student() { StudentID = id };
    db.Entry(studentToDelete).State = EntityState.Deleted;
    

    此程式碼只會使用主鍵值具現化 Student 實體,然後將實體狀態設定為 Deleted 。 這便是 Entity Framework 要刪除實體所需要的一切資訊。

    如所述, HttpGetDelete 方法不會刪除資料。 執行刪除作業以回應 GET 要求 (,或針對該情況執行任何編輯作業、建立作業,或任何其他變更資料作業) 建立安全性風險的作業。 如需詳細資訊,請參閱 ASP.NET MVC 提示 #46 — 請勿使用刪除連結,因為它們 會在 Stephen Walther 的部落格上建立安全性漏洞。

  3. Views\Student\Delete.cshtml中,于標題與 h3 標題之間 h2 新增錯誤訊息,如下列範例所示:

    <h2>Delete</h2>
    <p class="error">@ViewBag.ErrorMessage</p>
    <h3>Are you sure you want to delete this?</h3>
    

    選取 [學生] 索引標籤並按一下 [刪除 ] 超連結,以執行頁面:

    Student_Delete_page

  4. 按一下 [刪除] 。 顯示的 [索引] 頁面將不會包含遭刪除的學生。 (您將在此系列稍後的 處理並行 教學課程中看到錯誤處理常式代碼的運作範例。)

確保資料庫連接未保持開啟狀態

若要確定資料庫連結已正確關閉,以及其保留的資源已釋放,您應該會看到內容實例已處置。 這就是 Scaffolded 程式碼在StudentController.cs類別結尾提供Dispose方法的原因 StudentController ,如下列範例所示:

protected override void Dispose(bool disposing)
{
    db.Dispose();
    base.Dispose(disposing);
}

Controller 類已經實作 IDisposable 介面,因此此程式碼只會將覆寫新增至 Dispose(bool) 方法,以明確處置內容實例。

總結

您現在有一組完整的頁面,可執行實體的簡單 CRUD 作業 Student 。 您使用 MVC 協助程式來產生資料欄位的 UI 元素。 如需 MVC 協助程式的詳細資訊,請參閱 使用 HTML 協助程式 轉譯表單, (頁面適用于 MVC 3,但仍與 MVC 4) 相關。

在下一個教學課程中,您將藉由新增排序和分頁來擴充 [索引] 頁面的功能。

您可以在 ASP.NET 資料存取內容對應中找到其他 Entity Framework 資源的連結。