对于一个在线商店,域模型可能由表现产品、订单、客户等的类所组成,它对定义这些实体的数据和业务规则都进行了封闭,这种模型用作建立用户界面以及定义业务规则的基础。尽管这种办法可能适合某些应用程序(通常是有简单域的小型应用程序),但经常会带来麻烦,特别是当应用程序增长,且要求UI偏离业务逻辑需求时,一个利害冲突可能会导致过于复杂和不可维护的软件。
这个问题的解决方法是引入视图模型(View Model),以简化渲染用户界面所需的逻辑。我们将考擦如何定义视图模型,以及用来将用户界面的数据回发给控制器层的输入模型。
一、什么是视图模型
视图模型的目的十分简单,它是一个专门为用于视图而设计的模型,它提供了一个建立在域模型之上的简化接口,以保持视图决策最小化。
1.留言本例子中添加视图模型
在留言本这个例子中,GuestbookEntry类既作为域模型,也作为视图模型。它既表现了数据库中存储的数据,也表现了用户界面中的字段。
对于像留言簿这样的小型应用程序,这是足够的。但是,随着应用程序复杂性的提升,当复杂的用户界面结构必须不直接映射模型的结构时,即视图数据与模型结构不同,往往需要将两者分开。比如,让我们对Guestbook应用程序添加一个新的页面,以显示每个用户已递交了多少评论的摘要,如图所示。
为了创建这一屏幕,首先需要创建一个视图模型,它每一列含有一个属性——用户名和已递交的评论数:
public class CommentSummary { public string UserName { get; set; } public string NumberOfComments { get; set; } }
现在需要创建一个控制器动作,查询数据库以获取显示所必需的数据,然后将其注入CommentSummary类实例。
public ActionResult CommentSummary() { var entries = from entry in _db.Entries group entry by entry.Name into groupedByName orderby groupedByName.Count() descending select new CommentSummary { NumberOfComments = groupedByName.Count(), UserName = groupedByName.Key }; return View(entries.ToList()); }
这里使用了LINQ来查询留言簿数据,并按用户名对递交的评论进行分组。接着将数据投影成视图模型实例,然后便可以将其传递给视图。
@model IEnumerable<Guestbook.Models.CommentSummary> <table> <tr> <th>Number of comments</th> <th>User name</th> </tr> @foreach(var summaryRow in Model) { <tr> <td>@summaryRow.NumberOfComments</td> <td>@summaryRow.UserName</td> </tr> } </table>
2.在线商店示例
让我们从一个简单的在线商店示例开始。它可能包含Customer、Order和Product类,这些类对应于关系数据库中的表,并使用对象关系映射器进行映射。
public class Customer { public int Number { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public bool Active { get; set; } public ServiceLevel ServiceLevel { get; set; } public IEnumerable<Order> Orders { get; set; } } public enum ServiceLevel { Standard, Premier } public class Order { public DateTime Date { get; set; } public IEnumerable<Product> Product { get; set; } public decimal TotalAmount { get; set; } } public class Product { public string Name { get; set; } public decimal Cost { get; set; } }
商店的管理区可能包含一个Customer Summary(客户摘要)页面,该页面列出每个客户及其订单数。
建立这个UI的一个可选办法是可以直接通过域模型来建立该屏幕。我们可以从数据库取回客户列表,然后将其传递给视图,视图循环遍历客户列表并构建表格。当到达最后一列Most Recent Order Date(最近的订单日期)时,视图不得不循环遍历客户的Orders集合,以得出最近的一份订单。
这种方法的一个问题,是它使视图十分复杂。为了使视图尽可能是可维护的,它应该尽可能简化,将复杂的循环和计算逻辑放在更高层执行,视图唯一应该做的只是显示这种计算的结果。通过实现明确表示该表格的视图模型,可以做到这一点。
(1)建立视图模型
public class CustomerSummary { public string Name { get; set; } public string Active { get; set; } public string ServiceLevel { get; set; } public string OrderCount { get; set; } public string MostRecentOrderDate { get; set; } }
(2)交付表现模型
public class CustomerSummaryController : Controller { private CustomerSummaries _customerSummaries = new CustomerSummaries(); public ActionResult Index() { IEnumerable<CustomerSummary> summaries = _customerSummaries.GetAll(); return View(summaries); } }
public class CustomerSummaries { public IEnumerable<CustomerSummary> GetAll() { return new[] { new CustomerSummary { Active = "Yes", Name = "John Smith", MostRecentOrderDate = "02/07/10", OrderCount = "42", ServiceLevel = "Standard" }, new CustomerSummary { Active = "Yes", Name = "Susan Power", MostRecentOrderDate = "02/02/10", OrderCount = "1", ServiceLevel = "Standard" }, new CustomerSummary { Active = "Yes", Name = "Jim Doe", MostRecentOrderDate = "02/09/10", OrderCount = "7", ServiceLevel = "Premier" }, }; } }
(3)ViewData.Model
控制器与视图共享了一个ViewDataictionary类型的对象,其名称为ViewData。它有一个颇具特色的Model属性。当我们在清单5.3中调用return View(summaries)时,ViewData.Model会自动以CustomerSummary对象列表进行填充,这便做好了在视图中显示的准备。Model属性也是强类型的,因此视图确切地知道所期望的类型,也使开发者能够利用IDE智能感应之类的特性,以及对变量重命名的支持。Razor视图引擎遮盖了其中大部分内部机制,这使得定义模型类型变得简单。视图可以用@model指示符来描述它的模型类型:
@model IEnumerable<StoreCh05.Models.CustomerSummary>
<table> <tr> <th>Name</th> <th>Active?</th> <th>Service Level</th> <th>Order Count</th> <th>Most Recent Order Date</th> </tr> @foreach (var summary in Model) { <tr> <td>@summary.Name</td> <td>@summary.Active</td> <td>@summary.ServiceLevel</td> <td>@summary.OrderCount</td> <td>@summary.MostRecentOrderDate</td> </tr> } </table>
二、表现用户输入
一个强大的表现模型能够使视图易于使用数据,一个强大的输入模型也能够使应用程序易于使用用户输入。代替那种使用易于出错的字符串键和检测希望与输入元素名匹配的请求值,我们可以利用ASP.NET MVC的特性来使用强大的输入模型。
1.设计输入模型
图中的简单表单有两个文本框和一个复选框。作为一种应用程序的特征,该表单也值得作为一个正式化的表现:一个类。
public class NewCustomerInput { public string FirstName { get; set; } public string LastName { get; set; } public bool Active { get; set; } }
2.在视图中表示输入模型
public class CustomerController : Controller { public ViewResult New() { return View(); } public ViewResult Save(NewCustomerInput input) { return View(input); } }
@model StoreCh052.Models.NewCustomerInput <div> <form action="@Url.Action("Save")" method="post"> <fieldset> <div> @Html.LabelFor(x => x.FirstName) @Html.TextBoxFor(x => x.FirstName) </div> <div> @Html.LabelFor(x => x.LastName) @Html.TextBoxFor(x => x.LastName) </div> <div> @Html.LabelFor(x => x.Active) @Html.CheckBoxFor(x => x.Active) </div> <div> <button name="save"> Save</button> </div> </fieldset> </form> </div>
三、用于显示和输入的复杂模型