知识点5-1:视图模型
对于一个在线商店,域模型可能由表现产品、订单、客户等的类所组成,它对定义这些实体的数据和业务规则都进行了封闭,这种模型用作建立用户界面以及定义业务规则的基础。尽管这种办法可能适合某些应用程序(通常是有简单域的小型应用程序),但经常会带来麻烦,特别是当应用程序增长,且要求UI偏离业务逻辑需求时,一个利害冲突可能会导致过于复杂和不可维护的软件。
这个问题的解决方法是引入视图模型(View Model),以简化渲染用户界面所需的逻辑。我们将考擦如何定义视图模型,以及用来将用户界面的数据回发给控制器层的输入模型。
一、什么是视图模型
视图模型的目的十分简单,它是一个专门为用于视图而设计的模型,它提供了一个建立在域模型之上的简化接口,以保持视图决策最小化。
1.留言本例子中添加视图模型
在留言本这个例子中,GuestbookEntry类既作为域模型,也作为视图模型。它既表现了数据库中存储的数据,也表现了用户界面中的字段。
对于像留言簿这样的小型应用程序,这是足够的。但是,随着应用程序复杂性的提升,当复杂的用户界面结构必须不直接映射模型的结构时,即视图数据与模型结构不同,往往需要将两者分开。比如,让我们对Guestbook应用程序添加一个新的页面,以显示每个用户已递交了多少评论的摘要,如图所示。
为了创建这一屏幕,首先需要创建一个视图模型,它每一列含有一个属性——用户名和已递交的评论数:
public class CommentSummary
{
public string UserName { get; set; }
public int 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 Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public bool Active { get; set; } public ServiceLevel ServiceLevel { get; set; } public virtual ICollection<Order> Orders { get; set; } } public enum ServiceLevel { Standard, Premier }
public class Order { public int Id { get; set; } public DateTime Date { get; set; } public decimal TotalAmount { get; set; } public Customer customer { get; set; } public virtual ICollection<Product> Products { get; set; } }
public class Product { public int Id { get; set; } public string Name { get; set; } public decimal Cost { get; set; } }
public class StoreCh051DBContext:DbContext { public DbSet<Customer> Customers { get; set; } public DbSet<Order> Orders { get; set; } public DbSet<Product> Products { get; set; } }
<add name="StoreCh051DBContext" providerName="System.Data.SqlClient".....
商店的管理区可能包含一个Customer Summary(客户摘要)页面,该页面列出每个客户及其订单数。
建立这个UI的一个可选办法是可以直接通过域模型来建立该屏幕。我们可以从数据库取回客户列表,然后将其传递给视图,视图循环遍历客户列表并构建表格。当到达最后一列Most Recent Order Date(最近的订单日期)时,视图不得不循环遍历客户的Orders集合,以得出最近的一份订单。
这种方法的一个问题,是它使视图十分复杂。为了使视图尽可能是可维护的,它应该尽可能简化,将复杂的循环和计算逻辑放在更高层执行,视图唯一应该做的只是显示这种计算的结果。通过实现明确表示该表格的视图模型,可以做到这一点。
(1)建立视图模型
public class CustomerSummary { public string Name { get; set; } public bool Active { get; set; } public ServiceLevel ServiceLevel { get; set; } public int OrderCount { get; set; } public DateTime MostRecentOrderDate { get; set; } }
(2)交付表现模型
public class CustomerSummaryController : Controller { private StoreCh051DBContext _db = new StoreCh051DBContext(); public ActionResult Index() { var data = from c in _db.Customers orderby c.Orders.Count() descending select new CustomerSummary { Name = c.FirstName + " " + c.LastName, Active = c.Active, ServiceLevel = c.ServiceLevel, OrderCount = c.Orders.Count(), MostRecentOrderDate = (from d in c.Orders orderby d.Date descending select d.Date).FirstOrDefault() }; return View(data.ToList()); } }
(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>
@model StoreCh052.Models.NewCustomerInput @{ ViewBag.Title = "Save"; } <h2>Customer</h2> <div> Customer was saved. </div> <div>First Name: @Model.FirstName </div> <div>Last Name: @Model.LastName </div> <div>Active: @Model.Active </div>
三、用于显示和输入的复杂模型
图显示了一个表格,表格的每一行列出了客户摘要以及一个输入元素。最终用户可以看到客户的摘要列表,但也可以修改客户的状态;如果应该激活某用户,则选中其复选框。
1.设计显示和输入的组合模型
public class CustomerSummary
{
public string Name { get; set; }
public ServiceLevel ServiceLevel { get; set; }
public int OrderCount { get; set; }
public DateTime MostRecentOrderDate { get; set; }
public CustomerSummaryInput Input { get; set; }
public class CustomerSummaryInput
{
public int Number { get; set; }
public bool Active { get; set; }
}
}
2.使用输入模型
public class CustomerSummaryController : Controller { private StoreCh051DBContext _db = new StoreCh051DBContext(); public ActionResult Index() { var data = from c in _db.Customers orderby c.Orders.Count() descending select new CustomerSummary { Name = c.FirstName + " " + c.LastName, ServiceLevel = c.ServiceLevel, OrderCount = c.Orders.Count(), MostRecentOrderDate = (from d in c.Orders orderby d.Date descending select d.Date).FirstOrDefault(), Input = new CustomerSummary.CustomerSummaryInput { Number = c.Id, Active = c.Active } }; return View(data.ToList()); } [HttpPost] public ActionResult Save(int number,bool active) { Customer customer = _db.Customers.Find(number); customer.Active = active; _db.Entry(customer).State = EntityState.Modified; _db.SaveChanges(); return RedirectToAction("Index"); } }
Index视图
@model IEnumerable<StoreCh05.Models.CustomerSummary> <div> <h2>Customer Summary</h2> @using(Html.BeginForm("Save","CustomerSummary")) { <table> <tr> <th>Name</th> <th>Service Level</th> <th>Order Count</th> <th>Most Recent Order Date</th> <th>Active?</th> </tr> @foreach (var summary in Model) { <tr> <td>@summary.Name</td> <td>@summary.ServiceLevel</td> <td>@summary.OrderCount</td> <td>@summary.MostRecentOrderDate</td> <td> @Html.CheckBox("Active", summary.Input.Active) @Html.Hidden("Number",summary.Input.Number) </td> </tr> } </table> <button name="submit">Change Status</button> } </div>