使用 ViewData 和實作 ViewModel 類別
由 Microsoft 提供
這是免費的 "NerdDinner" 應用程式教學課程的第 6 個步驟,詳細介紹了如何使用 ASP.NET MVC 1 建置一個小型但完整的 Web 應用程式。
步驟 6 示範如何啟用對更豐富表單編輯案例的支援,並討論了可用於將資料從控制器傳遞到檢視的兩種方法:ViewData 和 ViewModel。
如果使用 ASP.NET MVC 3,建議遵循 MVC 3 使用者入門或 MVC Music 市集教學課程。
NerdDinner 步驟 6:ViewData 和 ViewModel
我們已涵蓋許多表單張貼案例,並討論如何實作建立、更新和刪除 (CRUD) 支援。 我們現在會進一步採用 DinnersController 實作,並啟用更豐富的表單編輯案例支援。 在執行此操作時,我們將討論兩種可用來將資料從控制器傳遞到檢視的方法:ViewData 和 ViewModel。
將資料從控制器傳遞到檢視 - 範本
MVC 模式的一個定義特性是能有效強調應用程式不同元件之間的「關注點分離」。 模型、控制器和檢視各有良好定義的角色和責任,並以良好定義的方式彼此通訊。 這有助於提升可測試性和程式碼重複使用。
當 [控制器] 類別決定將 HTML 回應轉譯回用戶端時,它負責明確地將所有轉譯回應所需的資料傳遞給檢視範本。 檢視範本絕對不應該執行任何資料擷取或應用程式邏輯,而應僅限於根據控制器傳遞的模型/資料來進行轉譯的程式碼。
現在,DinnersController 類別所傳遞至檢視範本的模型資料很簡單且直接,在 Index() 的情況下是一個 Dinner 物件的列表,而在 Details()、Edit()、Create() 和 Delete() 的情況下則是一個單一的 Dinner 物件。 當我們將更多 UI 功能新增至應用程式時,通常需要傳遞更多資料,以在檢視範本中轉譯 HTML 回應。 例如,我們可能想要將 [編輯] 和 [建立] 檢視內的 [國家/地區] 欄位從 HTML 文字方塊變更為下拉式清單。 與其在檢視範本中硬式編碼國家/地區和區域名稱的下拉式清單,我們可能會希望從動態填入的支援國家/地區和區域清單中產生。 我們需要一種方法,將 Dinner 物件和支援的國家/地區和區域清單,從我們的控制器傳遞至檢視範本。
讓我們看看實現這一目標的兩種方法。
使用 ViewData 字典
控制器基底類別公開了一個 [ViewData] 字典屬性,該屬性可用於將其他資料項目從 [控制器] 傳遞到 [檢視]。
例如,為了支援要將 [編輯] 檢視內的 [國家/地區] 文字方塊從 HTML 文字方塊變更為下拉式清單的案例,我們可以更新 Edit() 動作方法以傳遞 SelectList 物件 (除了 Dinner 物件之外),該物件可用來做為 [國家/地區] 下拉式清單的模型。
//
// GET: /Dinners/Edit/5
[Authorize]
public ActionResult Edit(int id) {
Dinner dinner = dinnerRepository.GetDinner(id);
ViewData["Countries"] = new SelectList(PhoneValidator.AllCountries, dinner.Country);
return View(dinner);
}
上述 SelectList 的建構函式會接受一份國家/地區和區域清單,以填入下拉式清單以及目前選取的值。
然後,我們可以更新 Edit.aspx 檢視範本,以使用 Html.DropDownList() 協助程式方法,而不是先前使用的 Html.TextBox() 協助程式方法:
<%= Html.DropDownList("Country", ViewData["Countries"] as SelectList) %>
上述 Html.DropDownList() 協助程式方法會採用兩個參數。 第一個是要輸出的 HTML 表單項目名稱。 第二個是透過 ViewData 字典傳遞的 “SelectList” 模型。 我們使用 C# “as” 關鍵詞,將字典內的類型轉換成 SelectList。
現在,當我們執行應用程式並存取瀏覽器內的 /Dinners/Edit/1 URL 時,我們會看到編輯 UI 已更新,以顯示國家和地區的下拉式清單而不是文字方塊:
因為我們也會從 HTTP-POST 編輯方法轉譯 [編輯] 檢視範本(在發生錯誤的案例),因此我們要確定也更新此方法,以在檢視範本在錯誤案例中轉譯時,將 SelectList 新增至 ViewData:
//
// POST: /Dinners/Edit/5
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection collection) {
Dinner dinner = dinnerRepository.GetDinner(id);
try {
UpdateModel(dinner);
dinnerRepository.Save();
return RedirectToAction("Details", new { id=dinner.DinnerID });
}
catch {
ModelState.AddModelErrors(dinner.GetRuleViolations());
ViewData["countries"] = new SelectList(PhoneValidator.AllCountries, dinner.Country);
return View(dinner);
}
}
現在我們的 DinnersController 編輯案例支援 DropDownList。
使用 ViewModel 模式
ViewData 字典方法的優點是相當快速且容易實作。 不過,有些開發人員不喜歡使用以字串為基礎的字典,因為錯字可能會導致在編譯時間不會被攔截的錯誤。 在檢視範本中使用類似 C# 的強型別語言時,不具型別的 ViewData 字典也需要使用 "as" 運算子或轉換。
我們可以使用的另一種方法是通常稱為 “ViewModel” 模式的方法。 使用此模式時,我們會建立針對特定檢視案例進行最佳化的強型別類,並公開檢視範本所需的動態值/內容的屬性。 然後,我們的控制器類別可以填入這些檢視最佳化類別,並將其傳遞至要使用的檢視範本。 這可在檢視範本內啟用類型安全性、編譯時間檢查和編輯器 Intellisense。
例如,若要啟用 Dinner 表單編輯案例,我們可以建立類似以下的 "DinnerFormViewModel" 類別,以公開兩個強型別屬性:Dinner 物件,以及填入 [國家/地區] 下拉式清單所需的 SelectList 模型:
public class DinnerFormViewModel {
// Properties
public Dinner Dinner { get; private set; }
public SelectList Countries { get; private set; }
// Constructor
public DinnerFormViewModel(Dinner dinner) {
Dinner = dinner;
Countries = new SelectList(PhoneValidator.AllCountries, dinner.Country);
}
}
然後,我們可以更新Edit() 動作方法,以使用我們從存放庫擷取的 Dinner 物件來建立 DinnerFormViewModel,然後將它傳遞至我們的檢視範本:
//
// GET: /Dinners/Edit/5
[Authorize]
public ActionResult Edit(int id) {
Dinner dinner = dinnerRepository.GetDinner(id);
return View(new DinnerFormViewModel(dinner));
}
然後,我們將更新檢視範本,透過變更 edit.aspx 頁面頂部的 [繼承] 屬性,使其需要 “DinnerFormViewModel” 而不是 “Dinner” 物件,如下所示:
Inherits="System.Web.Mvc.ViewPage<NerdDinner.Controllers.DinnerFormViewModel>
一旦我們這樣做了,檢視範本中 [模型] 屬性的 Intellisense 將被更新,以反映我們傳遞給它的 DiningFormViewModel 類型的物件模型:
然後,我們可以更新檢視程式碼,讓它根據這個來運作。 請注意下面我們如何不變更正在建立的輸入項目的名稱 (表單項目仍將命名為 "Title"、"Country"),但我們正在更新 HTML 協助程式方法以使用 DiningFormViewModel 類別檢索值:
<p>
<label for="Title">Dinner Title:</label>
<%= Html.TextBox("Title", Model.Dinner.Title) %>
<%=Html.ValidationMessage("Title", "*") %>
</p>
<p>
<label for="Country">Country:</label>
<%= Html.DropDownList("Country", Model.Countries) %>
<%=Html.ValidationMessage("Country", "*") %>
</p>
我們也會在轉譯錯誤時更新 [編輯] 貼文方法,以使用 DinnerFormViewModel 類別:
//
// POST: /Dinners/Edit/5
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection collection) {
Dinner dinner = dinnerRepository.GetDinner(id);
try {
UpdateModel(dinner);
dinnerRepository.Save();
return RedirectToAction("Details", new { id=dinner.DinnerID });
}
catch {
ModelState.AddModelErrors(dinner.GetRuleViolations());
return View(new DinnerFormViewModel(dinner));
}
}
我們也可以更新 Create() 動作方法,以重複使用完全相同的 DinnerFormViewModel 類別,同時在這些類別中啟用 “Countries” DropDownList。 以下是 HTTP-GET 實作:
//
// GET: /Dinners/Create
public ActionResult Create() {
Dinner dinner = new Dinner() {
EventDate = DateTime.Now.AddDays(7)
};
return View(new DinnerFormViewModel(dinner));
}
以下是 HTTP-POST 建立方法的實作:
//
// 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.AddModelErrors(dinner.GetRuleViolations());
}
}
return View(new DinnerFormViewModel(dinner));
}
現在,我們的 [編輯] 和 [建立] 畫面都支援選取國家或區域的下拉式清單。
自訂形狀的 ViewModel 類別
在上述案例中,DinnerFormViewModel 類別會直接公開 Dinner 模型物件作為屬性,以及支援的 SelectList 模型屬性。 這個方法適用於我們想要在檢視範本內建立的 HTML UI 與網域模型物件相對緊密對應的案例。
針對情況並非如此的案例,其中一個可用的選項是建立自訂形狀的 ViewModel 類別,該類別的物件模型最佳化程度較高以供檢視取用,而且其看起來可能與基礎網域模型物件完全不同。 例如,它可能會公開從多個模型物件收集的不同屬性名稱和/或彙總屬性。
自訂形狀的 ViewModel 類別可用來將資料從控制器傳遞至檢視以轉譯,以及協助處理張貼回控制器動作方法的表單資料。 對於後面的案例,您可能會讓動作方法使用表單張貼的資料更新 ViewModel 物件,然後使用 ViewModel 執行個體來對應或擷取實際的網域模型物件。
自訂形狀的 ViewModel 類別可以提供很大的靈活性,而且當您在檢視範本內找到轉譯程式碼,或動作方法內的表單張貼程式碼開始變得太複雜時,都可以進行調查。 這通常表示您的網域模型與您正在產生的 UI 不完全對應,並且中間自訂形狀的 ViewModel 類別可以提供協助。
後續步驟
現在讓我們看看如何使用部分和主版頁面,在應用程式之間重複使用和共用 UI。