共用方式為


提供 CRUD (建立、讀取、更新、刪除) 資料表單項目支援

Microsoft 提供

下載 PDF

這是免費的 "NerdDinner" 應用程式教學課程的第 5 步驟,詳細介紹了如何使用 ASP.NET MVC 1 建置一個小型但完整的 Web 應用程式。

步驟 5 示範如何透過啟用對 Dinners 的編輯、建立和刪除支援,來進一步擴展我們的 DiningsController 類別。

如果使用 ASP.NET MVC 3,建議遵循 MVC 3 使用者入門MVC Music 市集教學課程。

NerdDinner 步驟 5:建立、更新、刪除表單案例

我們已介紹了控制器和檢視,並涵蓋如何在網站上使用它們來實作 Dinners 的清單/詳細資料體驗。 下一個步驟是擴展 DinnersController 類別,使其也能支援編輯、建立和刪除 Dinners。

DinnersController 處理的 URL

我們先前已將動作方法新增至對兩個 URL 實作支援的 DinnersController:/Dinners/Dinners/Details/[id]

URL 動詞 用途
/Dinners/ GET 顯示即將推出的 Dinners 的 HTML 清單。
/Dinners/Details/[id] GET 顯示特定 Dinner 的詳細資料。

我們現在將新增動作方法來實作三個額外的 URL:/Dinners/Edit/[id]/Dinners/Create/Dinners/Delete/[id]。 這些 URL 將支援編輯現有的 Dinners、建立新的 Dinners 和刪除 Dinners。

我們將支援使用 HTTP GET 和 HTTP POST 動詞與這些新的 URL 進行互動。 對這些 URL 的 HTTP GET 要求,會顯示資料的初始 HTML 檢視 (在 [編輯] 案例中是填入 Dinner 資料的表單、在 [建立] 案例中是空白表單,和 [刪除] 案例中的刪除確認畫面)。 對這些 URL 的 HTTP POST 要求,將會儲存/更新/刪除 DinnerRepository (以及從該處到資料庫) 中的 Dinner 資料。

URL 動詞 用途
/Dinners/Edit/[id] GET 顯示填入 Dinner 資料的可編輯 HTML 表單。
POST 將特定 Dinner 的表單變更儲存至資料庫。
/Dinners/Create GET 顯示可讓使用者定義新 Dinners 的空白 HTML 表單。
POST 建立新的 Dinner,並將其儲存到資料庫。
/Dinners/Delete/[id] GET 顯示刪除確認畫面。
POST 刪除資料庫中的指定 Dinner。

編輯支援

讓我們先實作 [編輯] 案例。

HTTP-GET 編輯動作方法

我們將先實作編輯動作方法的 HTTP “GET” 行為。 當要求 /Dinners/Edit/[id] URL 時將會叫用此方法。 我們的實作看起來會像這樣:

//
// GET: /Dinners/Edit/2

public ActionResult Edit(int id) {

    Dinner dinner = dinnerRepository.GetDinner(id);
    
    return View(dinner);
}

上述程式碼使用 DinnerRepository 來擷取 Dinner 物件。 然後使用 Dinner 物件轉譯「檢視」範本。 因為我們尚未明確將範本名稱傳遞至 View() 協助程式方法,因此會使用慣例的預設路徑來解析檢視範本:/Views/Dinners/Edit.aspx。

現在讓我們建立這個檢視範本。 我們將在 [編輯] 方法內按一下滑鼠右鍵,然後選取 [新增檢視] 內容功能表命令來執行此動作:

建立檢視範本以在 Visual Studio 中新增檢視的螢幕擷取畫面。

我們將在 [新增檢視] 對話方塊中指出要將 Dinner 物件傳遞至檢視範本做為其模型,並選擇自動生成 [編輯] 範本:

[新增] 檢視以自動生成 [編輯] 範本的螢幕擷取畫面。

當我們按一下 [新增] 按鈕時,Visual Studio 會在 “\Views\Dinners” 目錄中新增一個新的 "Edit.aspx" 檢視範本檔案。 它也會在程式碼編輯器中開啟新的 "Edit.aspx" 檢視範本 – 填入初始的 [編輯] 框架實作,如下所示:

程式碼編輯器內新 [編輯] 檢視範本的螢幕擷取畫面。

讓我們對預設產生的 [編輯] 框架進行一些變更,並更新編輯檢視範本以取得下列內容 (這會移除我們不想公開的一些屬性):

<asp:Content ID="Title" ContentPlaceHolderID="TitleContent" runat="server">
    Edit: <%=Html.Encode(Model.Title)%>
</asp:Content>

<asp:Content ID="Main" ContentPlaceHolderID="MainContent" runat="server">

    <h2>Edit Dinner</h2>

    <%=Html.ValidationSummary("Please correct the errors and try again.") %>  
    
    <% using (Html.BeginForm()) { %>

        <fieldset>
            <p>
                <label for="Title">Dinner Title:</label>
                <%=Html.TextBox("Title") %>
                <%=Html.ValidationMessage("Title", "*") %>
            </p>
            <p>
                <label for="EventDate">EventDate:</label>
                <%=Html.TextBox("EventDate", String.Format("{0:g}", Model.EventDate))%>
                <%=Html.ValidationMessage("EventDate", "*") %>
            </p>
            <p>
                <label for="Description">Description:</label>
                <%=Html.TextArea("Description") %>
                <%=Html.ValidationMessage("Description", "*")%>
            </p>
            <p>
                <label for="Address">Address:</label>
                <%=Html.TextBox("Address") %>
                <%=Html.ValidationMessage("Address", "*") %>
            </p>
            <p>
                <label for="Country">Country:</label>
                <%=Html.TextBox("Country") %>               
                <%=Html.ValidationMessage("Country", "*") %>
            </p>
            <p>
                <label for="ContactPhone">ContactPhone #:</label>
                <%=Html.TextBox("ContactPhone") %>
                <%=Html.ValidationMessage("ContactPhone", "*") %>
            </p>
            <p>
                <label for="Latitude">Latitude:</label>
                <%=Html.TextBox("Latitude") %>
                <%=Html.ValidationMessage("Latitude", "*") %>
            </p>
            <p>
                <label for="Longitude">Longitude:</label>
                <%=Html.TextBox("Longitude") %>
                <%=Html.ValidationMessage("Longitude", "*") %>
            </p>
            <p>
                <input type="submit" value="Save"/>
            </p>
        </fieldset>
        
    <% } %>
    
