演示模型实战
在 ASP.NET 网站、Windows Forms 和 WPF 中使用 Presentation Model 模式
引言
著名的 Model-View-Controller (MVC) 设计模式将表示层与域/数据访问层分离。此外,MVC 还将表示逻辑分解为视图和控制器。
Presentation Model 和 Model View Presenter (MVP) 是 Model-View-Controller (MVC) 设计模式的变体,其中控制器变成 Presentation Model 和 Presenter,并以不同的方式组织状态、逻辑和引用。
Presentation Model 的主要特点
- 状态位于 Presentation Model 中
- 逻辑位于 Presentation Model 中
- 视图观察模型并据此更新
- 视图“了解”Presentation Model
- Presentation Model 不“了解”视图
Model View Presenter 有两种形式,分别称为 Supervising Controller 和 Passive View。它们共享相同的关键特征。
Model View Presenter 的主要特点
- 状态位于视图中
- 逻辑位于 Presenter 中
- Presenter 观察视图
- Presenter 更新视图
- Presenter “了解”视图
- 视图不“了解”Presenter
Presentation Model 是一种将表示行为从视图中提取出来的模式。它提供了一个集中的位置来存储状态/数据,以及一个完全独立于用于显示的视图的集中式事件源。
在实际项目中,我更喜欢使用 Presentation Model。本文将展示一个用于管理客户业务实体的设计过程,并说明 Presentation Model 如何应用于 ASP.NET 网站、Windows Forms 应用程序和 WPF 应用程序。
让我们开始分析一个虚构的业务实体——客户管理应用程序的需求。
需求分析
许多业务管理流程都可以抽象为以下场景:
- 用户搜索业务实体
- 用户查看实体列表
- 用户从列表中选择一个实体
- 用户查看所选实体的详细信息
- 用户操作所选实体,例如修改、删除等。
我们的故事是关于管理客户这个示例业务实体的。用例可以描述如下:
用例名称
搜索和查看客户信息
目标
用户搜索客户并查看所选客户的详细信息。
先决条件
用户启动应用程序
事件流程
- 系统在列表中显示所有客户及其数量
- 用户通过键入搜索文本来搜索客户
- 系统将搜索文本与客户的名字、姓氏和地址进行匹配
如果搜索文本为空,系统将显示所有客户 - 系统显示与搜索文本匹配的客户列表
- 系统根据搜索结果显示客户数量
- 用户选择一个客户
- 系统显示所选客户的详细信息
- 用户可以重复搜索
设计考虑因素
该应用程序可以设计为 ASP.NET 网站、Windows Forms 应用程序或 WPF 应用程序。本文将使用 Presentation Model 展示所有这三种应用程序形式。为了开始设计,首先来看一下网站上的一些用户界面选项。
在网站上,通常有三种实现此客户管理的方法:
选项 1:列表页和弹出详细信息页
此选项显示客户列表,每行都有一个超链接。单击超链接时,将显示一个新的弹出窗口,其中包含客户的详细信息。

选项 2:列表页和切换到详细信息页
虽然第一种选项是许多早期 Web 应用程序中常用的模式,但较新的 Web 应用程序会在同一窗口中显示详细信息,以避免弹出窗口。

选项 3:列表页和嵌入式详细信息页
与选项 2 类似,不会有弹出窗口,而且此选项在显示详细信息时也不会隐藏列表。它将列表和详细信息一起显示。

