繁体   English   中英

如何构造C#WinForms模型视图呈现器(被动视图)程序?

[英]How to Structure a C# WinForms Model-View-Presenter (Passive View) Program?

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

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

现在,我将使用TreeView进行文件导航,使用ListView来选择要在编辑器中显示的控件,并使用RichTextBox来记录日志。 根据在TreeView中选择的内容,编辑器将具有2种类型的编辑模式。 编辑器将是一个RichTextBox,用于手动编辑文件中的文本,或者它将是一个具有拖放数据面板视图的面板和用于在此面板中进行编辑的子文本框。

我试图遵循Passive View设计模式,以将Model与View完全分离,反之亦然。 该项目的性质是,我添加的任何组件都必须进行编辑/删除。 因此,我需要独立于给定的控件到下一个控件。 如果今天我使用TreeView进行文件导航,但是明天我被告知要使用其他方法,那么我想相对轻松地实现一个新控件。

我根本不了解如何构建程序。 我了解每个控件一个Presenter,但是我不知道如何使它起作用,以至于我拥有一个带有控件(子视图)的视图(程序的整个GUI),使得整个视图以及单个视图都可以替换。反映我的模型的控件。

在主视图(按被动视图标准应该是轻量级的)中,我是否单独实现子视图? 如果是这样,说我有一个接口INavigator来抽象Navigator对象的角色。 导航器将需要一个Presenter和一个Model才能在Navigator视图和主视图之间起作用。 我觉得我迷失在某个地方的设计模式行话中。

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

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

谢谢,

丹尼尔

抽象是好的,但重要的是要记住,在某个时候某件事必须对一两件事了解一两件事,否则我们只会把一堆精美抽象的乐高玩具坐在地板上,而不是组装成一堆一个房子。

无论我们如何称呼它,本周称为Autofac的控制反转/依赖注入/ flippy-dippy- upside - down都可以真正帮助将它们拼凑在一起。

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

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

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

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

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

但是,到底如何将在ListProductsPresenter 创建的该实例EditProductUserControl Autofac的委托工厂在这里是一个真正的快乐,因为您只需将一个代表注入到演示者中,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在其构造函数中接受演示者/视图模型/控制器,并在适当时对自身进行数据绑定,从而尽可能多地推迟给演示者。 有些人通过IEditProductView类的界面将UserControl隐藏在演示IEditProductView ,如果视图不是完全被动的,则很有用。 我倾向于对所有内容都使用数据绑定,因此通信是通过INotifyPropertyChanged完成的,不要打扰。

但是,如果演示者与视图无懈可击,您将使生活变得更加轻松。 对象模型中的属性是否与数据绑定无关? 公开一个新的属性。 您永远都不会拥有具有一个布局的EditProductPresenterEditProductUserControl ,然后想要编写与同一演示者一起使用的用户控件的新版本。 您将只对它们进行编辑,它们对于所有意图和目的都是一个单元,一项功能,演示者仅存在,因为它很容易进行单元测试,而用户控件则不行。

如果您希望功能是可替换的,则需要像这样抽象整个功能。 因此,您可能会与MainForm进行对话的INavigationFeature接口。 你可以有一个TreeBasedNavigationPresenter实现INavigationFeature并通过消耗TreeBasedUserControl 而且您可能拥有一个CarouselBasedNavigationPresenter ,它也实现了INavigationFeature ,并由CarouselBasedUserControl 用户控件和演示者仍然可以并驾齐驱,但是您的MainForm不必关心它是否与基于树的视图或基于轮播的视图进行交互,并且您可以将它们交换出去而无需将MainForm明智的。

最后,很容易混淆自己。 每个人都是学究的人,并且使用略有不同的术语来传达它们在相似的建筑模式之间的细微差异(通常不重要)。 在我的拙见中,依赖关系注入对于构建可组合的,可扩展的应用程序确实产生了奇迹,因为耦合一直保持不变。 将功能分为“演示者/视图模型/控制器”和“视图/用户控件/表单”确实对质量产生了疑问,因为大多数逻辑都被拉入了前者中,因此可以轻松地对其进行单元测试; 而将这两个原则结合起来似乎确实是您所要寻找的,只是您对术语感到困惑。

或者,我可能会充满。 祝好运!

我知道这个问题将近2岁了,但我发现自己处在非常相似的情况。 像您一样,我在互联网上搜寻了DAYS天,却没有找到适合自己需求的具体示例-搜索次数越多,我越一遍又一遍又一遍地回到同一网站,直到我发现大约10页紫色Google中的链接!

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

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

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

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

“主窗体”包含一个用于保存文件等常用内容的工具栏。 “ ControlsView”用户控件允许用户单击“扫描文档”。它还包含一个Treeview控件,用于显示文档和页面的层次结构。“ 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                
}

在扫描每个页面时,扫描循环会调用“产量返回新页面(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);                                                         
}

不知道这是好事,坏事还是无动于衷!

无论如何,关于一个两年的老问题,这是多么大的篇幅-希望能得到一些反馈……

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM