提供 CRUD (建立、讀取、更新、刪除) 資料表單項目支援
由 Microsoft 提供
這是免費的 "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。
現在讓我們建立這個檢視範本。 我們將在 [編輯] 方法內按一下滑鼠右鍵,然後選取 [新增檢視] 內容功能表命令來執行此動作:
我們將在 [新增檢視] 對話方塊中指出要將 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 時,我們將會看到下列頁面:
我們的檢視所產生的 HTML 標記如下所示。 它是標準 HTML – 當按下 [儲存]<input type="submit"/> 按鈕時,<表單>項目會執行 /Dinners/Edit/1 URL 的 HTTP POST。 每個可編輯屬性的 HTML <input type="text"/> 項目已經輸出:
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 頁面,如下所示:
按下 [儲存] 按鈕會導致表單張貼至 /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 的詳細資訊 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 (因為發生錯誤),而且會重新顯示表單:
我們的應用程式具有良好的錯誤處理體驗。 無效輸入的文字項目會以紅色醒目提示,並向終端使用者顯示驗證錯誤訊息。 表單也會保留使用者最初輸入的輸入資料,因此不需要重新填入任何內容。
您可能會問,這是怎麼發生的? 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> 清單:
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 中開啟:
讓我們對為我們產生的預設 [建立] 框架檔案進行一些變更,並加以修改,如下所示:
<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:
實作 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 的詳細資料:
刪除支援
現在讓我們將 [刪除] 支援新增至 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:
側邊主題:為什麼我們會執行 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 刪除確認畫面,如下所示:
當我們按一下 [刪除] 按鈕時,它會對 /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。