</asp:Content>

當我們執行應用程式並要求 "/Dinners/Edit/1" URL 時,我們將會看到下列頁面:

[My M V C 應用程式] 頁面的螢幕擷取畫面。

我們的檢視所產生的 HTML 標記如下所示。 它是標準 HTML – 當按下 [儲存]<input type="submit"/> 按鈕時,<表單>項目會執行 /Dinners/Edit/1 URL 的 HTTP POST。 每個可編輯屬性的 HTML <input type="text"/> 項目已經輸出:

產生的 H T M L 標記的螢幕擷取畫面。

Html.BeginForm() 和 Html.TextBox() Html 協助程式方法

我們的 “Edit.aspx” 檢視範本使用數個 "Html Helper" 方法:Html.ValidationSummary()、Html.BeginForm()、Html.TextBox() 和 Html.ValidationMessage()。 除了為我們產生 HTML 標記之外,這些協助程式方法還提供內建的錯誤處理和驗證支援。

Html.BeginForm() 協助程式方法

Html.BeginForm() 協助程式方法是我們標記中輸出 HTML <表單>項目的內容。 在我們的 Edit.aspx 檢視範本中,您會發現在使用此方法時,我們套用了 C# “using” 陳述式。 左大括號表示<表單>內容的開頭,而右大括號表示 </form> 項目的結尾:

<% using (Html.BeginForm()) { %>

   <fieldset>
   
      <!-- Fields Omitted for Brevity -->
   
      <p>
         <input type="submit" value="Save"/>
      </p>
   </fieldset>
   
<% } %>

或者,如果您在類似這樣的案例中發現 "using" 陳述式方法不自然,則可以使用 Html.BeginForm() 和 Html.EndForm() 組合 (執行相同工作):

<% Html.BeginForm();  %>

   <fieldset>
   
      <!-- Fields Omitted for Brevity -->
   
      <p>
          <input type="submit" value="Save"/>
      </p>
   </fieldset>
   
<% Html.EndForm(); %>

呼叫不含任何參數的 Html.BeginForm() 將導致它輸出一個表單項目,該項目對目前要求的 URL 執行 HTTP-POST。 這就是為什麼我們的 [編輯] 檢視會產生 <form action="/Dinners/Edit/1" method="post"> 項目的原因。 如果我們想要張貼到不同的 URL,我們也可以將明確的參數傳遞至 Html.BeginForm()。

Html.TextBox() 協助程式方法

我們的 Edit.aspx 檢視使用 Html.TextBox() 協助程式方法來輸出 <input type="text"/> 項目:

<%= Html.TextBox("Title") %>

上述 Html.TextBox() 方法會採用單一參數 ,用來指定要輸出之 <input type=“text”/> 項目的 id/name 屬性,以及要填入文字方塊值的來源模型屬性。 例如,我們傳遞至 [編輯] 檢視的 Dinner 物件具有 “.NET Futures” 的 “Title” 屬性值,因此 Html.TextBox(“Title”) 方法會呼叫輸出:<input id=“Title” name=“Title” type=“text” value=“.NET Futures” />

或者,我們可以使用第一個 Html.TextBox() 參數來指定該項目的 id/name,然後將要使用的值作為第二個參數傳遞:

<%= Html.TextBox("Title", Model.Title)%>

我們通常會想要對輸出的值執行自訂格式設定。 .NET 內建的 String.Format() 靜態方法對於這些案例非常有用。 我們的 Edit.aspx 檢視範本會使用這個來格式化 EventDate 值 (DateTime 類型),使其不會顯示時間的秒數:

<%= Html.TextBox("EventDate", String.Format("{0:g}", Model.EventDate)) %>

Html.TextBox() 的第三個參數可以選擇性地用來輸出其他 HTML 屬性。 下列程式碼片段示範如何在 <input type="text"/> 項目上轉譯額外的 size=“30” 屬性和 class=“mycssclass” 屬性。 請注意,如何使用 "@" 字元逸出類別屬性的名稱,因為 "class" 是 C# 中的保留關鍵字:

<%= Html.TextBox("Title", Model.Title, new { size=30, @class="myclass" } )%>

實作 HTTP-POST 編輯動作方法

我們現在已實作編輯動作方法的 HTTP-GET 版本。 當使用者要求 /Dinners/Edit/1 URL 時,他們會收到 HTML 頁面,如下所示:

當使用者要求 Edit Dinner 時,H T M L 輸出的螢幕擷取畫面。

按下 [儲存] 按鈕會導致表單張貼至 /Dinners/Edit/1 URL,並使用 HTTP POST 動詞提交 HTML <輸入>表單值。 現在,讓我們實作編輯動作方法的 HTTP POST 行為,以處理 Dinner 的儲存。

我們將開始在 DinnersController 中新增一個具有 “AcceptVerbs” 屬性的多載的 [編輯] 動作方法,以指示它處理 HTTP POST 案例:

