WinForms MVP - WinForms 的 MVP 框架






4.86/5 (40投票s)
WinForms MVP 框架的基本介绍及使用方法。
介绍
本文并非介绍 Model View Presenter (MVP) 模式本身。本文将介绍如何使用 MVP 模式的一个具体实现,即 WinformsMVP(包含示例文件夹)。
WinForms MVP 是 MVP 模式在 WinForms 平台上的实现。我编写这个框架是因为我觉得 MVP 模式是 WinForms 的一个绝佳选择,而我在寻找一个适用于 WinForms 的 MVP 框架时,找不到让我满意的。我已知晓 WebFormsMVP 的存在,于是我便着手将其移植到 WinForms。
下载代码中附带了一个示例项目。本文将介绍的代码可以在该示例项目中找到。
更新:迄今为止,该框架最大的问题是通用窗体缺乏 Visual Studio 设计器支持。这既不是框架的 bug,也不是 Visual Studio 的 bug。Visual Studio 根本不支持。我现在通过添加一个非通用 MvpForm 来直接解决了这个问题。此外,社区中的一位开发者也提出了一个可以实现设计器支持的解决方案(请参阅下方标题为“Vs Designer Can not suport Generic Form”的帖子中 N Meakins 的评论)。
更新 2:我为该框架添加了另一个早已应该添加的功能——对正确依赖注入的支持。WinformsMVP 支持两种依赖注入库:Unity 和 StructureMap。源代码中现在有两个新项目,分别对应这两种依赖注入库。
我创建了另一个非常小的示例应用程序,名为 LicenceTracker,用于演示新窗体和 N Meakins 建议的解决方案的使用方法,以及如何通过新的依赖注入功能(针对 Unity)将服务注入到 Presenter 中。我已在本文末尾列出了您在创建具有 Visual Studio 设计器支持的窗体时拥有的不同选项。之后,我将解释如何使用新的依赖注入功能。
您可以在此处下载新的示例应用程序代码 - Licence Tracker 应用程序。
入门
我们将直接开始创建一个模型类
public class MainViewModel
{
public IList<KeyValuePair<Type, String>> MenuItems { get; set; }
}
正如您所见,这个模型包含一个将在我们将要创建的主窗体上用于填充 ListBox 的成员。接下来我们需要创建一个接口来表示我们的 View。该接口将实现强类型接口 WinFormsMvp.IView
,并将其强类型化为我们的模型 MainViewModel
。
public interface IMainView : IView<MainViewModel>
{
event EventHandler CloseFormClicked;
void Exit();
}
还有几个成员是从框架中获取的。接口 WinFormsMvp.IView<TModel>
提供了成员
TModel Model { get; set; }
(注意,您可以通过下载框架源代码 http://winformsmvp.codeplex.com/,或使用 JustDecompile 或 dotPeek 等工具进行反编译来查看这些接口的内容)。
并且该接口实现了 WinFormsMvp.IView
接口,它提供了以下两个成员:
bool ThrowExceptionIfNoPresenterBound { get; }
event EventHandler Load;
对于本文的目的,请不要过于担心 ThrowExceptionIfNoPresenterBound
属性。但是,Load
事件将在所有窗体和控件中使用。
一个 View
接下来我们需要创建 View。在这种情况下,我们将创建一个继承自 WinFormsMvp.Forms.MvpForm<TModel>
的类。该类继承自标准的 System.Windows.Forms.Form
类,即标准的、普通的 WinForm。我们的新窗体还将实现我们上面创建的接口 IMainView
。一旦我们实现了该接口,我们的窗体就会像这样:
public class MainView : MvpForm<MainViewModel>, IMainView
{
#region Private Variables
private Button exitButton;
private ListBox menuListBox;
private Label menuTitleLabel;
private MvpUserControl<InfoControlModel> panel;
private bool firstLoad = true;
#endregion
#region IMainView members
public event EventHandler CloseFormClicked;
public void Exit()
{
Close();
}
#endregion
…
我将在文章后面详细介绍该类的其余部分。但目前,请注意它继承强类型 MvpForm
类并实现 IMainView
接口的方式。
一个 Presenter
我们现在需要一个 Presenter 来配合我们的 View。我将简单地谈谈连接。对于这个 Presenter,我们将通过约定进行绑定。您将在示例项目中看到 MainView
窗体在名为Views的文件夹中创建。我们将把 Presenter(我们即将创建的)放在名为Presenters的文件夹中。约定绑定的工作方式是,在以下位置查找与 View 具有相同前缀的 Presenter。
"{namespace}.Logic.Presenters.{presenter}",
"{namespace}.Presenters.{presenter}",
"{namespace}.Logic.{presenter}",
"{namespace}.{presenter}"
也就是说,如果 View 被命名为 MainView
,它将查找名为 MainPresenter
的 Presenter,共同的前缀是“Main”。在示例项目中,名为 MainPresenter
的 Presenter 是在 Presenters 目录中创建的,这是约定绑定器查找它的位置之一。Presenter 的样子如下:
public class MainPresenter : Presenter<IMainView>
{
public MainPresenter(IMainView view) : base(view)
{
View.CloseFormClicked += View_CloseFormClicked;
View.Load += View_Load;
}
void View_CloseFormClicked(object sender, EventArgs e)
{
View.Exit();
}
private void View_Load(object sender, EventArgs e)
{
View.Model = new MainViewModel
{
MenuItems = new List<KeyValuePair<Type, string>>
{
new KeyValuePair<Type, string>(typeof (FirstInfoControl),
"FirstInfoContol"),
new KeyValuePair<Type, string>(typeof (SecondInfoUserControl),
"SecondInfoUserControl")
}
};
}
}
框架将 View 注入到构造函数中,然后传递给基类构造函数,框架将其分配给 Presenter<TView>
类的 View 属性。但您不必担心这一点。只要您符合约定(即使用共同的前缀并在正确的目录中创建文件),Presenter 绑定器就能通过约定将 Presenter 绑定到 View。
然后,我们可以通过 IMainView
接口(以及上面讨论的 IView
接口)的成员来挂钩两个事件。在 View_Load
处理程序中,我们将一个模型分配给 View 的 Model
属性。然后,在调用 CloseFormClicked
处理程序时,我们调用 Exit
方法。
回到 View
退出按钮的 Click 事件处理程序展示了 MVP 模式的经典用法,即 View 上的处理程序引发一个事件(CloseFormClicked
)给订阅它的 Presenter(请参阅上面关于 Presenter 的段落,其中显示了该订阅)。然后 Presenter 可以根据需要组织状态,然后调用 View 上的方法(Exit)来执行窗体关闭。您也会在 MvpUserControls 中看到这种引发事件到 Presenter 的模式(见下文)。
View 的布局非常简单。它将仅包含一个 ListBox 和一个 UserControl。UserControl 的类型将由 ListBox 中选定的项目决定,即当用户在左侧 ListBox 中选择一个项目时,右侧将显示相应的 UserControl。
ListBox 的内容将在窗体的 OnLoad
处理程序中填充。您可能还记得,我们在 Presenter 中将一个对象分配给了 View 的 Model
属性。现在,我们可以将 Model
的 MenuItem
属性分配给 ListBox 的 DataSource。
当用户单击 ListBox 中的项目时,SelectedIndexChanged
处理程序会实例化一个对象,其类型在 ListBox 中被选中。
Type typeOfControlToLoad = ((KeyValuePair<Type, string>)menuListBox.SelectedItem).Key;
// The next call creates the usercontrol. The presenter binding for the UserControl occurs now as it is instantiated.
// Place a break point in the constructor of the relevant presenter to observe it's instantiation.
panel = (Activator.CreateInstance(typeOfControlToLoad) as MvpUserControl<InfoControlModel>);
这给了我一个机会来演示如何处理 MvpUserControls
;这是框架中的一个类,它继承自 UserControl,并设计用于与 MVP 模式结合使用。
处理 MvpUserControls
处理 MvpUserControl
类与处理 MvpForm
相同。首先,我们需要一个 Model,我们将使用一个简单的 Model 来显示一条消息。
public class InfoControlModel
{
public string Message { get; set; }
}
其次,我们将创建一个 View 的契约。
public interface IFirstInfoView : IView<InfoControlModel>
{
event EventHandler PanelClicked;
void ClearPanel();
}
接下来,我们将实现契约在 View 中的成员(请注意,为简洁起见,此处未包含 View 的所有代码。您可以在下载代码中查看 View 的其余代码)。
public class FirstInfoControl : MvpUserControl<InfoControlModel>, IFirstInfoView
{
void InfoClick(object sender, EventArgs e)
{
PanelClicked(this, EventArgs.Empty);
}
public event EventHandler PanelClicked;
public void ClearPanel()
{
infoLabel.Text = string.Empty;
}
...
}
最后,我们将创建一个挂钩 View 接口事件的 Presenter。
public class FirstInfoPresenter : Presenter<IFirstInfoView>
{
public FirstInfoPresenter(IFirstInfoView view) : base(view)
{
View.Load += View_Load;
View.PanelClicked += View_PanelClicked;
}
void View_PanelClicked(object sender, System.EventArgs e)
{
View.ClearPanel();
}
void View_Load(object sender, System.EventArgs e)
{
View.Model = new InfoControlModel { Message = "Convention bound;This control's presenter was bound by convention. The View is called FirstInfoControl and lives in the Views directory. The Presenter is called FirstInfoPresenter and lives in the Presenters directory. Both classes have the prefix \"FirstInfo\". As the View's name ends in \"Control\" and the Presenter's name ends in \"Presenter, the binder has enough information to perform the binding without any specific/express binding (i.e. outside of the framework itself)." };
}
}
如您所见,在 MVP 模式编码方面,使用这个框架来处理 MvpUserControl
与处理 MvpForm
几乎相同。但是,如果我们因为某种原因无法通过约定将 Presenter 绑定到 View,该怎么办?下一节将介绍这种情况。
使用属性绑定
在上述约定不适用于您的项目的情况下,框架还支持另一种绑定方式。该框架支持使用属性将 View 与 Presenter 绑定。为了演示,我使用了一个第二个 UserControl,它将通过属性执行绑定。做到这一点的方法是简单地用属性修饰 UserControl 的类名,如下所示:
[PresenterBinding(typeof(PresenterOfSecondInfo))]
public class SecondInfoUserControl : MvpUserControl<InfoControlModel>, ISecondInfoView
{
…
}
这将导致 SecondInfoUserControl
View 绑定到 PresenterOfSecondInfo
Presenter。如您所见,Presenter 的名称不符合上述约定。因此,唯一将其绑定的方法是使用 PresenterBinding
属性。因此,当用户单击第二个 UserControl 的 ListBox 时,将创建一个 SecondInfoUserControl
对象,然后绑定 Presenter。该 UserControl 和 Presenter 的完整代码包含在下载代码中。
结论
WinForms 平台在许多生产环境中仍然活跃且蓬勃发展。它也是未来开发的一个可行平台(尽管大部分开发将集中在 Windows 8 或 WPF 等更新的平台上)。我编写这个框架的全部原因是,我曾负责支持一个包含大量小型 WinForms 应用程序的系统。
WinForms MVP 非常适合小型 WinForms 应用程序,并且对于想要学习如何针对 MVP 模式进行编程的开发人员来说,它也是一个有用的入门工具。本文概述了如何在 WinForms 编程环境中使用的 WinForms MVP 框架的基础知识。您可以在随框架源代码一起提供的示例项目中看到更多用法示例,可以在 WinForms MVP 下载。
更新 - Visual Studio 设计器支持
使用新的非通用窗体
以下步骤介绍了如何创建使用新的非通用 MvpForm
的窗体。
- 为您的 View 创建一个接口。这次,继承自
IView
,而不是通用版本IView<tmodel></tmodel>
。 - 右键单击 Views 文件夹,然后从上下文菜单中选择 添加 > Windows 窗体(为其命名并按 Enter)。
- 按 F7 进入代码隐藏,并将父类从
Form
更改为MvpForm
(非通用版本)。确保它实现了步骤 1 中的接口。 - 以常规方式创建 Presenter。
public interface IAddProductView : IView
{
event EventHandler CloseFormClicked;
event EventHandler AddProductClicked;
int Id { get; set; }
string Description { get; set; }
string Name { get; set; }
int TypeId { get; set; }
Dictionary<int,> SoftwareTypes { get; set; }
void Exit();
}
</int,>
public partial class AddProductView : MvpForm, IAddProductView
{
...
}
public class AddProductPresenter : Presenter<iaddproductview>
{
private readonly ISoftwareService softwareService;
private AddProductModel model;
public AddProductPresenter(IAddProductView view)
: base(view)
{
View.CloseFormClicked += View_CloseFormClicked;
View.Load += View_Load;
View.AddProductClicked += View_AddProductClicked;
softwareService = new SoftwareService();
model = new AddProductModel { AllSoftwareTypes = softwareService.GetSoftwareTypes().ToList() };
}
void View_AddProductClicked(object sender, EventArgs e)
{
model.NewSoftwareProduct = new Software
{
Description = View.Description,
Name = View.Name,
TypeId = View.TypeId,
};
softwareService.AddNewProduct(model.NewSoftwareProduct);
View.Id = model.NewSoftwareProduct.Id;
}
void View_Load(object sender, EventArgs e)
{
Dictionary<int,> softwareTypes = new Dictionary<int,>(model.AllSoftwareTypes.Count);
foreach (var softwareType in model.AllSoftwareTypes.Select(x => new KeyValuePair<int,>(x.Id, x.Name)))
{
softwareTypes.Add(softwareType.Key, softwareType.Value);
}
View.SoftwareTypes = softwareTypes;
}
void View_CloseFormClicked(object sender, EventArgs e)
{
View.Exit();
}
}
</int,></int,></int,></iaddproductview>
在此示例中,您可以看到接口 IAddProductView
不包含我们的任何领域实体类型。它包含的属性由创建新 Software
产品所需的各种实体的原子部分组成。它们本质上是 View 中心化的。
查看 Presenter AddProductPresenter
,它知道 Model
(它有一个 AddProductModel
模型变量作为私有成员)。但是,AddProductView
View 本身不知道 Model
。它通过 IAddProductView
接口公开了一组属性,使 Presenter 能够:
- 显示
SoftwareTypes
列表(供用户选择); - 访问用户设置的一组值,这些值可用于创建新的软件产品。
这是一个非常纯粹的 MVP 模式示例,其中 Presenter
知道 Model
和 View
,但 View
和 Model
互相不知道。在这种 MVP 风格中,Model
更像是真正的领域 Model
,而不是 ViewModel
。
使用新的非通用窗体和手动添加 Model
使用新的非通用窗体,您可以采取另一种方法,即实现一个继承自 IView<TModel>
接口的接口。这将导致 View 具有 Model
属性。但是,您需要自己添加 Model
属性。请参阅 LicenceTracker 示例应用程序中的 AddSoftwareType
窗体以了解此方法(有关下载链接,请参阅文章顶部的更新段落)。
使用支持通用窗体设计器支持的解决方案
在新的 LicenceTracker 示例应用程序的添加人员功能中,可以看到支持使用通用 MvpForm<TModel>
的解决方案。实现此目的的步骤如下:
- 在项目中创建一个新窗体,并让它继承
MvpForm<TModel>
(在 LicenceTracker 示例应用程序中,它继承MvpForm<AddPersonModel>
)。参见下图。这个窗体将是一个中间窗体,介于通用MvpForm
和您实际打算在设计器中操作的窗体之间。 - 创建另一个新窗体,并让这个窗体继承自您在第 1 步中创建的窗体。这个窗体将是您打算在 Visual Studio 设计器中操作的窗体,使您能够拖放控件并立即获得视觉反馈。
- 让您在第 2 步中创建的窗体实现您为
View
创建的接口。
现在您将获得设计器支持,以及强类型窗体的优势。感谢 N Meakins 为此解决方案做出的贡献。
public partial class AddPersonViewSlice : MvpForm<addpersonmodel>
{
public AddPersonViewSlice()
{
}
}
</addpersonmodel>
public partial class AddPersonView : AddPersonViewSlice, IAddPersonView
{
public AddPersonView()
{
InitializeComponent();
}
private void CloseFormButton_Click(object sender, System.EventArgs e)
{
CloseFormClicked(this, EventArgs.Empty);
}
public event System.EventHandler CloseFormClicked;
public event System.EventHandler AddPersonClicked;
public void Exit()
{
Close();
}
private void AddPersonButton_Click(object sender, EventArgs e)
{
Model.NewPerson.FirstName = FirstNameTextBox.Text.Trim();
Model.NewPerson.LastName = LastNameTextBox.Text.Trim();
AddPersonClicked(this, EventArgs.Empty);
}
}
如何将服务注入 Presenter
在本例中,我将使用 Unity 依赖注入容器将一个服务注入到 Presenter 中。在编写任何代码之前,您需要将以下库包含到您的解决方案中:
- Unity(可以使用 Nuget 下载);
- WinFormsMvp.Unity.dll
有了这些库,我们就需要创建一个 UnityContainer
对象,我们将使用它来注册我们想要从它们所实现的接口中实例化的类型。使用 ContainerControlledLifetimeManager
将创建该服务作为单例。如果您不希望它是单例,请使用 TransientLifetimeManager
。现在我们已经注册了我们的容器将产生的所有类型,我们需要设置 PresenterBinder
类的静态 Factory
属性。这很容易做到,只需将容器传递给 UnityPresenterFactory
对象的构造函数,然后将其分配给静态 Factory
属性。将以下代码放在 Program
类的 Main
方法中:
_unityContainer = new UnityContainer();
_unityContainer.RegisterType<isoftwareservice,>(new ContainerControlledLifetimeManager());
PresenterBinder.Factory = new UnityPresenterFactory(_unityContainer);
</isoftwareservice,>
我们的 Presenter 的构造函数现在将看起来像(还显示了私有字段 softwareService
):
private readonly ISoftwareService softwareService;
public AddPersonPresenter(IAddPersonView view, ISoftwareService softwareService)
:base(view)
{
this.softwareService = softwareService;
View.CloseFormClicked += View_CloseFormClicked;
View.AddPersonClicked += View_AddPersonClicked;
View.Load += View_Load;
}
Presenter 的软件服务由 PresentBinder
的工厂注入,该工厂负责实例化服务。您可以在 示例项目中看到这一点。
历史
文章
版本 | 日期 | 摘要 |
1.0 | 2013 年 2 月 6 日 | 最初发布文章。 |
1.1 | 2013 年 11 月 29 日 | 添加了关于 Visual Studio 设计器支持的部分。 |
1.2 | 2014 年 1 月 20 日 | 添加了关于依赖注入的部分。 |