【问题标题】:How to Structure a C# WinForms Model-View-Presenter (Passive View) Program?如何构建 C# WinForms Model-View-Presenter(被动视图)程序?
【发布时间】:2010-12-01 22:24:28
【问题描述】:

我正在设计一个具有以下基本思想的 GUI(类似地模仿 Visual Studio 的基本外观):

  1. 文件导航
  2. 控件选择器(用于选择在编辑器组件中显示的内容)
  3. 编辑器
  4. 记录器(错误、警告、确认等)

现在,我将使用 TreeView 进行文件导航,使用 ListView 选择要在编辑器中显示的控件,以及使用 RichTextBox 进行 Logger。根据在 TreeView 中选择的内容,编辑器将具有 2 种类型的编辑模式。编辑器可以是用于手动编辑文件内文本的 RichTextBox,也可以是带有拖放 DataGridView 和子文本框的面板,用于在此面板中进行编辑。

我正在尝试遵循被动视图设计模式,将模型与视图完全分离,反之亦然。这个项目的性质是我添加的任何组件都可以编辑/删除。因此,我需要从一个给定的控制独立到下一个。如果今天我使用 TreeView 进行文件导航,但明天我被告知要使用其他东西,那么我想相对轻松地实现一个新控件。

我根本不明白如何构建程序。我了解每个控件一个演示者,但我不知道如何使它工作,以便我有一个带有控件(子视图)的视图(程序的整个 GUI),这样整个视图以及个人视图都是可替换的反映我的模型的控件。

在按被动视图标准应该是轻量级的主视图中,我是否单独实现子视图?如果是这样,假设我有一个接口 INavigator 来抽象 Navigator 对象的角色。导航器将需要一个演示者和一个模型来在导航器视图和主视图之间进行操作。我觉得我在某个地方迷失了设计模式的行话。

可以找到最相似的问题here,但它没有足够详细地回答我的问题。

有人能帮我理解如何“构建”这个程序吗?感谢您的帮助。

谢谢,

丹尼尔