//
// POST: /Dinners/Edit/2

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection formValues) {
   ...
}

當 [AcceptVerbs] 屬性套用至多載動作方法時,ASP.NET MVC 會根據傳入的 HTTP 動詞自動處理分派要求到適當的動作方法。 對 /Dinners/Edit/[id] URL 的 HTTP POST 要求將轉到上面的 [編輯] 方法,而對 /Dinners/Edit/[id] URL 的所有其他 HTTP 動詞要求將轉到我們實作的第一個 [編輯] 方法 (該方法沒有 [AcceptVerbs] 屬性)。

側邊主題:為什麼要透過 HTTP 動詞來區分?
您可能會問 – 為什麼我們會使用單一 URL,並透過 HTTP 動詞來區分其行為? 為什麼不只是有兩個不同的 URL 來處理載入和儲存編輯變更? 例如:/Dinners/Edit/[id] 可顯示初始表單,而 /Dinners/Save/[id] 則處理表單張貼以將其儲存? 發佈兩個不同的 URL 的缺點是,如果我們張貼到 /Dinners/Save/2,然後因為輸入錯誤而需要重新顯示 HTML 表單,結果終端使用者的瀏覽器的網址列會出現 /Dinners/Save/2 URL (因為那是表單張貼的 URL)。 如果終端使用者將此重新顯示的頁面加入瀏覽器我的最愛清單,或複製/貼上 URL 並以電子郵件將其傳送給朋友,結果他們儲存的 URL 在未來無法運作 (因為該 URL 取決於張貼值)。 藉由公開單一 URL (例如:/Dinners/Edit/[id]) 和以 HTTP 動詞區分其處理方式,終端使用者就可以安全地將編輯頁面加入書籤,以及/或將 URL 傳送給其他人。

擷取表單張貼值

我們有多種方式可以在 HTTP POST [編輯] 方法中存取張貼的表單參數。 一個簡單的方法是只使用 Controller 基底類別上的 Request 屬性來存取表單集合,並直接擷取張貼的值:

//
// POST: /Dinners/Edit/2

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection formValues) {

    // Retrieve existing dinner
    Dinner dinner = dinnerRepository.GetDinner(id);

    // Update dinner with form posted values
    dinner.Title = Request.Form["Title"];
    dinner.Description = Request.Form["Description"];
    dinner.EventDate = DateTime.Parse(Request.Form["EventDate"]);
    dinner.Address = Request.Form["Address"];
    dinner.Country = Request.Form["Country"];
    dinner.ContactPhone = Request.Form["ContactPhone"];

    // Persist changes back to database
    dinnerRepository.Save();

    // Perform HTTP redirect to details page for the saved Dinner
    return RedirectToAction("Details", new { id = dinner.DinnerID });
}

不過,上述方法有點過於冗長,尤其是在我們新增錯誤處理邏輯之後。

此案例的較佳方法是利用 Controller 基底類別上的內建 UpdateModel() 協助程式方法。 它支援使用傳入的表單參數來更新我們傳遞給它的物件的屬性。 它會使用反映來判斷物件上的屬性名稱,然後根據用戶端提交的輸入值,自動轉換和指派值給它們。

我們可以使用 UpdateModel() 方法來使用此程式碼簡化 HTTP-POST 編輯動作:

//
// POST: /Dinners/Edit/2

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection formValues) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    UpdateModel(dinner);

    dinnerRepository.Save();

    return RedirectToAction("Details", new { id = dinner.DinnerID });
}

我們現在可以造訪 /Dinners/Edit/1 URL,並變更 Dinner 的標題:

[編輯 Dinner] 頁面的螢幕擷取畫面。

當我們按一下 [儲存] 按鈕時,我們會執行 [編輯] 動作的表單張貼,並將更新的值保存在資料庫中。 然後我們將被重新導向到 Dinner 的詳細資訊 URL (它將顯示新儲存的值):

Dinner 詳細資料 URL 的螢幕擷取畫面。

處理編輯錯誤

我們目前的 HTTP-POST 實作正常運作 – 除非發生錯誤。

當使用者編輯表單時發生錯誤,我們需要確保重新顯示表單,並提供具有指導性的錯誤訊息,幫助他們修正錯誤 這包括終端使用者張貼不正確的輸入的案例 (例如:格式錯誤的日期字串),以及輸入格式有效但違反商務規則的案例。 當發生錯誤時,表單應該保留使用者最初輸入的輸入資料,因此他們不需要手動重新填入變更。 必須視需要重複此流程多次,直到表單成功完成為止。

ASP.NET MVC 包含一些不錯的內建功能,讓錯誤處理和表單重新顯示更加容易。 若要查看這些功能的實際運作,請使用下列程式碼更新 [編輯] 動作方法:

//
// POST: /Dinners/Edit/2

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection formValues) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    try {

        UpdateModel(dinner);

        dinnerRepository.Save();

        return RedirectToAction("Details", new { id=dinner.DinnerID });
    }
    catch {

        foreach (var issue in dinner.GetRuleViolations()) {
            ModelState.AddModelError(issue.PropertyName, issue.ErrorMessage);
        }

        return View(dinner);
    }
}

上述程式碼與先前的實作類似,不同之處在於我們在工作流程中加入了 try/catch 錯誤處理區塊。 如果在呼叫 UpdateModel() 時發生例外狀況,或嘗試儲存 DinnerRepository 時發生例外狀況 (如果我們嘗試儲存的 Dinner 物件因模型中的規則違規而無效,則會引發例外狀況),將會執行我們的 catch 錯誤處理區塊。 在此區塊中,我們循環處理 Dinner 物件中的任何規則違反,並將其新增到 ModelState 物件中 (我們稍後會討論這一點)。 然後,我們會重新顯示檢視。

