WinForms中的Model-View-Presenter模式

94

我正尝试第一次使用WinForms实现MVP模式。

我试图理解每个层的功能。

在我的程序中,有一个GUI按钮,当点击它时就会打开一个openfiledialog窗口。

因此,按照MVP的方式,GUI处理按钮单击事件,然后调用presenter.openfile();

在presenter.openfile()内部,是否应该将打开文件的操作委托给模型层,或者由于没有要处理的数据或逻辑,它应该只是响应请求并打开openfiledialog窗口呢?

更新:我决定提供奖励,因为我觉得我需要进一步的帮助,并且最好是针对下面的具体问题,以便我有上下文。

好的,在阅读了MVP之后,我决定实现Passive View。 实际上,我将在Winform上拥有一堆控件,由Presenter处理,然后任务被委托给Model(们)。 我的具体问题如下:

  1. 当Winform加载时,它必须获取一个treeview。 我是否正确地认为视图应该调用这样的方法:presenter.gettree(),这将进而委托给模型,后者将获取treeview的数据,创建并配置它,返回给presenter,后者将传递给视图,然后简单地将其分配给,比如说,一个面板?

  2. 对于Winform上的任何数据控件,这是否都是相同的呢?因为我还有一个datagridview?

  3. 我的应用程序具有许多具有相同程序集的模型类。它还支持插件架构,需要在启动时加载插件。 视图是否仅需调用presenter方法,后者将调用一种加载插件并在视图中显示信息的方法?哪个层将控制插件引用?视图会保存引用还是presenter?

  4. 我是否正确地认为视图应该处理有关演示的每一件事情,从treeview节点颜色到datagrid大小等等?

我认为这些是我的主要关注点,如果我了解这些流程应该如何进行,我想我会没问题的。


这个链接http://lostechies.com/derekgreer/2008/11/23/model-view-presenter-styles/解释了MVP的一些风格。除了Johann的出色回答之外,它可能会证明是有帮助的。 - ak3nat0n
3个回答

131
这是我对MVP和你的具体问题的谦虚看法。
首先,用户可以交互或仅显示的任何内容都是视图。这种视图的规律、行为和特性由接口描述。该接口可使用WinForms UI、控制台UI、Web UI甚至没有UI(通常在测试Presenter时)来实现 - 只要它遵守其视图接口的规律即可,具体实现并不重要。
其次,视图始终由Presenter控制。这种Presenter的规律、行为和特性也由接口描述。只要它遵守其视图接口的规律,该接口就不会对具体视图实现产生兴趣。
第三,由于Presenter控制其视图,为了最小化依赖关系,视图根本不需要知道有关其Presenter的任何信息。Presenter与视图之间有一个协议,并由视图接口声明。
第三点的含义是:
- Presenter没有任何视图可以调用的方法,但视图具有Presenter可以订阅的事件。 - Presenter知道它的视图。我喜欢通过具体Presenter的构造函数注入来实现这一点。 - 视图不知道哪个Presenter正在控制它;它永远不会提供任何Presenter。
对于您的问题,在简化代码中可能如下所示:
interface IConfigurationView
{
    event EventHandler SelectConfigurationFile;

    void SetConfigurationFile(string fullPath);
    void Show();
}

class ConfigurationView : IConfigurationView
{
    Form form;
    Button selectConfigurationFileButton;
    Label fullPathLabel;

    public event EventHandler SelectConfigurationFile;

    public ConfigurationView()
    {
        // UI initialization.

        this.selectConfigurationFileButton.Click += delegate
        {
            var Handler = this.SelectConfigurationFile;

            if (Handler != null)
            {
                Handler(this, EventArgs.Empty);
            }
        };
    }

    public void SetConfigurationFile(string fullPath)
    {
        this.fullPathLabel.Text = fullPath;
    }

    public void Show()
    {
        this.form.ShowDialog();        
    }
}

interface IConfigurationPresenter
{
    void ShowView();
}

class ConfigurationPresenter : IConfigurationPresenter
{
    Configuration configuration = new Configuration();
    IConfigurationView view;

    public ConfigurationPresenter(IConfigurationView view)
    {
        this.view = view;            
        this.view.SelectConfigurationFile += delegate
        {
            // The ISelectFilePresenter and ISelectFileView behaviors
            // are implicit here, but in a WinForms case, a call to
            // OpenFileDialog wouldn't be too far fetched...
            var selectFilePresenter = Gimme.The<ISelectFilePresenter>();
            selectFilePresenter.ShowView();
            this.configuration.FullPath = selectFilePresenter.FullPath;
            this.view.SetConfigurationFile(this.configuration.FullPath);
        };
    }

    public void ShowView()
    {
        this.view.SetConfigurationFile(this.configuration.FullPath);
        this.view.Show();
    }
}