【问题讨论】:

    标签: c# winforms design-patterns mvp passive-view


    【解决方案1】:

    抽象是好的,但重要的是要记住,在某些时候 something 必须对一两件事了解一两件事,否则我们只会有一堆抽象得很好的乐高积木坐在地板上,而不是被组装成一个房子。

    Autofac 这样的控制反转/依赖注入/flippy-dippy-upside-down-whatever-were-calling-it-this-week 容器可以真正帮助将这一切拼凑在一起。

    当我将一个 WinForms 应用程序组合在一起时,我通常会得到一个重复的模式。

    我将从配置 Autofac 容器的Program.cs 文件开始,然后从中获取MainForm 的实例,并显示MainForm。有些人称其为外壳或工作区或桌面,但无论如何它是具有菜单栏并显示子窗口或子用户控件的“表单”,当它关闭时,应用程序退出。

    接下来是前面提到的MainForm。我在 Visual Studio 视觉设计器中做一些基本的事情,比如拖放一些 SplitContainersMenuBars 等,然后我开始对代码感兴趣:我将某些关键接口“注入”到MainForm 的构造函数,这样我就可以使用它们,这样我的 MainForm 就可以编排子控件,而不必真正了解它们。

    例如,我可能有一个IEventBroker 接口,它允许各种组件发布或订阅“事件”,例如BarcodeScannedProductSaved。这允许应用程序的某些部分以松散耦合的方式响应事件,而不必依赖于连接传统的 .NET 事件。例如,与我的EditProductUserControl 一起出现的EditProductPresenter 可以说this.eventBroker.Fire("ProductSaved", new EventArgs<Product>(blah)),而IEventBroker 将检查其订阅者列表以查找该事件并调用他们的回调。例如,ListProductsPresenter 可以侦听该事件并动态更新它所附加的ListProductsUserControl。最终结果是,如果用户将产品保存在一个用户控件中,另一个用户控件的演示者可以在它碰巧打开时做出反应并自行更新,而任何一个控件都不必知道彼此的存在,也不需要 MainForm必须协调该事件。

    如果我正在设计一个 MDI 应用程序,我可能会让 MainForm 实现一个具有 Open()Close() 方法的 IWindowWorkspace 接口。我可以将该界面注入到我的各种演示者中,以允许他们打开和关闭其他窗口,而他们不会直接知道MainForm。例如,当用户双击ListProductsUserControl 中的数据网格中的一行时,ListProductsPresenter 可能想要打开一个EditProductPresenter 和对应的EditProductUserControl。它可以引用IWindowWorkspace——实际上是MainForm,但它不需要知道这一点——并调用Open(newInstanceOfAnEditControl) 并假设控件以某种方式显示在应用程序的适当位置。 (MainForm 实现可能会将控件交换到某个面板上的视图中。)

    但是ListProductsPresenter 到底是如何创建 EditProductUserControl 的实例的呢? Autofac's delegate factories 在这里是一种真正的乐趣,因为您只需将一个委托注入到演示者中,Autofac 就会自动将其连接起来,就好像它是一个工厂一样(伪代码如下):

    public class EditProductUserControl : UserControl { public EditProductUserControl(EditProductPresenter presenter) { // initialize databindings based on properties of the presenter } } public class EditProductPresenter { // Autofac will do some magic when it sees this injected anywhere public delegate EditProductPresenter Factory(int productId); public EditProductPresenter( ISession session, // The NHibernate session reference IEventBroker eventBroker, int productId) // An optional product identifier { // do stuff.... } public void Save() { // do stuff... this.eventBroker.Publish("ProductSaved", new EventArgs(this.product)); } } public class ListProductsPresenter { private IEventBroker eventBroker; private EditProductsPresenter.Factory factory; private IWindowWorkspace workspace; public ListProductsPresenter( IEventBroker eventBroker, EditProductsPresenter.Factory factory, IWindowWorkspace workspace) { this.eventBroker = eventBroker; this.factory = factory; this.workspace = workspace; this.eventBroker.Subscribe("ProductSaved", this.WhenProductSaved); } public void WhenDataGridRowDoubleClicked(int productId) { var editPresenter = this.factory(productId); var editControl = new EditProductUserControl(editPresenter); this.workspace.Open(editControl); } public void WhenProductSaved(object sender, EventArgs e) { // refresh the data grid, etc. } }

    所以ListProductsPresenter 知道Edit 功能集(即编辑演示者和编辑用户控件)——这非常好,它们齐头并进——但事实并非如此需要了解Edit 功能集的所有依赖项,而不是依赖 Autofac 提供的委托来解决所有这些依赖项。

    一般来说,我发现我在“演示者/视图模型/监督控制器”之间存在一对一的对应关系(我们不要太关注差异,因为最终它们都非常相似)和一个“UserControl/Form”。 UserControl 在其构造函数中接受演示者/视图模型/控制器,并根据需要自行绑定数据,尽可能地服从演示者。有些人通过接口向演示者隐藏UserControl,例如IEditProductView,如果视图不是完全被动的,这可能很有用。我倾向于对所有事情都使用数据绑定,所以通信是通过INotifyPropertyChanged 完成的,不用费心。

    但是,如果演示者无耻地与视图绑定,您的生活会变得更轻松。您的对象模型中的属性是否与数据绑定不匹配?暴露一个新的属性。你永远不会有一个 EditProductPresenter 和一个 EditProductUserControl 有一个布局,然后想要编写一个新版本的用户控件,它可以与同一个演示者一起工作。您只需编辑它们,它们都是出于所有意图和目的,一个单元,一个功能,演示者仅存在,因为它易于单元测试而用户控件不是。

    如果您希望某个功能可替换,则需要将整个功能抽象为这样。所以你可能有一个INavigationFeature 接口,你的MainForm 与之对话。您可以拥有一个实现INavigationFeature 并由TreeBasedUserControl 使用的TreeBasedNavigationPresenter。您可能有一个CarouselBasedNavigationPresenter,它也实现了INavigationFeature,并被CarouselBasedUserControl 使用。用户控件和演示者仍然齐头并进,但您的 MainForm 不必关心它是与基于树的视图还是基于轮播的视图交互,您可以在没有 @ 的情况下将它们换掉987654376@更聪明。

    在结束时,很容易混淆自己。每个人都是迂腐的,并且使用稍微不同的术语来传达他们在相似架构模式之间的微妙(而且通常是不重要的)差异。在我看来,依赖注入对于构建可组合、可扩展的应用程序确实有很大的帮助,因为耦合被抑制了。将功能分离为“演示者/视图模型/控制器”和“视图/用户控件/表单”对质量有好处,因为大多数逻辑都被拉入前者,从而可以轻松进行单元测试;并且结合这两个原则似乎确实是您正在寻找的东西,您只是对术语感到困惑。

    或者,我可以充满它。祝你好运!

    【讨论】:

    • 感谢您的回复。我并不想全神贯注于可以从谈论设计模式中获得灵感的哲学术语,但我正在努力获得一些技术知识。因此,这种被动视图真的让我很生气。如何从头开始创建带有子视图的主视图(这样整个主视图以及主视图中的任何子视图都是可互换的)?我在网上找不到任何东西可以正确解决这个问题(也许还没有人明确解决这个问题?)。
    • 这个答案已有 6 多年的历史,但它是我在互联网上找到的对实际 MVP 使用的最佳解释之一。谢谢,它具有令人难以置信的教育意义,即使 WinForms 编程现在已经有些过时了。
    【解决方案2】:

    我知道这个问题已经有将近 2 年的历史了,但我发现自己的情况非常相似。像你一样,我已经在互联网上搜索了 DAYS 并没有找到适合我需要的具体示例 - 我搜索得越多,我就一遍又一遍地回到相同的网站,直到我有大约 10 页紫色Google 中的链接!

    无论如何,我想知道您是否曾经提出过令人满意的解决方案?根据上周阅读的内容,我将概述到目前为止我的工作方式:

    我的目标是: 被动表单,演示者优先(演示者实例化表单,因此表单不知道它的演示者) 通过在表单(视图)中引发事件来调用演示者中的方法

    应用程序有一个 FormMain,其中包含 2 个用户控件:

    ControlsView(有 3 个按钮) DocumentView(第 3 方图像缩略图查看器)

    “主窗体”包含一个工具栏,用于保存常用文件等内容,仅此而已。 “ControlsView”用户控件允许用户单击“扫描文档” 它还包含一个树视图控件,用于显示文档和页面的层次结构 “DocumentView”显示扫描文档的缩略图

    我真的觉得每个控件都应该有自己的 MVP 三元组以及主窗体,但我希望它们都引用相同的模型。我只是不知道如何协调控件之间的通信。

    例如,当用户单击“扫描”时,ControlsPresenter 负责从扫描仪获取图像,我希望它在扫描仪返回的每个页面时将页面添加到树视图 - 没问题 - 但我也希望缩略图同时出现在 DocumentsView 中(问题是演示者彼此不了解)。

    我的解决方案是让 ControlsPresenter 调用模型中的方法将新页面添加到业务对象中,然后在模型中引发“PageAdded”事件。

    然后我让 ControlsPresenter 和 DocumentPresenter “侦听”此事件,以便 ControlsPesenter 告诉它的视图将新页面添加到树视图中,而 DocumentPresenter 告诉它的视图添加新缩略图。

    总结一下:

    控件视图 - 引发事件“ScanButtonClicked”

    Controls Presenter - 监听事件,调用 Scanner 类到 AcquireImages 如下:

    GDPictureScanning scanner = new GDPictureScanning();
    
    IEnumerable<Page> pages = scanner.AquireImages();
    foreach (Page page in pages)
    {
    m_DocumentModel.AddPage(page);                
    //The view gets notified of new pages via events raised by the model
    //The events are subscribed to by the various presenters so they can 
    //update views accordingly                
    }
    

    扫描每一页时,扫描循环调用“yield return new Page(PageID)”。 上述方法调用 m_DocumentModel.AddPage(page)。 新页面被添加到模型中,这会引发一个事件。 控制演示者和文档演示者都“听到”事件并相应地添加项目。

    我不确定的一点是所有演示者的初始化 - 我在 Program.cs 中执行此操作,如下所示:

    static void Main()
    {
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);
    
    IDocumotiveCaptureView view = new DocumotiveCaptureView();
    IDocumentModel model = new DocumentModel();
    IDocumotiveCapturePresenter Presenter = new DocumotiveCapturePresenter(view, model);
    IControlsPresenter ControlsPresenter = new ControlsPresenter(view.ControlsView, model);
    IDocumentPresenter DocumentPresenter = new DocumentPresenter(view.DocumentView, model);
    
    Application.Run((Form)view);                                                         
    }
    

    不确定这是好、坏还是无关紧要!

    无论如何,关于一个两年前的问题的帖子真是太棒了——尽管得到一些反馈是件好事......

    【讨论】:

    • 很好,如果你不介意的话,我会试试的。我目前正在我的 .Net 2.0 WinForms 程序上尝试使用 Passive View 的 MVPVM。我也更喜欢让视图尽可能地愚蠢,但在网络上的许多示例中,它们将视图与 Presenter 紧密耦合:-/
    • 好的,我删除了演示者接口,不需要那个,然后我创建了 DocumotiveCaptureView 的 ControlsView 和 DocumentView 公共属性,它们在 DocumotiveCaptureView 构造函数中被实例化。因此,我只将“view”传递给每个Presenter,然后在Presenter的构造函数中订阅view.FormMainButtonClicked,或者view.DocumentView.DocumentViewButtonClicked。
    猜你喜欢
    • 2011-09-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-06-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多