编辑:我在博客上更新了这个答案:
http://www.samwheat.com/post/The-function-of-ViewModels-in-MVC-web-development
我的回答有点冗长,但我认为将视图模型与其他类型的常用模型进行比较以了解它们为何不同以及它们为何必要是很重要的。
总结并直接回答所提出的问题:
一般来说,视图模型是一个包含渲染视图所需的所有属性和方法的对象。视图模型属性通常与客户和订单等数据对象相关,此外,它们还包含与页面或应用程序本身相关的属性,例如用户名、应用程序名称等。视图模型提供了一个方便的对象来传递给渲染引擎来创建一个 HTML 页面。使用视图模型的众多原因之一是视图模型提供了一种对某些表示任务进行单元测试的方法,例如处理用户输入、验证数据、检索数据以进行显示等。
这里是实体模型(又名 DTO,又名模型)、表示模型和视图模型的比较。
数据传输对象又名“模型”
数据传输对象 (DTO) 是一个类,其属性与数据库中的表架构相匹配。 DTO 因其在数据存储中往返数据的常见用途而得名。
DTO 的特点:
- 是业务对象 - 它们的定义取决于应用程序数据。
- 通常只包含属性 - 没有代码。
- 主要用于在数据库之间传输数据。
- 属性与数据存储中特定表上的字段完全匹配或紧密匹配。
数据库表通常被规范化,因此 DTO 通常也被规范化。这使得它们在呈现数据方面的用途有限。但是,对于某些简单的数据结构,它们通常做得很好。
以下是 DTO 的两个示例:
public class Customer
{
public int ID { get; set; }
public string CustomerName { get; set; }
}
public class Order
{
public int ID { get; set; }
public int CustomerID { get; set; }
public DateTime OrderDate { get; set; }
public Decimal OrderAmount { get; set; }
}
演示模型
表示模型是一个实用程序类,用于在屏幕或报表上呈现数据。表示模型通常用于对由来自多个 DTO 的数据组成的复杂数据结构进行建模。表示模型通常表示数据的非规范化视图。
表示模型的特点:
- 是业务对象 - 它们的定义取决于应用程序数据。
- 主要包含属性。代码通常仅限于格式化数据或将其转换为 DTO 或从 DTO 转换。演示模型不应包含业务逻辑。
- 经常呈现非规范化的数据视图。也就是说,它们通常会结合来自多个 DTO 的属性。
- 通常包含与 DTO 不同的基本类型的属性。例如,美元金额可以表示为字符串,因此它们可以包含逗号和货币符号。
- 通常由它们的使用方式及其对象特性来定义。换句话说,一个简单的 DTO 用作渲染网格的支持模型实际上也是该网格上下文中的表示模型。
表示模型在“需要时”和“需要时”使用(而 DTO 通常与数据库模式相关联)。表示模型可用于为整个页面、页面上的网格或页面上的网格上的下拉菜单建模数据。表示模型通常包含其他表示模型的属性。表示模型通常是为单一用途而构建的,例如在单个页面上呈现特定网格。
演示模型示例:
public class PresentationOrder
{
public int OrderID { get; set; }
public DateTime OrderDate { get; set; }
public string PrettyDate { get { return OrderDate.ToShortDateString(); } }
public string CustomerName { get; set; }
public Decimal OrderAmount { get; set; }
public string PrettyAmount { get { return string.Format("{0:C}", OrderAmount); } }
}
查看模型
视图模型类似于表示模型,因为它是用于呈现视图的支持类。但是,它的构造方式与表示模型或 DTO 有很大不同。视图模型通常包含与表示模型和 DTO 相同的属性,因此,它们经常会相互混淆。
视图模型的特征:
- 是用于呈现页面或屏幕的单一数据源。通常,这意味着视图模型将公开页面上的任何控件正确呈现自身所需的每个属性。使视图模型成为视图的单一数据源,极大地提高了其单元测试的能力和价值。
- 是复合对象,包含由应用程序数据和应用程序代码使用的属性组成的属性。在设计可重用视图模型时,这一特性至关重要,下面的示例将对此进行讨论。
- 包含应用程序代码。视图模型通常包含在渲染期间和用户与页面交互时调用的方法。此代码通常与事件处理、动画、控件的可见性、样式等有关。
- 包含调用业务服务以检索数据或将其发送到数据库服务器的代码。此代码经常被错误地放置在控制器中。从控制器调用业务服务通常会限制视图模型对单元测试的有用性。需要明确的是,视图模型本身不应包含业务逻辑,而应调用包含业务逻辑的服务。
- 通常包含属于其他页面或屏幕的其他视图模型的属性。
- 写成“每页”或“每屏”。通常为应用程序中的每个页面或屏幕编写一个唯一的视图模型。
- 通常从基类派生,因为大多数页面和屏幕都具有共同的属性。
查看模型组合
如前所述,视图模型是复合对象,因为它们将应用程序属性和业务数据属性结合在一个对象上。在视图模型上使用的常用应用程序属性示例如下:
- 用于显示应用程序状态的属性,例如错误消息、用户名、状态等。
- 用于格式化、显示、样式化或动画控件的属性。
- 用于数据绑定的属性,例如保存用户输入的中间数据的列表对象和属性。
以下示例说明了为什么视图模型的复合性质很重要,以及我们如何才能最好地构建一个高效且可重用的视图模型。
假设我们正在编写一个 Web 应用程序。应用程序设计的要求之一是页面标题、用户名和应用程序名称必须显示在每个页面上。如果我们想创建一个页面来展示一个展示订单对象,我们可以修改展示模型如下:
public class PresentationOrder
{
public string PageTitle { get; set; }
public string UserName { get; set; }
public string ApplicationName { get; set; }
public int OrderID { get; set; }
public DateTime OrderDate { get; set; }
public string PrettyDate { get { return OrderDate.ToShortDateString(); } }
public string CustomerName { get; set; }
public Decimal OrderAmount { get; set; }
public string PrettyAmount { get { return string.Format("{0:C}", OrderAmount); } }
}
这种设计可能可行……但如果我们想创建一个显示订单列表的页面怎么办? PageTitle、UserName 和 ApplicationName 属性将重复使用,并且难以使用。另外,如果我们想在类的构造函数中定义一些页面级的逻辑呢?如果我们为每个将要显示的订单创建一个实例,我们就不能再这样做了。
组合优于继承
我们可以通过以下方式重构订单展示模型,使其成为真正的视图模型,并有助于显示单个 PresentationOrder 对象或 PresentationOrder 对象的集合:
public class PresentationOrderVM
{
// Application properties
public string PageTitle { get; set; }
public string UserName { get; set; }
public string ApplicationName { get; set; }
// Business properties
public PresentationOrder Order { get; set; }
}
public class PresentationOrderVM
{
// Application properties
public string PageTitle { get; set; }
public string UserName { get; set; }
public string ApplicationName { get; set; }
// Business properties
public List<PresentationOrder> Orders { get; set; }
}
查看上面的两个类,我们可以看到考虑视图模型的一种方式是它是一个表示模型,其中包含另一个表示模型作为属性。顶级表示模型(即视图模型)包含与页面或应用程序相关的属性,而表示模型(属性)包含与应用程序数据相关的属性。
我们可以更进一步,创建一个基础视图模型类,它不仅可以用于 PresentationOrders,还可以用于任何其他类:
public class BaseViewModel
{
// Application properties
public string PageTitle { get; set; }
public string UserName { get; set; }
public string ApplicationName { get; set; }
}
现在我们可以像这样简化我们的 PresentationOrderVM:
public class PresentationOrderVM : BaseViewModel
{
// Business properties
public PresentationOrder Order { get; set; }
}
public class PresentationOrderVM : BaseViewModel
{
// Business properties
public List<PresentationOrder> Orders { get; set; }
}
我们可以通过将 BaseViewModel 设为通用来使其更具可重用性:
public class BaseViewModel<T>
{
// Application properties
public string PageTitle { get; set; }
public string UserName { get; set; }
public string ApplicationName { get; set; }
// Business property
public T BusinessObject { get; set; }
}
现在我们的实现很轻松:
public class PresentationOrderVM : BaseViewModel<PresentationOrder>
{
// done!
}
public class PresentationOrderVM : BaseViewModel<List<PresentationOrder>>
{
// done!
}