用户体验设计师通常会决定在最终产品中使用哪种选项。这个决定基于用户的反馈以及 UI 布局和外观设计。
这个决定可能会在项目中间发生变化。例如,最初选择了选项 1。然后用户报告说他们屏蔽了弹出窗口,因此必须改为选项 2。最后,由于布局和字体使屏幕有很大的空间,设计师可能会问我们是否可以同时显示列表和详细信息。然后就变成了选项 3。
架构设计应预见潜在的需求变更,并在发生变更时对代码的影响降到最低。换句话说,代码应该尽可能地可重用。
好消息是我们可以使用用户控件。用户控件是实现可重用的绝佳技术。它们被称为应用程序构建块。它创建了一个客户列表用户控件和一个客户详细信息用户控件。所有这三个选项都可以基于它们构建。
在选项 1 中,一个页面托管客户列表用户控件。一个弹出页面托管客户详细信息用户控件。
在选项 2 中,一个页面托管客户列表用户控件和客户详细信息用户控件。选择客户时,隐藏客户列表用户控件并显示客户详细信息用户控件。
在选项 3 中,一个页面托管客户列表用户控件和客户详细信息用户控件。选择客户时,显示客户详细信息用户控件。有时可能需要滚动屏幕到客户详细信息区域的顶部。
再次的好消息是,无论屏幕布局或窗口排列如何变化,后台的数据模型实际上是相同的。这是一个客户列表和一个选定的客户。
我们需要一个类来保存客户列表和一个选定的客户。如果由于用户输入搜索文本而导致客户列表发生更改,它将发送一个“客户列表已更改”的事件。如果用户选择了一个客户,它将发送一个“选定客户已更改”的事件。
用户控件连接到此类并拉取客户列表或选定的客户以数据绑定到 UI 元素,例如网格视图或表单视图。
用户控件还监听此类发出的事件。如果客户列表已更改,则重新绑定网格视图。或者,如果选定的客户已更改,则重新绑定表单视图。
很自然地,Presentation Model 模式就派上用场了。上面描述的类正是 Presentation Model,我们将其命名为 `CustomerPresentationModel`。
`CustomerPresentationModel` 类具有用于保存客户列表和选定客户数据的属性。
public interface ICustomerPresentation : INotifyPropertyChanged
{
IEnumerable<Customer> Items { get; set; }
Customer SelectedItem { get; set; }
int ItemCount { get; set; }
}
为了发送事件,我们可以定义一个自定义事件处理程序。但是,由于 Windows Forms 数据绑定和 WPF 数据绑定依赖于 `INotifyPropertyChanged` 接口,因此我们使用此 `INotifyPropertyChanged` 接口。
`CustomerPresentationModel` 类是通用 Presentation Model 类 `PresentationModel<T>` 的子类。
public class CustomerPresentationModel :
PresentationModel<Customer>, ICustomerPresentation
{
//...
}
public abstract class PresentationModel<T> : INotifyPropertyChanged
{
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
protected int itemCount;
public int ItemCount
{
get { return itemCount; }
set { itemCount = value; NotifyPropertyChanged("ItemCount"); }
}
protected IEnumerable items;
public virtual IEnumerable Items
{
get { return GetItems(); }
set { items = value; NotifyPropertyChanged("Items"); }
}
protected abstract IEnumerable GetItems();
private T selectedItem;
public virtual T SelectedItem
{
get { return selectedItem; }
set { selectedItem = value; NotifyPropertyChanged("SelectedItem"); }
}
protected void Reset()
{
Items = null;
SelectedItem = default(T);
}
}
`PresentationModel<T>` 类是使用泛型的基类。有了这个类,我们可以将 Presentation Model 设计模式应用于将来各种业务实体,例如订单、产品等。
现在,我们将只关注 `CustomerPresentationModel` 及其用于构建三种类型应用程序的使用。
构建 ASP.NET 网站

