3.4 视图模型
-----------------------------------------------------------------
问题的提出:
----------->?
在Model层定义的数据模型会运用在整个项目里,无论是由Controller进行信息操作(CRUD),还是在View里面参考Modl层定义的数据模型都会用到。不过,毕竟Model层创建数据模型时,主要是以数据为中心来定义,并不一定适用所有View层的要求。
以会员信息为例,同一个Member数据模型,在会员注册时输入的字段可能是Username、Password、Name、Email,等等,而且每个字段都设置为必填。而同样用到Member数据名,在开发会员登录窗体时,却只要输入Username与Password即可,在登录页面是不用输入Name与Email字段的,因此,若你在会员登录窗体使用Member数据模型进行参考时,就会导致进行数据模型绑定(Model Binding)时发生字段验证失败的问题,此时就需要使用额外定义的ViewModel当作会员登录窗体的数据模型。
这类专门提供给View使用的数据模型,通常称为数据视图模型(ViewModel)。
1. 什么是视图模型
视图模型的目的十分简单,它是一个专门为用于视图而设计的模型,它提供了一个建立在域模型之上的简化接口,以保持视图决策最小化。
2. 用于显示的视图模型
(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.GuestbookEntries 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 StoreDBContext:DbContext
{
public DbSet<Customer> Customers { get; set; }
public DbSet<Order> Orders { get; set; }
public DbSet<Product> Products { get; set; }
}
<add name="StoreDBContext" providerName="System.Data.SqlClient".....
商店的管理区可能包含一个Customer Summary(客户摘要)页面,该页面列出每个客户及其订单数。
建立这个UI的一个可选办法是可以直接通过域模型来建立该屏幕。我们可以从数据库取回客户列表,然后将其传递给视图,视图循环遍历客户列表并构建表格。当到达最后一列Most Recent Order Date(最近的订单日期)时,视图不得不循环遍历客户的Orders集合,以得出最近的一份订单。
这种方法的一个问题,是它使视图十分复杂。为了使视图尽可能是可维护的,它应该尽可能简化,将复杂的循环和计算逻辑放在更高层执行,视图唯一应该做的只是显示这种计算的结果。通过实现明确表示该表格的视图模型,可以做到这一点。
a. 建立视图模型
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; } }
b. 实现控制器代码
public class CustomerController : Controller { private StoreDBContext _db = new StoreDBContext(); 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()); } }
c. 设计视图
控制器与视图共享了一个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>
别忘记在数据表中输入相应的数据,测试运行,成功!
3. 表现用户输入的用户模型
一个强大的表现模型能够使视图易于使用数据,一个强大的输入模型也能够使应用程序易于使用用户输入。代替那种使用易于出错的字符串键和检测希望与输入元素名匹配的请求值,我们可以利用ASP.NET MVC的特性来使用强大的输入模型。
(1)设计输入模型
图中的简单表单有两个文本框和一个复选框。作为一种应用程序的特征,该表单也值得作为一个正式化的表现:一个类。
public class NewCustomerInput
{
public string FirstName { get; set; }
public string LastName { get; set; }
public bool Active { get; set; }
}
(2)编写控制器代码
[HttpGet] public ActionResult New() { return View(); } [HttpPost] public ActionResult New(NewCustomerInput input) { Customer customer = new Customer(); customer.FirstName = input.FirstName; customer.LastName = input.LastName; customer.Active = input.Active; db.Customers.Add(customer); db.SaveChanges(); return RedirectToAction("Index"); }
(3)设计视图
3. 用于显示和输入的复杂模型
图显示了一个表格,表格的每一行列出了客户摘要以及一个输入元素。最终用户可以看到客户的摘要列表,但也可以修改客户的状态;如果应该激活某用户,则选中其复选框。
(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()
{
ViewBag.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();
}
[HttpPost]
public ActionResult Save(List<CustomerSummary.CustomerSummaryInput> inputs)
{
foreach(var item in inputs)
{
Customer cus = _db.Customers.Where(x => x.Id == item.Number).FirstOrDefault();
cus.Active = item.Active;
_db.SaveChanges();
}
return RedirectToAction("Index");
} }
(3)设计视图
@model IEnumerable<CustomerSytem.Models.CustomerSummary.CustomerSummaryInput>
<div>
<h2>Customer Summary</h2>
@using (Html.BeginForm("Save", "Home"))
{
<table>
<tr>
<th>Name</th>
<th>Service Level</th>
<th>Order Count</th>
<th>Most Recent Order Date</th>
<th>Active?</th>
</tr>
@{ int i = 0;}
@foreach (var summary in ViewBag.data)
{
<tr>
<td>@summary.Name</td>
<td>@summary.ServiceLevel</td>
<td>@summary.OrderCount</td>
<td>@summary.MostRecentOrderDate</td>
<td>
@{
string active = "[" + i + "].Active";
string number = "[" + i + "].Number";
bool isChecked = summary.Input.Active;
int id = summary.Input.Number;
i++;
}
@Html.CheckBox(active, isChecked)
@Html.Hidden(number,id)
</td>
</tr>
}
</table>
<button name="submit">Change Status</button>
}
</div>