若要查看此功能的運作,請重新執行應用程式、編輯 Dinner,將其變更為空白「標題」、EventDate 變更為 “BOGUS”,並使用英國電話號碼搭配美國的國家/地區值。 當我們按下 [儲存] 按鈕時,HTTP POST 編輯方法將無法儲存 Dinner (因為發生錯誤),而且會重新顯示表單:

由於使用 H T T P S P O S T 編輯方法出現錯誤而重新顯示的表單螢幕擷取畫面。

我們的應用程式具有良好的錯誤處理體驗。 無效輸入的文字項目會以紅色醒目提示,並向終端使用者顯示驗證錯誤訊息。 表單也會保留使用者最初輸入的輸入資料,因此不需要重新填入任何內容。

您可能會問,這是怎麼發生的? Title、EventDate 和 ContactPhone 文字方塊如何以紅色醒目提示自己,並知道如何輸出原先輸入的使用者值? 錯誤訊息是如何顯示在頂端的清單中? 好消息是,這並不是憑空發生的,而是因為我們使用了一些內建的 ASP.NET MVC 功能,這些功能讓輸入驗證和錯誤處理案例變得簡單。

了解 ModelState 和驗證 HTML 協助程式方法

控制器類別具有 "ModelState" 屬性集合,提供一種方式來指出模型物件所存在的錯誤傳遞至「檢視」。 ModelState 集合中的錯誤項目會識別問題模型屬性的名稱 (例如:"Title"、"EventDate" 或 "ContactPhone"),並允許指定人性化的錯誤訊息 (例如:「需要標題」)。

UpdateModel() 協助程式方法會在嘗試將表單值指派給模型物件上的屬性時,自動填入 ModelState 集合。 例如,Dinner 物件之 EventDate 的屬性類型為 DateTime。 當 UpdateModel() 方法無法在上述案例中將字串值 "BOGUS" 指派給它時,UpdateModel() 方法已將項目新增至 ModelState 集合,指出該屬性發生指派錯誤的情況。

開發人員還可以撰寫程式碼來明確地將錯誤項目新增至 ModelState 集合中,就像我們在下面的 "catch" 錯誤處理區塊中所做的那樣,該區塊根據 Dinner 物件中的活動規則違規使用項目填入 ModelState 集合:

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection formValues) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    try {
    
        UpdateModel(dinner);

        dinnerRepository.Save();

        return RedirectToAction("Details", new { id=dinner.DinnerID });
    }
    catch {
    
        foreach (var issue in dinner.GetRuleViolations()) {
            ModelState.AddModelError(issue.PropertyName, issue.ErrorMessage);
        }

        return View(dinner);
    }
}

Html 協助程式與 ModelState 整合

HTML 協助程式方法 - 例如 Html.TextBox(),在轉譯輸出時檢查 ModelState 集合。 如果該項目發生錯誤,則會轉譯使用者輸入的值和 CSS 錯誤類別。

例如,在我們的 [編輯] 檢視中,我們使用 Html.TextBox() 協助程式方法來轉譯 Dinner 物件的 EventDate:

<%= Html.TextBox("EventDate", String.Format("{0:g}", Model.EventDate)) %>

在錯誤案例中轉譯檢視時,Html.TextBox() 方法會檢查 ModelState 集合,以查看是否有任何錯誤與 Dinner 物件的 “EventDate” 屬性相關聯。 當它判斷有錯誤時,它會將提交的使用者輸入 (“BOGUS”) 轉譯為值,並將 CSS 錯誤類別新增至其所產生的 <input type="textbox"/> 標記:

<input class="input-validation-error"id="EventDate" name="EventDate" type="text" value="BOGUS"/>

您可以自訂 CSS 錯誤類別的外觀以視需要查看。 預設 CSS 錯誤類別 – “input-validation-error” – 定義於 \content\site.css 樣式表中,如下所示:

.input-validation-error
{
    border: 1px solid #ff0000;
    background-color: #ffeeee;
}

此 CSS 規則會導致無效的輸入項目被醒目提示,如下所示:

醒目提示無效輸入項目的螢幕擷取畫面。

Html.ValidationMessage() 協助程式方法

Html.ValidationMessage() 協助程式方法可用來輸出與特定模型屬性相關聯的 ModelState 錯誤訊息:

<%= Html.ValidationMessage("EventDate")%>

上述程式碼會輸出:<span class=“field-validation-error”> 值 'BOGUS' 無效</span>

Html.ValidationMessage() 協助程式方法也支援第二個參數,可讓開發人員覆寫顯示的錯誤文字訊息:

<%= Html.ValidationMessage("EventDate","*") %>

上述程式碼會輸出:<span class=“field-validation-error”>*</span>,而不是 EventDate 屬性出現錯誤時的預設錯誤文字。

Html.ValidationSummary() 協助程式方法

Html.ValidationSummary() 協助程式方法可用來轉譯摘要錯誤訊息,並隨附 ModelState 集合中所有詳細錯誤訊息的 <ul><li/></ul> 清單:

ModelState 集合中所有詳細錯誤訊息清單的螢幕擷取畫面。

Html.ValidationSummary() 協助程式方法會採用選擇性的字串參數 – 該參數用於定義要顯示在詳細錯誤清單上方的摘要錯誤訊息:

<%= Html.ValidationSummary("Please correct the errors and try again.") %>

您可以選擇性地使用 CSS 來覆寫錯誤清單的外觀。

使用 AddRuleViolations 協助程式方法

