使用 ViewData 和实现 ViewModel 类
这是免费的 “NerdDinner”应用程序教程 的第 6 步,介绍如何使用 ASP.NET MVC 1 生成小型但完整的 Web 应用程序。
步骤 6 演示如何启用对更丰富的表单编辑方案的支持,还讨论了两种可用于将数据从控制器传递到视图的方法:ViewData 和 ViewModel。
如果你使用的是 ASP.NET MVC 3,我们建议你遵循入门与 MVC 3 或 MVC 音乐应用商店教程。
NerdDinner 步骤 6:ViewData 和 ViewModel
我们介绍了许多表单发布方案,并讨论了如何实现创建、更新和删除 (CRUD) 支持。 现在,我们将进一步实现 DinnersController,并为更丰富的表单编辑方案提供支持。 执行此操作时,我们将讨论两种可用于将数据从控制器传递到视图的方法:ViewData 和 ViewModel。
将数据从控制器传递到 View-Templates
MVC 模式的一个定义特征是它有助于在应用程序的不同组件之间强制实施严格的“关注点分离”。 模型、控制器和视图都有明确定义的角色和职责,并且它们以明确定义的方式相互通信。 这有助于促进可测试性和代码重用。
当 Controller 类决定将 HTML 响应呈现回客户端时,它负责将呈现响应所需的所有数据显式传递到视图模板。 视图模板绝不应执行任何数据检索或应用程序逻辑,而应将自身限制为仅具有由控制器传递给它的模型/数据驱动的呈现代码。
现在,DinnersController 类传递给视图模板的模型数据简单而直接 – Index () 为 Dinner 对象的列表,在 Details () 、Edit () 、Create () 和 Delete () 的情况下为单个 Dinner 对象。 当我们向应用程序添加更多 UI 功能时,通常需要传递的不仅仅是此数据,才能在视图模板中呈现 HTML 响应。 例如,我们可能需要将“编辑和创建视图”中的“国家/地区”字段从 HTML 文本框更改为下拉列表。 我们可能希望从动态填充的受支持国家和地区的列表生成它,而不是对视图模板中的国家和地区名称下拉列表进行硬编码。 我们需要一种方法将 Dinner 对象 和支持 的国家和地区列表从控制器传递到视图模板。
让我们看一下实现此目的的两种方法。
使用 ViewData 字典
Controller 基类公开“ViewData”字典属性,该属性可用于将其他数据项从 Controllers 传递到 Views。
例如,为了支持我们想要将“编辑”视图中的“国家/地区”文本框从 HTML 文本框更改为下拉列表的方案,我们可以更新 Edit () 操作方法,以便除了传递 Dinner 对象) 可用作“国家/地区”下拉列表模型的 SelectList 对象之外,还传递 (。
//
// 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” 关键字 (keyword) 将字典中的类型强制转换为 SelectList。
现在,当我们运行应用程序并在浏览器中访问 /Dinners/Edit/1 URL 时,我们将看到编辑 UI 已更新为显示国家和地区的下拉列表,而不是文本框:
由于我们还在发生错误) 的情况下从 HTTP-POST Edit 方法 (呈现“编辑视图”模板,因此我们希望确保我们还更新此方法,以在错误情况下呈现视图模板时将 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”模式。 使用此模式时,我们将创建针对特定视图方案进行优化的强类型类,并公开视图模板所需的动态值/内容的属性。 然后,我们的控制器类可以填充这些视图优化类并将其传递给视图模板以使用。 这将在视图模板中启用类型安全、编译时检查和编辑器智能感知。
例如,若要启用晚餐表单编辑方案,我们可以创建一个如下所示的“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));
}
然后,我们将更新视图模板,使其需要“DinnerFormViewModel”而不是“Dinner”对象,方法是更改 edit.aspx 页面顶部的“inherits”属性,如下所示:
Inherits="System.Web.Mvc.ViewPage<NerdDinner.Controllers.DinnerFormViewModel>
执行此操作后,视图模板中“模型”属性的智能感知将更新,以反映我们传递它的 DinnerFormViewModel 类型的对象模型:
然后,我们可以更新视图代码以处理它。 请注意下面我们如何不更改要创建的输入元素的名称, (表单元素仍将命名为“Title”、“Country”) ,但我们更新 HTML 帮助程序方法以使用 DinnerFormViewModel 类检索值:
<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>
我们还将更新 Edit post 方法,以在呈现错误时使用 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 类,以在这些方法中启用“国家/地区”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 Create 方法的实现:
//
// 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。