65.9K
CodeProject 正在变化。 阅读更多。
Home

WinForms MVP - WinForms 的 MVP 框架

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.86/5 (40投票s)

2013年1月15日

CPOL

12分钟阅读

viewsIcon

198647

downloadIcon

8042

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/,或使用 JustDecompiledotPeek 等工具进行反编译来查看这些接口的内容)。

并且该接口实现了 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 属性。现在,我们可以将 ModelMenuItem 属性分配给 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 的窗体。

  1. 为您的 View 创建一个接口。这次,继承自 IView,而不是通用版本 IView<tmodel></tmodel>
  2. 右键单击 Views 文件夹,然后从上下文菜单中选择 添加 > Windows 窗体(为其命名并按 Enter)。
  3. 按 F7 进入代码隐藏,并将父类从 Form 更改为 MvpForm(非通用版本)。确保它实现了步骤 1 中的接口。
  4. 以常规方式创建 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 能够:

  1. 显示 SoftwareTypes 列表(供用户选择);
  2. 访问用户设置的一组值,这些值可用于创建新的软件产品。

这是一个非常纯粹的 MVP 模式示例,其中 Presenter 知道 ModelView,但 ViewModel 互相不知道。在这种 MVP 风格中,Model 更像是真正的领域 Model,而不是 ViewModel

使用新的非通用窗体和手动添加 Model

使用新的非通用窗体,您可以采取另一种方法,即实现一个继承自 IView<TModel> 接口的接口。这将导致 View 具有 Model 属性。但是,您需要自己添加 Model 属性。请参阅 LicenceTracker 示例应用程序中的 AddSoftwareType 窗体以了解此方法(有关下载链接,请参阅文章顶部的更新段落)。

使用支持通用窗体设计器支持的解决方案

在新的 LicenceTracker 示例应用程序的添加人员功能中,可以看到支持使用通用 MvpForm<TModel> 的解决方案。实现此目的的步骤如下:

  1. 在项目中创建一个新窗体,并让它继承 MvpForm<TModel>(在 LicenceTracker 示例应用程序中,它继承 MvpForm<AddPersonModel>)。参见下图。这个窗体将是一个中间窗体,介于通用 MvpForm 和您实际打算在设计器中操作的窗体之间。
  2. 创建另一个新窗体,并让这个窗体继承自您在第 1 步中创建的窗体。这个窗体将是您打算在 Visual Studio 设计器中操作的窗体,使您能够拖放控件并立即获得视觉反馈。
  3. 让您在第 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 中。在编写任何代码之前,您需要将以下库包含到您的解决方案中:

  1. Unity(可以使用 Nuget 下载);
  2. 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 日 添加了关于依赖注入的部分。
© . All rights reserved.