我們的初始 HTTP-POST 編輯實作使用其 catch 區塊內的 foreach 陳述式來循環處理 Dinner 物件的規則違規,並將其新增至控制器的 ModelState 集合:

catch {
        foreach (var issue in dinner.GetRuleViolations()) {
            ModelState.AddModelError(issue.PropertyName, issue.ErrorMessage);
        }

        return View(dinner);
    }

我們可以將 "ControllerHelpers" 類別新增至 NerdDinner 專案,並在其中實作 “AddRuleViolations” 擴充方法,將協助程式方法新增至 ASP.NET MVC ModelStateDictionary 類別,讓此程式碼稍微更加簡潔。 此擴充方法可以封裝在 ModelStateDictionary 中填入 RuleViolation 錯誤清單所需的邏輯:

public static class ControllerHelpers {

   public static void AddRuleViolations(this ModelStateDictionary modelState, IEnumerable<RuleViolation> errors) {
   
       foreach (RuleViolation issue in errors) {
           modelState.AddModelError(issue.PropertyName, issue.ErrorMessage);
       }
   }
}

然後,我們可以更新 HTTP-POST 編輯動作方法,以使用此擴充方法在 ModelState 集合中填入我們的 Dinner 規則違規。

完成編輯動作方法實作

下列程式碼會實作 [編輯] 案例所需的所有控制器邏輯:

//
// GET: /Dinners/Edit/2

public ActionResult Edit(int id) {

    Dinner dinner = dinnerRepository.GetDinner(id);
    
    return View(dinner);
}

//
// POST: /Dinners/Edit/2

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection formValues) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    try {
    
        UpdateModel(dinner);

        dinnerRepository.Save();

        return RedirectToAction("Details", new { id=dinner.DinnerID });
    }
    catch {
    
        ModelState.AddRuleViolations(dinner.GetRuleViolations());

        return View(dinner);
    }
}

[編輯] 實作的好處是我們的 [控制器] 類別和 [檢視] 範本都不需要知道 Dinner 模型所強制執行的特定驗證或商務規則。 我們可以在未來將其他規則新增至模型,而且不需要變更控制器或檢視的任何程式碼就能支援這些規則。 這可讓我們靈活地在未來以最少的程式碼變更,輕鬆地發展應用程式需求。

建立支援

我們已完成實作 DinnersController 類別的 [編輯] 行為。 現在讓我們繼續實作其上的 [建立] 支援,讓使用者能夠新增 Dinners。

HTTP-GET 建立動作方法

我們將從實作建立動作方法的 HTTP “GET” 行為開始。 當有人造訪 /Dinners/Create URL 時,將會呼叫這個方法。 我們的實作看起來如下:

//
// GET: /Dinners/Create

public ActionResult Create() {

    Dinner dinner = new Dinner() {
        EventDate = DateTime.Now.AddDays(7)
    };

    return View(dinner);
}

上述程式碼會建立新的 Dinner 物件,並將其 EventDate 屬性指派為未來一週。 然後,它會轉譯以新 Dinner 物件為基礎的「檢視」。 因為我們尚未明確將名稱傳遞至 View() 協助程式方法,因此會使用慣例的預設路徑來解析檢視範本:/Views/Dinners/Create.aspx。

現在讓我們建立這個檢視範本。 我們可以在 [建立] 動作方法內按一下滑鼠右鍵,然後選取 [新增檢視] 內容功能表命令來執行此動作。 在 [新增檢視] 對話方塊中,我們將指出要將 Dinner 物件傳遞至檢視範本,然後選擇自動生成 [建立] 範本:

[新增檢視] 以建立檢視範本的螢幕擷取畫面。

當我們按一下 [新增] 按鈕時,Visual Studio 會將新的框架型 “Create.aspx” 檢視儲存至 “\Views\Dinners” 目錄,並在 IDE 中開啟:

編輯程式碼之 I D E 的螢幕擷取畫面。

讓我們對為我們產生的預設 [建立] 框架檔案進行一些變更,並加以修改,如下所示:

<asp:Content ID="Title" ContentPlaceHolderID="TitleContent" runat="server">
     Host a Dinner
</asp:Content>

<asp:Content ID="Main" ContentPlaceHolderID="MainContent" runat="server">

    <h2>Host a Dinner</h2>

    <%=Html.ValidationSummary("Please correct the errors and try again.") %>
 
    <% using (Html.BeginForm()) {%>
  
        <fieldset>
            <p>
                <label for="Title">Title:</label>
                <%= Html.TextBox("Title") %>
                <%= Html.ValidationMessage("Title", "*") %>
            </p>
            <p>
                <label for="EventDate">EventDate:</label>
                <%=Html.TextBox("EventDate") %>
                <%=Html.ValidationMessage("EventDate", "*") %>
            </p>
            <p>
                <label for="Description">Description:</label>
                <%=Html.TextArea("Description") %>
                <%=Html.ValidationMessage("Description", "*") %>
            </p>
            <p>
                <label for="Address">Address:</label>
                <%=Html.TextBox("Address") %>
                <%=Html.ValidationMessage("Address", "*") %>
            </p>
            <p>
                <label for="Country">Country:</label>
                <%=Html.TextBox("Country") %>
                <%=Html.ValidationMessage("Country", "*") %>
            </p>
            <p>
                <label for="ContactPhone">ContactPhone:</label>
                <%=Html.TextBox("ContactPhone") %>
                <%=Html.ValidationMessage("ContactPhone", "*") %>
            </p>            
            <p>
                <label for="Latitude">Latitude:</label>
                <%=Html.TextBox("Latitude") %>
                <%=Html.ValidationMessage("Latitude", "*") %>
            </p>
            <p>
                <label for="Longitude">Longitude:</label>
                <%=Html.TextBox("Longitude") %>
                <%=Html.ValidationMessage("Longitude", "*") %>
            </p>
            <p>
                <input type="submit" value="Save"/>
            </p>
        </fieldset>
    <% } 
