Model View Presenter via .NET






4.14/5 (24投票s)
本文介绍了一种在 .NET 中实现 Model View Presenter 模式的方法,并将其与现有的 MVP、MVC 实现进行对比,并使用相互依赖的接口来实现抽象协调。
MVP 到底是什么?
Model-View-Presenter(模型-视图-表示器)是一种设计模式,用于在与底层对象模型/域交互时分离显示和显示协调的关注点。更准确地说,由于实际涉及的连接顺序,应该是 Model, Presenter, View。该范式可以分解为:
- Model - 用户故事所关注的可观察数据模型。
- Presenter - 一个将数据从模型获取并暴露给视图的构造。它还负责从视图接收更新并将其重新应用到模型。
- View - 系统的显示门户。显示应用程序的交互和显示界面。
可以说 MVP 是 MVC(Model View Controller,模型-视图-控制器)的一个变体,在 MVC 中,控制器充当外部中介。MVP 模式的不同之处在于,它将模型置于显示实现更远的地方。为了论证起见,我们将 MVP 的“Presenter”部分称为“Controller”,并比较和对比不同范式中每个实体的作用。
元素 |
模型-视图-控制器 |
模型-视图-表示器 |
视觉上 |
|
|
模型 |
应用程序域逻辑 |
应用程序域逻辑 |
视图 |
从控制器接收模型元素。显示并与元素交互。 |
从表示器接收数据,通知表示器交互。 |
控制器 (Controller) |
监控视图/模型,响应事件/更改执行操作。 |
将事件委托给模型。将模型传出的事件重新映射为状态更改。 |
每种技术都有其优点。Martin Fowler 将 MVP 识别为 Model-View-Controller 的“Passive View”(被动视图)变体。在传统的 MVC 中,视图会接收更新,并在其控制器配置后相应地操作模型。在 Passive View 或 MVP 中,视图会将更新通知其表示器,然后表示器会代表视图进行相应的模型更改,充当双向映射层。
现有的 MVP 实现
正如任何设计模式一样,多年来有很多人尝试过 Model View Presenter,包括 Microsoft .NET Framework 的版本。由于双向抽象通信的复杂性(用于表示器/视图协调),大多数实现通常只抽象系统的某一方:视图。
这个选择迫使我们接受一些不良特性,但关键的是你无法模拟表示器或完全替换实现。如果视图需要通知表示器更改,它需要针对特定的视图类型进行编码。
通常有人认为,在构建 Web 应用程序时,这种方法就足够了,因为你很少想测试视图实现,而是倾向于测试具有模拟视图的表示器。然而,一些开发人员对此并不满意,并且经常需要模拟表示器,并允许视图单独构建,甚至大部分提前构建(这在与设计机构合作时很常见)。
抽象表示器/视图
到目前为止,我们已经根据传统的 MVP 指示进行了抽象,但这让一些“纯粹主义者”感到不满。实际上,我们只抽象了堆栈的三分之一“抽象”,而且我们的抽象视图与表示器实现硬连接,而表示器又与模型实现硬连接。我们可以通过使用传统接口来抽象我们对模型的依赖来解决后一个问题,但是表示器本身呢?
理想情况下,我们希望最终达到类似于以下情况的局面:
表示器处理模型,通过接口与视图进行交互。视图实现反过来接收更新并处理表示器接口。
使用 .NET Generics 实现耦合契约
.NET Framework 中的泛型语法的一个能力是创建**自约束泛型**,即对其自身做出某种保证的泛型。我们可以使用它来为视图和表示器创建接口定义,这些接口既是强类型的,又是双向的!
public interface IView<TViewContract, TPresenterContract>
where TViewContract : IView<TViewContract, TPresenterContract>
where TPresenterContract : IPresenter<TPresenterContract, TViewContract>
{
void AttachToPresenter(TPresenterContract presenter, bool requiresInitialState);
void DetatchFromPresenter();
TPresenterContract Presenter { get; }
}
public interface IPresenter<TPresenterContract, TViewContract>
where TPresenterContract : IPresenter&<TPresenterContract, TViewContract>
where TViewContract : IView<TViewContract, TPresenterContract>
{
void ConnectView(TViewContract viewInstance, bool requiresState);
void DisconnectView(TViewContract viewInstance);
}
这种看似奇怪的泛型语法使我们能够实现实例之间的双向耦合,这些实例完全由接口驱动,使我们能够替换协议的任一端而不会牺牲任何强类型!表示器可以维护一个强类型的视图集合,同时,视图也可以维护一个强类型的表示器属性和其他方面。
要使用这些接口,只需声明形式为的契约对:
public interface ISomeView : IView<ISomeView, ISomePresenter>
{
// Put view operations here
}
public interface ISomePresenter : IPresenter<ISomePresenter, ISomeView>
{
// Put presenter operations here.
}
实现这些接口的相应类将具有强类型机制,以实现对象间的通信。
示例 - Tick, Tock, Model-View, Clock?
演示设计模式的“一劳永逸”的不可动摇的实用性的一个好的起点是“时钟”场景。为了保持既定传统,附带的项目包含一个示例应用程序套件,该套件使用 Model-View-Presenter 约定来实现时钟。
模型是一个“TimeModel”,它维护当前时间(并定期触发事件以指示时间已更改!)。表示器连接到此模型的事件,并通知其视图任何相应的时间更改。视图会收到通知并更新自身,但也为用户提供了更改时区的机制。当用户决定更改时区时,表示器会收到视图的通知,并在模型中进行相应的更改。
因此,模型上会引发一个事件;订阅的表示器然后将信息推送到其每个视图。作为序列图,这大致是场景:
示例项目中各个库之间的关系如下:
该应用程序可以使用 Unity 或任何其他控制反转机制来执行组件的激活和连接。然而,为了不将本文变成 Enterprise Library 的教程,我将其留给读者!
在具体实现方面,示例应用程序中的协调器组件是一个简单的 Windows 窗体,模型和表示器都是常规类。表示器继承自一个基表示器类,该类处理许多比较繁琐的实现方面(例如管理连接视图的集合等)。
视图本身实现为 WinForms UserControl,并在窗体上显示两次,如下所示:
更改一个视图的时区时,另一个视图也会收到更新(通过表示器的协调逻辑,响应模型更改)。数据交换之所以可行,是因为契约耦合。
好了,这就是 Model-View-Presenter 模式,它实现了通信的双向强类型,同时又与具体类解耦。
结束语 - 为什么要有耦合契约?
我之前被问过为什么我选择了相互保证的契约——这是否意味着特定的表示器必须有特定类型的视图?如果我想构建一个完全不同的表示器并让它使用这些视图怎么办?
对于这些情况,可以使用视图/表示器接口的继承,以便你的表示器可以添加额外的操作。接口也允许多重继承,所以一个表示器实际上可以通过相同的可靠模式支持多种类型的视图。
如果你想将此模式更改为启用“Active View”(活动视图),而不是“Passive View”,唯一需要更改的是让表示器将模型接口传递给它。这将允许视图类直接与模型交互,但仍然允许事件冒泡到表示器并导致视图接收相应的更新。
下次 - ASP .NET 使用 MVP 进行协调?
在我的下一篇文章中,我将介绍如何将此模式与 ASP.NET 结合使用,而无需依赖大量代码隐藏来执行视图、表示器和模型之间的链接。
祝好!
历史
- 2009 年 10 月 10 日 - 初稿(+ 更新以修复使泛型部分看起来很奇怪的格式问题)。