构建 ASP.NET 网站的步骤如下:
|
|
这些步骤都很直接,但有一些值得注意的地方。
ASP.NET 网站可以是无状态的,这意味着对象通常在每次请求服务时创建和销毁。在这种情况下,由于 Presentation Model 被多个用户控件访问,因此它应该在请求开始时创建,并在请求结束前一直存在。但是,虽然 ASP.NET Framework 提供了应用程序范围的存储和会话存储来存储自定义对象,但与 JSP 不同,它没有请求范围的存储。因此,我们将 `CustomerPresentationModel` 存储在会话中。
public static CustomerPresentationModel Instance
{
get
{
CustomerPresentationModel instance = HttpContext.Current.Session["_c_"]
as CustomerPresentationModel;
if (instance == null)
{
instance = new CustomerPresentationModel();
HttpContext.Current.Session["_c_"] = instance;
}
return instance;
}
}
数据绑定到 Presentation Model 实例
ASP.NET 具有出色的声明性数据绑定模型,可以绑定到普通 .NET 对象。这是通过 ASP.NET `ObjectDataSource` 控件完成的,该控件将数据绑定控件(如 `GridView`、`FormView` 或 `DetailsView` 控件)连接到对象。
<asp:GridView .... DataSourceID="CustomerListDataSource" DataKeyNames="Id">
<Columns>
...
</Columns>
</asp:GridView>
<asp:ObjectDataSource ... DataObjectTypeName="Demo.DataModel.Customer"
TypeName="CustomerPresentationModel" SelectMethod="GetCustomerList"
OnObjectCreating="CustomerListDataSource_ObjectCreating" />
`ObjectDataSource` 暴露一个 `TypeName` 属性,该属性指定用于执行数据操作的对象类型(类名)。在我们的例子中,它是 `CustomerPresentationModel`。
默认情况下,`ObjectDataSource` 将通过默认构造函数(无参数)实例化 `CustomerPresentationModel` 实例,但我们已经在会话中有了 `CustomerPresentationModel` 对象。幸运的是,我们可以处理 `ObjectCreating` 事件,将 `CustomerPresentationModel` 对象从会话分配给 `ObjectDataSource` 的 `ObjectInstance` 属性。
protected void CustomerDataSource_ObjectCreating
(object sender, ObjectDataSourceEventArgs e)
{
e.ObjectInstance = CustomerPresentationModel.Instance;
}
当数据绑定控件需要数据时,`ObjectDataSource` 会调用 `CustomerPresentationModel` 实例的 `GetCustomerList` 方法(在 `ObjectDataSource` 的 `SelectMethod` 属性中声明)。该方法应返回任何 `Object` 或 `IEnumerable` 列表、集合或数组。
`CustomerPresentationModel` 以属性而非方法的形式暴露客户列表和选定的客户,因此这里我们需要一些包装才能与 `ObjectDataSource` 一起使用。
public IEnumerable<Customer> GetCustomerList()
{
return Items;
}
public Customer GetCustomer()
{
return SelectedItem;
}
监听事件
用户控件监听 Presentation Model 的事件。客户列表用户控件监视“项目已更改”事件以重新绑定网格视图。客户详细信息用户控件同样监视“选定项目已更改”事件。这不仅是为了刷新表单视图,也是为了在 `CustomerPresentationModel` 中选定的项目为 `null` 时隐藏自身。
在 CustomerEdit.ascx.cs 中,
void pm_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "SelectedItem")
{
if (pm.GetCustomer() != null)
{
CustomerFormView.DataBind();
Visible = true;
}
else
{
Visible = false;
}
}
}
事件驱动使得选项 2 和选项 3 非常容易实现。在选项 2 中,逻辑是如果没有选择客户,则显示列表并隐藏详细信息。在选项 3 中,不需要额外的逻辑。
当用户搜索客户列表时,`CustomerPresentationModel` 发出“项目已更改”事件,通知客户列表用户控件相应地刷新。
在 Customer2.aspx.cs 中,
void pm_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "SelectedItem")
{
this.CustomerList1.Visible = pm.GetCustomer() == null;
}
}
这种事件驱动机制的好处是,可能有多个用户控件需要通知和刷新。它还提供了一种同步不同用户控件状态的解决方案。
需要依赖注入框架
查看 CustomerList.ascx.cs 和 CustomerEdit.ascx.cs 的代码,您会发现重复的代码,例如:
CustomerPresentationModel pm;
protected void Page_Load(object sender, EventArgs e)
{
pm = CustomerPresentationModel.Instance;
System.Diagnostics.Debug.Assert(pm != null);
if (pm != null)
{
pm.PropertyChanged += new PropertyChangedEventHandler(pm_PropertyChanged);
}
}
protected void Page_Unload(object sender, EventArgs e)
{
System.Diagnostics.Debug.Assert(pm != null);
if (pm != null)
{
pm.PropertyChanged -= new PropertyChangedEventHandler(pm_PropertyChanged);
}
}
void pm_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
}
如果有一个 Dependency Injection 框架,那么上面的代码可以简化为类似这样的内容:
[PresentationMode(Scope= Session)]
CustomerPresentationModel pm;
[PropertyChangedEventSubscription]
void pm_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
}
我正在寻找一个可以用于 Web 应用程序的轻量级依赖注入框架。
Windows Forms 应用程序