%>
</asp:Content>

現在,當我們執行應用程式並存取瀏覽器中的 "/Dinners/Create" URL 時,它會從我們的 [建立] 動作實作中轉譯如下的 UI:

當我們執行應用程式並存取 Dinners U R L 時 [建立] 動作實作的螢幕擷取畫面。

實作 HTTP-POST 建立動作方法

我們已實作 [建立] 動作方法的 HTTP-GET 版本。 當使用者按一下 [儲存] 按鈕時,它會對 /Dinners/Create URL 執行表單貼文,並使用 HTTP POST 動詞提交 HTML <輸入>表單值。

現在讓我們實作建立動作方法的 HTTP POST 行為。 我們將開始在 DinnersController 中新增一個具有 “AcceptVerbs” 屬性的多載的 [建立] 動作方法,以指示它處理 HTTP POST 案例:

//
// POST: /Dinners/Create

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create() {
    ...
}

我們有多種方式可以在啟用 HTTP-POST 的 [建立] 方法中存取已張貼的表單參數。

其中一種方法是建立新的 Dinner 物件,然後使用 UpdateModel() 協助程式方法 (就像我們在 [編輯] 動作中所做的一樣),填入張貼的表單值。 然後,我們可以將其新增至 DinnerRepository、將它保存到資料庫,並將使用者重新導向至 [詳細資料] 動作,以使用下列程式碼顯示新建立的 Dinner:

//
// POST: /Dinners/Create

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create() {

    Dinner dinner = new Dinner();

    try {
    
        UpdateModel(dinner);

        dinnerRepository.Add(dinner);
        dinnerRepository.Save();

        return RedirectToAction("Details", new {id=dinner.DinnerID});
    }
    catch {
    
        ModelState.AddRuleViolations(dinner.GetRuleViolations());

        return View(dinner);
    }
}

或者,我們可以使用一種方法,讓 Create() 動作方法將 Dinner 物件作為方法參數。 ASP.NET MVC 接著會自動為我們具現化新的 Dinner 物件、使用表單輸入填入其屬性,並將其傳遞至我們的動作方法:

//
//
// POST: /Dinners/Create

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(Dinner dinner) {

    if (ModelState.IsValid) {

        try {
            dinner.HostedBy = "SomeUser";

            dinnerRepository.Add(dinner);
            dinnerRepository.Save();

            return RedirectToAction("Details", new {id = dinner.DinnerID });
        }
        catch {        
            ModelState.AddRuleViolations(dinner.GetRuleViolations());
        }
    }
    
    return View(dinner);
}

上述的動作方法會藉由檢查 ModelState.IsValid 屬性,確認 Dinner 物件是否已成功填入表單張貼值。 如果有輸入轉換問題,這會傳回 false (例如:EventDate 屬性的 “BOGUS” 字串),如果有任何問題,我們的動作方法會重新顯示該表單。

如果輸入值有效,則該動作方法會嘗試將新的 Dinner 新增並儲存至 DinnerRepository。 它會將這項工作包裝在 try/catch 區塊中,並在發生任何商務規則違規時重新顯示表單 (這會導致 dinnerRepository.Save() 方法引發例外狀況)。

若要實際查看此錯誤處理行為,我們可以要求 /Dinners/Create URL,並填寫新 Dinner 的詳細資料。 不正確的輸入或值會導致建立表單重新顯示,並醒目提示錯誤,如下所示:

表單重新顯示,並醒目提示錯誤的螢幕擷取畫面。

請注意,我們的 [建立] 表單如何遵循與 [編輯] 表單完全相同的驗證和商務規則。 這是因為我們使用模型定義驗證和商務規則,而且未內嵌在應用程式的 UI 或控制器內。 這表示我們稍後可以在單一位置變更/演進驗證或商務規則,並將其套用在整個應用程式中。 我們不需要變更 [編輯] 或 [建立] 動作方法內的任何程式碼,以自動遵循現有規則的任何新規則或修改。

當我們修正輸入值,再按一下 [儲存] 按鈕時,我們對 DinnerRepository 的新增就會成功,並且新的 Dinner 將被新增到資料庫中。 然後,我們會被重新導向至 /Dinners/Details/[id] URL – 我們將在這裡看到新建立的 Dinner 的詳細資料:

最新建立之 Dinner 的螢幕擷取畫面。

刪除支援

現在讓我們將 [刪除] 支援新增至 DinnersController。

HTTP-GET 刪除動作方法

我們將從實作刪除動作方法的 HTTP GET 行為開始。 當有人造訪 /Dinners/Delete/[id] URL 時,就會呼叫此方法。 以下是實作:

//
// HTTP GET: /Dinners/Delete/1

public ActionResult Delete(int id) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    if (dinner == null)
         return View("NotFound");
    else
        return View(dinner);
}

動作方法會嘗試擷取要刪除的 Dinner。 如果 Dinner 存在,則會根據 Dinner 物件轉譯 [檢視]。 如果物件不存在 (或已經刪除),則會傳回 [檢視],該檢視會轉譯我們稍早針對 [詳細資料] 動作方法所建立的 "NotFound" 檢視範本。

我們可以在 [刪除] 動作方法內按一下滑鼠右鍵,然後選取 [新增檢視] 內容功能表命令,以建立 [刪除] 檢視範本。 在 [新增檢視] 對話方塊中,我們將指出我們要將 Dinner 物件傳遞至檢視範本做為其模型,並選擇建立空白範本:

將 [刪除] 檢視範本建立為空白範本的螢幕擷取畫面。

