7

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

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

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

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

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

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

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

有人可以帮我理解如何“构建”这个程序吗?我很感激任何帮助。

谢谢,

丹尼尔

4

2 回答 2

23

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

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

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

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

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

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

如果我正在设计一个 MDI 应用程序,我可能会MainForm实现一个IWindowWorkspace具有Open()Close()方法的接口。我可以将该界面注入到我的各种演示者中,以允许他们打开和关闭其他窗口,而他们不会MainForm直接意识到这一点。例如,ListProductsPresenterEditProductPresenter用户EditProductUserControl双击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构造函数中接受演示者/视图模型/控制器,并在适当的时候对自身进行数据绑定,尽可能地服从演示者。UserControl有些人通过界面(例如 )向演示者隐藏IEditProductView,如果视图不是完全被动的,这可能很有用。我倾向于对所有事情都使用数据绑定,这样通信就可以通过INotifyPropertyChanged并且不用打扰。

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

如果你想要一个特性是可替换的,你需要抽象整个特性。因此,您可能有一个与之对话的INavigationFeature界面。MainForm您可以拥有一个TreeBasedNavigationPresenter实现INavigationFeature并被 a 使用的 a TreeBasedUserControl。而且您可能有一个CarouselBasedNavigationPresenter也实现INavigationFeature并被 a 使用的 a CarouselBasedUserControl。用户控件和演示者仍然齐头并进,但您MainForm不必关心它是与基于树的视图还是基于轮播的视图交互,您可以在不明智的情况下将它们换掉MainForm

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

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

于 2010-12-02T01:00:05.650 回答
4

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

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

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

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

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

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

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

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

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

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

总结一下:

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

控制 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);                                                         
}

不知道这是好是坏还是无所谓!

不管怎样,关于一个两年前的问题,这是一篇多么庞大的帖子——尽管得到一些反馈,但还是很高兴……

于 2012-09-16T18:11:07.540 回答