除了上述内容,我通常会有一个基础的IView接口,其中包含我的视图通常需要的Show()和任何拥有者视图或视图标题的信息。
回答你的问题:
1. 当窗体加载时,它必须获取一个树形视图。我是否正确地认为视图应该调用像 presenter.gettree() 这样的方法,这将委托给模型,模型将获取树形数据,创建并配置它,将其返回给 presenter,然后再传递给视图,最后将其分配给面板?
我将在IConfigurationPresenter.ShowView()中调用IConfigurationView.SetTreeData(...),就在调用IConfigurationView.Show()之前。
2. 对于窗体上的任何数据控件,如datagridview,是否也是这样?
是的,对于这个我会调用IConfigurationView.SetTableData(...)。由视图来格式化给定的数据。Presenter只是遵循视图的契约,即它想要表格数据。
3. 我的应用程序有许多具有相同程序集的模型类。它还支持插件架构,需要在启动时加载插件。视图是否只需调用一个 presenter 方法,然后调用一个加载插件并在视图中显示信息的方法?哪一层将控制插件引用?视图会保存对它们的引用还是 presenter?
如果插件与视图相关,则视图应该知道它们,但不应该由 presenter 知道。如果它们都是关于数据和模型的,则视图不应该涉及它们。
4. 我是否正确地认为视图应该处理有关呈现的每一个细节,从树形节点颜色到datagrid大小等等?
是的。可以将其看作 presenter 提供描述数据的 XML,而视图则采用 CSS 样式表对其进行渲染。具体来说,presenter 可能会调用IRoadMapView.SetRoadCondition(RoadCondition.Slippery),然后视图以红色渲染道路。
5. 如果我单击树节点时,我是否应该将特定节点传递给 presenter,然后 presenter 将计算出它需要的数据,并向模型请求该数据,最后再将其呈现回视图?
是的,当单击树节点时,我会将特定节点传递给 presenter,然后 presenter 将计算出它需要的数据,并向模型请求该数据,最后再将其呈现回视图。
如果可能的话,我会在一次性地传递所有呈现树所需的数据。但如果某些数据太大而无法从一开始传递,或者如果其动态性质需要从模型(通过Presenter)获取“最新快照”,那么我会在视图接口中添加类似于event LoadNodeDetailsEventHandler LoadNodeDetails的内容,以便Presenter可以订阅它,在模型中获取节点的详细信息LoadNodeDetailsEventArgs.Node (可能通过其某种ID), 以便当事件处理程序委托返回时,视图可以更新其显示的节点详细信息。请注意,如果获取数据可能过慢导致用户体验不佳,则可能需要使用异步模式。

3
我认为你不一定需要将视图和Presenter解耦。我通常会将模型和Presenter解耦,使Presenter监听模型事件并相应地采取行动(更新视图)。在视图中有一个Presenter可以方便视图与Presenter之间的通信。 - kasperhj
11
@lejon: 你说在视图中有一个主持人可以缓解视图和主持人之间的沟通,但我强烈不同意。我的立场是:当视图知道关于主持人的信息时,对于每个视图事件,视图必须决定调用哪个主持人方法才是正确的。这是“两个复杂点”,因为视图实际上不知道哪个视图事件对应哪个主持人方法。契约没有明确规定这一点。 - Johann Gerell
5
如果另一方面,视图只展示实际事件,则演示者本身(知道当视图事件发生时希望做什么)只需订阅它以执行正确的操作。这只有“1个复杂度点”,在我看来比“2个复杂度点”好一倍。一般而言,较少的耦合意味着在项目运行期间的维护成本更低。 - Johann Gerell
10
我也倾向于使用“封装的Presenter”,就像这个链接中所解释的那样:http://lostechies.com/derekgreer/2008/11/23/model-view-presenter-styles/,其中视图是唯一持有者。 - ak3nat0n
4
关于您提供的链接中解释的三种MVP风格,我认为Johann的回答最符合第三种风格,即“观察者Presenter风格”:“观察者Presenter风格的好处是它完全将Presenter的知识与View分离开来,使View不太容易受到Presenter中的变化的影响。” - DavidRR

12

包含所有逻辑的Presenter应该响应按钮被点击的事件,就像@JochemKempe says。实际上,按钮点击事件处理程序调用presenter.OpenFile()。然后Presenter能够确定应该执行什么操作。

如果它决定用户必须选择文件,则通过视图接口回调到视图(包含所有UI技术细节)并让视图显示OpenFileDialog。这是非常重要的区别,因为Presenter不应该允许执行与正在使用的UI技术相关联的操作。

然后将选定的文件返回给Presenter,Presenter继续其逻辑。这可能涉及任何处理文件的模型或服务。

我认为使用MVP模式的主要原因是将UI技术与视图逻辑分离。因此,Presenter编排所有逻辑,而View将其与UI逻辑分开。这具有使Presenter完全可单元测试的非常好的副作用。

更新:由于展示者是在一个特定视图中找到的逻辑的体现,因此视图-展示者关系在我看来是一对一的关系。就实际目的而言,一个视图实例(比如一个表单)与一个展示者实例交互,并且一个展示者实例与仅一个视图实例交互。

话虽如此,在我使用WinForms实现MVP时,展示者总是通过表示视图UI能力的接口与视图进行交互。没有限制视图实现此接口的内容,因此不同的“小部件”可以实现相同的视图接口并重用展示者类。


谢谢。所以在 presenter.OpenFile() 方法中,它不应该有显示 openfiledialog 的代码吗?相反,它应该返回到视图中以显示该窗口? - Darren Young
4
没错,我绝不会让演示者直接打开对话框,因为那样会破坏你的测试。可以将其转移至视图,或者像我在某些情况下所做的那样,使用单独的“FileOpenService”类来处理实际的对话框交互。这样,在测试期间就可以模拟文件打开服务了。将此类代码放入一个单独的服务中可能会带来不错的可重用性效果 :) - Peter Lillevold

2

演示者应该在请求端采取行动,并像你建议的那样显示openfiledialog窗口。由于不需要从模型中获取数据,演示者可以并且应该处理请求。

假设您需要数据来创建模型中的某些实体。您可以将流传递到访问层,其中有一个从流创建实体的方法,但我建议您在演示者中处理文件的解析,并在模型中为每个实体使用构造函数或Create方法。


1
谢谢您的回复。另外,您是否有一个单一的视图呈现者?该呈现者要么处理请求,要么如果需要数据,则委托给任意数量的模型类来处理特定的请求?这是正确的方式吗?再次感谢。 - Darren Young
3
一个视图只有一个Presenter,但一个Presenter可以有多个视图。 - JochemKempe

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接