當我們按一下 [新增] 按鈕時,Visual Studio 會在 “\Views\Dinners” 目錄中為我們新增一個新的 "Delete.aspx" 檢視範本檔案。 我們會將一些 HTML 和程式碼新增至範本,以實作刪除確認畫面,如下所示:

<asp:Content ID="Title" ContentPlaceHolderID="TitleContent" runat="server">
    Delete Confirmation:  <%=Html.Encode(Model.Title) %>
</asp:Content>

<asp:Content ID="Main" ContentPlaceHolderID="MainContent" runat="server">

    <h2>
        Delete Confirmation
    </h2>

    <div>
        <p>Please confirm you want to cancel the dinner titled: 
           <i> <%=Html.Encode(Model.Title) %>? </i> 
        </p>
    </div>
    
    <% using (Html.BeginForm()) {  %>
        <input name="confirmButton" type="submit" value="Delete" />        
    <% } %>
     
</asp:Content>

上面的程式碼顯示將被刪除的 Dinner 標題,並輸出一個<表單>項目,如果終端使用者按一下其中的 [刪除] 按鈕,將執行 POST 請求到 /Dinners/Delete/[id] URL。

當我們執行應用程式並存取有效的 Dinner 物件的 “/Dinners/Delete/[id]” URL 時,它會如下轉譯 UI:

H T T P G E T 刪除動作方法中 Dinner 刪除確認 U I 的螢幕擷取畫面。

側邊主題:為什麼我們會執行 POST?
您可能會問,為什麼我們在 [刪除] 確認畫面內建立<表單>? 為什麼不只是使用標準超連結來連結至實際刪除作業的動作方法? 這是因為我們要小心防範網路爬蟲和搜尋引擎發現我們的 URL,並在存取連結時無意中導致資料被刪除。 HTTP-GET 型 URL 被視為「安全」,可供他們存取/耙梳,而且應該不會遵循 HTTP-POST URL。 良好的規則是確保您始終將破壞性或資料修改作業放在 HTTP-POST 要求之後。

實作 HTTP-POST 刪除動作方法

我們現在已實作 [刪除] 動作方法的 HTTP-GET 版本,它會顯示刪除確認畫面。 當終端使用者按一下 [刪除] 按鈕時,它會對 /Dinners/Dinner/[id] URL 執行表單張貼。

現在讓我們使用下列程式碼實作刪除動作方法的 HTTP “POST” 行為:

// 
// HTTP POST: /Dinners/Delete/1

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Delete(int id, string confirmButton) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    if (dinner == null)
        return View("NotFound");

    dinnerRepository.Delete(dinner);
    dinnerRepository.Save();

    return View("Deleted");
}

[刪除] 動作方法的 HTTP-POST 版本會嘗試擷取要刪除的 dinner 物件。 如果找不到它 (因為它已經被刪除),它會轉譯我們的 "NotFound" 範本。 如果找到 Dinner,它會將其從 DinnerRepository 刪除。 然後,它會轉譯 [已刪除] 範本。

若要實作 [已刪除] 範本,我們將在動作方法中按一下滑鼠右鍵,然後選擇 [新增檢視] 內容功能表。 我們會將檢視命名為 [已刪除],並讓它成為空的範本 (而不是採用強型別模型物件)。 接著,我們會將一些 HTML 內容新增至其中:

<asp:Content ID="Title" ContentPlaceHolderID="TitleContent" runat="server">
    Dinner Deleted
</asp:Content>

<asp:Content ID="Main" ContentPlaceHolderID="MainContent" runat="server">

    <h2>Dinner Deleted</h2>

    <div>
        <p>Your dinner was successfully deleted.</p>
    </div>
    
    <div>
        <p><a href="/dinners">Click for Upcoming Dinners</a></p>
    </div>
    
</asp:Content>

現在,當我們執行應用程式並存取有效 Dinner 物件的 “/Dinners/Delete/[id]” URL 時,它會轉譯我們的 Dinner 刪除確認畫面,如下所示:

H T T P P O S T 刪除動作方法中 Dinner 刪除確認畫面的螢幕擷取畫面。

當我們按一下 [刪除] 按鈕時,它會對 /Dinners/Delete/[id] URL 執行 HTTP-POST,這會從資料庫刪除 Dinner,並顯示 「已刪除」檢視範本:

[已刪除] 檢視範本的螢幕擷取畫面。

模型繫結安全性

我們已討論兩種不同的方式,以使用 ASP.NET MVC 的內建模型繫結功能。 第一個使用 UpdateModel() 方法來更新現有模型物件的屬性,第二個則使用 ASP.NET MVC 的支援將模型物件作為動作方法參數傳遞。 這兩種技術都非常強大且非常有用。

這種權力也帶來了責任。 在接受任何使用者輸入時,務必對安全性保持偏執,在將物件繫結到表單輸入時也是如此。 對任何使用者輸入的值進行 HTML 編碼時請務必小心,以避免 HTML 和 JavaScript 插入式攻擊,並小心 SQL 插入式攻擊 (請注意:我們在應用程式使用 LINQ to SQL,這會自動編碼參數以防止這些類型的攻擊)。 切勿只依賴客戶端驗證,而且請務必採用伺服器端驗證來防止駭客試圖向您傳送虛假值。

在使用 ASP.NET MVC 的繫結功能時,需要確保考慮的另一個安全性事項是所繫結物件的範圍。 具體來說,您需要確保了解允許繫結之屬性的安全性含義,並確保只允許更新那些真正應該由終端使用者更新的屬性。