我们已经创建了 ASP.NET 网站。现在是时候创建一个 Windows Forms 应用程序了。
在 ASP.NET 网站中使用的步骤也适用于此处,包括:
|
|
在 Windows Forms 应用程序中,`CustomPresentationModel` 可以作为单例存在。通过在 Visual Studio 中创建 `BindingSource`,数据绑定也非常方便。
但重要的是要记住,Windows Forms 数据绑定是由更改通知驱动的。这意味着只有当数据源通知数据已更改(通过提供通知事件)时,Windows Forms 才会更新用户界面元素。
在我们的例子中,数据源是 `CustomPresentationModel` 实例。当客户列表重新加载时,它会发出两个 `PropertyChanged` 事件。一个属性名更改为 `Items`,另一个属性名更改为 `ItemCount`。
绑定到 `ItemCount` 属性的标签自动更新,但网格似乎没有响应 `Items` 更改事件。为什么?
对于简单的属性到属性绑定,数据源需要通过提供“PropertyName
更改事件”或实现 `INotifyPropertyChanged` 接口来提供属性更改通知。
当数据源是列表时,数据源需要提供列表更改通知,该通知用于在列表中的项被添加、删除或删除时通知用户界面元素,通常通过 `IBindingList` 接口。
也就是说,要与 Windows Forms 数据绑定完全集成,规则是:在业务实体类中实现 `INotifyPropertyChanged` 接口,并为业务实体集合使用 `IBindingList` 接口。
我喜欢数据绑定,但犹豫是否要用它来污染我的实体和实体集合。我宁愿使用匿名方法通过代码刷新网格视图。
private void CustomerList_Load(object sender, EventArgs e)
{
customerPresentationModelBindingSource.DataSource =
CustomerPresentationModel.Instance;
customerBindingSource.DataSource =
CustomerPresentationModel.Instance.Items;
CustomerPresentationModel.Instance.PropertyChanged +=
delegate(object s, PropertyChangedEventArgs ev)
{
if (ev.PropertyName == "Items")
customerBindingSource.DataSource =
CustomerPresentationModel.Instance.Items;
};
}
}
在 Composite UI Application Block 中,有作为依赖注入容器的 Work Items,以及提供多对多、松耦合事件系统机制的 Event Broker。这个应用程序块是使用 Presentation Model 模式构建应用程序的理想平台。
WPF 应用程序

正如预期的那样,步骤与 ASP.NET 网站和 Windows Forms 应用程序中所使用的相同。
![]() |
|
在这里,`CustomPresentationModel` 像在 Windows Forms 应用程序中一样,以单例的形式存在。
绑定到客户列表的列表视图会随着 `Items` 更改事件而更新。除此之外,WPF 数据绑定还有其他一些很棒的功能。
在 WPF 中,我们可以直接连接到对象,而无需像 ASP.NET 中的 `ObjectDataSource` 和 Windows Forms 中的 `BindingSource` 这样的辅助工具。通常,我定义一个 `static` 资源来引用 `CustomPresentationModel` 实例。
<UserControl x:Class="Demo.WpfApp.CustomerEdit"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Width="550"
xmlns:local="clr-namespace:Demo.WpfApp">
<UserControl.Resources>
<ObjectDataProvider x:Key="controller"
ObjectType="{x:Type local:CustomerPresentationModel}"
MethodName="get_Instance" />
</UserControl.Resources>
还可以将数据绑定到 UI 元素的 `DataContext` 属性,并允许子元素继承数据源信息,从而简化绑定语法。这在 CustomerEdit.asmx 用户控件中得到了很好的演示,其中顶层网格的 `DataContext` 被绑定到选定的客户对象。网格内的元素随后仅使用路径绑定到客户对象的属性。
<UserControl x:Class="Demo.WpfApp.CustomerEdit"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Width="550"
xmlns:local="clr-namespace:Demo.WpfApp">
<UserControl.Resources>
<ObjectDataProvider x:Key="controller"
ObjectType="{x:Type local:CustomerPresentationModel}"
MethodName="get_Instance" />
<SolidColorBrush x:Key="LabelForegroundBrush" Color="#FFFFFFFF"/>
</UserControl.Resources>
<Grid DataContext="{Binding Source={StaticResource controller},
Path=SelectedItem}" Background="#FF595959">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Label Grid.Row="0" Foreground="{DynamicResource LabelForegroundBrush}">
First Name</Label>
<TextBox Grid.Row="0" Grid.Column="1" Width="150"
HorizontalAlignment="Left" VerticalAlignment="Center"
Text="{Binding Path=FirstName}">
</TextBox>
<Label Grid.Row="0" Grid.Column="2"
Foreground="{DynamicResource LabelForegroundBrush}">Last Name</Label>
<TextBox Grid.Row="0" Grid.Column="3" Width="150"
HorizontalAlignment="Left" VerticalAlignment="Center"
Text="{Binding Path=LastName}"/>
<Label Grid.Row="1" Foreground="{DynamicResource LabelForegroundBrush}">Address</Label>
<TextBox Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="3"
HorizontalAlignment="Stretch" VerticalAlignment="Center"
Text="{Binding Path=Address}"/>
<Label Grid.Row="2" Foreground="{DynamicResource LabelForegroundBrush}">Comments</Label>
<TextBox Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="3"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
MinHeight="50"
Text="{Binding Path=Comments}" TextWrapping="Wrap" />
</Grid>
</UserControl>
关于 WPF 的一些思考。WPF 旨在带来应用程序的“差异化用户体验”或“差异化 UI”。在我们的例子中,它可能是在“选定项”更改事件发生时,在应用程序的底部区域激活模拟 3D 效果或翻页效果的 storyboard 或 timeline。
差异化 UI 对 LOB(线业务)应用程序意味着什么?可能不仅仅是翻页。我正在等待看到 WPF Composite Client Application Block 将如何定义它。
评估
到目前为止,我们已经展示了如何在 ASP.NET 网站、Windows Forms 应用程序和 WPF 应用程序中使用 Presentation Model。尽管这里使用的 `Customer` 类是虚构的,但网站 UI 选项的设计实践实际上是一个真实的故事。直到后来找到以下参考文献,我们才意识到 Presentation Model 正在发挥作用:
- Karsten Lentzsch 的 Desktop Patterns and Data Binding Presentation
- John Gossman 关于 M-V-VM 模式用于开发 WPF 应用程序的博客 M-V-VM pattern for developing WPF applications
- Dan Crevier 的 精彩多部分博客文章
- Martin Fowler 的 Patterns of Enterprise Application Architecture
- Paul Williams 的博客 Presentation Patterns - Presentation Model
看起来,由于 Presentation Model 起源于 Smalltalk、Java 世界、FLEX 世界以及 .NET 世界,它们都在尝试和使用这种模式。这鼓励我们继续在这条道路上进行探索。
Microsoft Web Client Software Factory 和 Microsoft Smart Client Software Factory 使用 MVP 模式。有几点不如 Presentation Model 好。例如,每个视图,通常是一个用户控件,都需要一个 Presenter 类添加到现有的代码隐藏类/代码旁边类中。我觉得这增加了代码维护的难度。不如上面展示的 Presentation Model 清晰。
Microsoft Smart Client Software Factory 基于 Composite UI Application Block (CAB),它提供了许多出色的功能,例如依赖注入 (IoC) 框架、事件代理。使用 CAB 和 Presentation Model 构建应用程序既简单又有趣。
在 WPF 界,WPF 团队一直在推广 Model-View-ViewModel 模式。本质上,Model-View-ViewModel 就是 Presentation Model。它们是同一事物的两个标签。
在即将推出的 WPF Composite Client Application Block 中,看到 Microsoft 的 Patterns & Practices 团队将如何使用 Presentation Model 模式或 MVP 将非常有趣。依赖注入将如何工作?如何将 WPF 命令/事件处理扩展到 AOP 风格?
结论
正如本文所示,Presentation Model 被证明是一种有效的实践模式。尽管 ASP.NET、Windows Forms 和 WPF 是完全不同的技术,但它们可以共享同一个 Presentation Model 类。该模式带来了解耦数据模型和表示层的价值。它还可以与许多 .NET 技术集成并利用它们,例如用户控件和数据绑定。
希望您在阅读本文后开始喜欢 Presentation Model,如果您还没有。为了使其更具说服力,我还有另一篇文章展示了如何在 SharePoint 中使用 Presentation Model。
历史
- 2008 年 2 月 3 日:添加了文章链接 Presentation Model in SharePoint
- 2008 年 1 月 31 日:初版