根據預設,UpdateModel() 方法會嘗試更新模型物件上符合傳入表單參數值的所有屬性。 同樣地,根據預設,以動作方法參數傳遞的物件也可以透過表單參數設定其所有屬性。

根據每次使用情況鎖定繫結

您可以透過提供明確的 [包含清單] 來指定允許更新的屬性,從而針對每次使用情況鎖定繫結原則。 這可以透過將額外的字串陣列參數傳遞給 UpdateModel() 方法來完成,如下所示:

string[] allowedProperties = new[]{ "Title","Description", 
                                    "ContactPhone", "Address",
                                    "EventDate", "Latitude", 
                                    "Longitude"};
                                    
UpdateModel(dinner, allowedProperties);

作為動作方法參數傳遞的物件也支援 [繫結] 屬性,該屬性允許指定允許的屬性的 [包含清單],如下所示:

//
// POST: /Dinners/Create

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create( [Bind(Include="Title,Address")] Dinner dinner ) {
    ...
}

以類型為基礎鎖定繫結

您也可以根據每一類型鎖定繫結規則。 這允許您指定一次繫結規則,然後將它們應用於所有控制器和動作方法的所有案例 (包括 UpdateModel 和動作方法參數案例)。

您可以通過在類型上新增 [繫結] 屬性或在應用程式的 Global.asax 檔案中予以註冊,來自訂每類型繫結規則 (對於您不擁有該類型的情况很有用)。 然後,您可以使用 [繫結] 屬性的 [包含] 和 [排除] 屬性,來控制哪些屬性對於特定類別或介面是可繫結的。

我們將在 NerdDinner 應用程式中的 Dinner 類別中使用此技術,並將 [繫結] 屬性新增至其中,將可繫結屬性清單限制為以下內容:

[Bind(Include="Title,Description,EventDate,Address,Country,ContactPhone,Latitude,Longitude")]
public partial class Dinner {
   ...
}

請注意,我們不允許透過繫結操作 RSVP 集合,也不允許透過繫結設定 DinnerID 或 HostedBy 屬性。 基於安全性考慮,我們只會使用動作方法內的明確程式碼來操作這些特定屬性。

CRUD 總結

ASP.NET MVC 包含一些內建功能,可協助實作表單張貼案例。 我們使用各種不同的功能,在 DinnerRepository 之上提供 CRUD UI 支援。

我們使用以模型為主的方法來實作應用程式。 這表示我們的所有驗證和商務規則邏輯都是在我們的模型層內定義,而不是在我們的控制器或檢視中定義的。 我們的 [控制器] 類別和 [檢視] 範本都不知道我們的 Dinner 模型類別所強制執行的特定商務規則。

這將使我們保持乾淨的應用程式架構並使其更易於測試。 我們可以在未來將其他規則新增至模型層,而且不需要變更 [控制器] 或 [檢視] 的任何程式碼就能支援這些規則。 這將為我們提供很大的靈活性,以便在未來發展和變更我們的應用程式。

我們的 DinnersController 現在支援 Dinner 清單/詳細資料,以及建立、編輯和删除支援。 您可以在下列找到該類別的完整程式碼:

public class DinnersController : Controller {

    DinnerRepository dinnerRepository = new DinnerRepository();

    //
    // GET: /Dinners/

    public ActionResult Index() {

        var dinners = dinnerRepository.FindUpcomingDinners().ToList();
        return View(dinners);
    }

    //
    // GET: /Dinners/Details/2

    public ActionResult Details(int id) {

        Dinner dinner = dinnerRepository.GetDinner(id);

        if (dinner == null)
            return View("NotFound");
        else
            return View(dinner);
    }

    //
    // GET: /Dinners/Edit/2

    public ActionResult Edit(int id) {

        Dinner dinner = dinnerRepository.GetDinner(id);
        return View(dinner);
    }

    //
    // POST: /Dinners/Edit/2

    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult Edit(int id, FormCollection formValues) {

        Dinner dinner = dinnerRepository.GetDinner(id);

        try {
            UpdateModel(dinner);

            dinnerRepository.Save();

            return RedirectToAction("Details", new { id= dinner.DinnerID });
        }
        catch {
            ModelState.AddRuleViolations(dinner.GetRuleViolations());

            return View(dinner);
        }
    }

    //
    // GET: /Dinners/Create

    public ActionResult Create() {

        Dinner dinner = new Dinner() {
            EventDate = DateTime.Now.AddDays(7)
        };
        return View(dinner);
    }

    //
    // POST: /Dinners/Create

    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult Create(Dinner dinner) {

        if (ModelState.IsValid) {

            try {
                dinner.HostedBy = "SomeUser";

                dinnerRepository.Add(dinner);
                dinnerRepository.Save();

                return RedirectToAction("Details", new{id=dinner.DinnerID});
            }
            catch {
                ModelState.AddRuleViolations(dinner.GetRuleViolations());
            }
        }

        return View(dinner);
    }

    //
    // HTTP GET: /Dinners/Delete/1

    public ActionResult Delete(int id) {

        Dinner dinner = dinnerRepository.GetDinner(id);

        if (dinner == null)
            return View("NotFound");
        else
            return View(dinner);
    }

    // 
    // HTTP POST: /Dinners/Delete/1

    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult Delete(int id, string confirmButton) {

        Dinner dinner = dinnerRepository.GetDinner(id);

        if (dinner == null)
            return View("NotFound");

        dinnerRepository.Delete(dinner);
        dinnerRepository.Save();

        return View("Deleted");
    }
}

後續步驟

我們現在有基本的 CRUD (建立、讀取、更新和刪除) 支援在我們的 DinnersController 類別內實作。

現在讓我們看看如何使用 ViewData 和 ViewModel 類別,在表單上啟用更豐富的 UI。