C# WinForms 模型-视图-控制器(被动视图)

20

我正在使用C#开发一个WinForms应用程序。我的GUI编程经验有限,我必须在学习的同时不断尝试。以下是我正在构建的内容。

请查看以下链接中的一般GUI外观:

GUI http://img227.imageshack.us/img227/1084/program0.jpg

我已经完成了很多工作,但是采用了非常糟糕的自主设计模式。我不知道项目会达到什么规模,因此现在是进行重构的时候了。

我已经学习了大量关于GUI设计模式的知识,我希望实现的模式是被动视图(详见http://martinfowler.com/eaaDev/PassiveScreen.html)。我正在寻求一些帮助,以使这些内容结合起来。

背景:

1)根据用户在“TreeView”中点击的内容,左下角的“List”将显示可以填充“Editor”区域对象列表。这些对象可能是TextBox或DataGridView。用户切换列表以选择他/她想在“Editor”中查看什么。

2)该模型本质上是一个带有数据和配置文件的文件夹。有一个外部程序在给定目录上运行,创建输出文件/文件夹等。我正在开发的程序旨在以用户友好的方式有效地管理/配置这些对象。

3)我一直在做的事情的问题是几乎不可能测试,因此转向MVP-esque被动视图设计模式。

我正在尝试使程序独立于视图工作。我找不到任何使用被动视图模式进行更复杂、交互式视图的示例。

问题:

1) 我需要为整个程序实现一个大型界面/视图,然后为每个TreeView、Editor、Logger等实现子接口/子视图吗?还是有更好的“结构”来完成这个任务?

2) 当涉及从视图到Presenter/Controller (无论使用何种术语来描述被动视图设计模式)的事件“交接”时,我应该如何做?有时我需要更新简单属性,有时我需要展开一系列步骤。

我希望能够得到有关此主题的建议和建议。我搜遍了互联网,但没有找到足够的示例来帮助我继续进行这个项目。

提前感谢!

丹尼尔

2个回答

18
这是一个简单的示例,演示了使用MVP设计模式实现被动视图的概念。因为我们使用被动视图,所以视图不知道有关主持人的任何信息。主持人将仅订阅由视图发布的事件并相应地采取行动。
首先,我们需要为我们的视图定义一个合同。通常使用接口来实现这一点,基本上,我们希望与我们的视图具有非常松散的耦合。我们希望能够切换到不同的视图或者创建模拟视图进行单元测试。
以下是描述用于显示客户信息的简单视图的合同。
public interface ICustomerManagementView
{
    void InitializeCustomers(ICustomer[] customers);
    void DisplayCustomer(ICustomer customer);
    event EventHandler<EventArgs<ICustomer>> SelectedCustomerChanged;
}

它暴露了一个单一的方法InitializeCustomers,该方法将用于使用模型中的对象初始化我们的视图。

我们还有一个事件SelectedCustomerChanged,它将被我们的Presenter用于接收通知,以表明视图中发生了某个操作。

一旦我们拥有了合同,就可以在Presenter中开始处理这些交互。

public class CustomerManagementPresenter
{
    private ICustomer _selectedCustomer;
    private readonly ICustomerManagementView _managementView;
    private readonly ICustomerRepository _customerRepository;

    public CustomerManagementPresenter(ICustomerManagementView managementView, ICustomerRepository customerRepository)
    {
        _managementView = managementView;
        _managementView.SelectedCustomerChanged += this.SelectedCustomerChanged;

        _customerRepository = customerRepository;

        _managementView.InitializeCustomers(_customerRepository.FetchCustomers());
    }

    private void SelectedCustomerChanged(object sender, EventArgs<ICustomer> args)
    {
        // Perform some logic here to update the view
        if(_selectedCustomer != args.Value)
        {
            _selectedCustomer = args.Value;
            _managementView.DisplayCustomer(_selectedCustomer);
        }
    }
}

在演示文稿中,我们可以使用另一种设计模式依赖注入来提供对视图和任何可能需要的模型类的访问。在此示例中,我有一个CustomerRepository负责获取客户详细信息。
在构造函数中,我们有两个重要的代码行,首先我们已经订阅了视图中的SelectedCustomerChanged事件,在这里我们可以执行相关操作。其次,我们使用来自存储库的数据调用了InitializeCustomers。
此时我们实际上还没有为视图定义具体实现,我们所要做的就是创建一个实现ICustomerManagementView的对象。例如,在Windows Forms应用程序中,我们可以执行以下操作
public partial class CustomerManagementView : Form, ICustomerManagementView
{
    public CustomerManagementView()
    {
        this.InitializeComponents();
    }

    public void InitializeCustomers(ICustomer[] customers)
    {
        // Populate the tree view with customer details
    }

    public void DisplayCustomer(ICustomer customer)
    {
        // Display the customer...
    }

    // Event handler that responds to node selection
    private void CustomerTreeViewAfterSelect(object sender, TreeViewEventArgs e)
    {
        var customer = e.Node.Tag as ICustomer;
        if(customer != null)
        {
            this.OnSelectedCustomerChanged(new EventArgs<ICustomer>(customer));
        }
    }

    // Protected method so that we can raise our event
    protected virtual void OnSelectedCustomerChanged(EventArgs<ICustomer> args)
    {
        var eventHandler = this.SelectedCustomerChanged;
        if(eventHandler != null)
        {
            eventHandler.Invoke(this, args);
        }
    }

    // Our view will raise an event each time the selected customer changes
    public event EventHandler<EventArgs<ICustomer>> SelectedCustomerChanged;
}

如果我们想要测试我们的演示逻辑,我们可以模拟我们的视图并执行一些断言。
编辑:包括自定义事件参数。
public class EventArgs<T> : EventArgs
{
    private readonly T _value;

    public EventArgs(T value)
    {
        _value = value;
    }

    public T Value
    {
        get { return _value; }
    }
}

1
我更喜欢将其分解为较小的组件,特别是如果您想鼓励重用。创建具有单一目的但可以一起使用以创建复合 UI 的视图。 - Rohan West
此外,如果您查看演示者构造函数,您可以看到如何处理来自视图的事件。 - Rohan West
是的,我现在看到了。第一次没注意到。谢谢。 - Daniel McClelland
2
再次感谢您的帮助。如果客户内部的属性发生更改,例如他/她的地址,则我们会触发一个事件通知更改。然后,CustomerRepository应该获得该事件,然后它应该通知View需要进行更改。这是正确的吗?这意味着每个客户事件到CustomerRepository事件到View接收该事件的比率为1-1-1。因此,对于每个更新的属性,有3个事件将其从客户传递到视图。有没有办法减少这个数字? - Daniel McClelland
1
最后一个问题,顺便问一下(感谢您忍受了这么多!)。对于我的程序,它由4个屏幕部分组成,用户可以与程序进行交互,我正在考虑如何将工作分开。应该有一个主Presenter对象(分割成几个部分类文件)来处理所有4个功能部分,还是应该在主Presenter对象内嵌入每个4个GUI部分的子Presenter?因此,我可以为每个部分分配其自己的责任,或者将其视为一个大的View/Presenter组合。你怎么看? - Daniel McClelland
显示剩余4条评论

0

我会将它们分解成具有自己的表示层的单独视图,并使用“控制”Presenter/View来管理它们之间的消息委派。这不仅有助于可测试性,而且还可以使您的控件实现SRP。

因此,在您的情况下,您可能会拥有一个IFormManager,您的主窗口将实现它,然后是IFileManager、ILoggerWindow等等。

虽然使用它可能有点过度,但我建议您看一下Smart Client Software Factory(来自Microsoft Patterns and Practices团队)-它不再被积极开发,但它有一个很好的MVP实现,并且非常擅长这种视图组合,因此可能会给您一些好的想法。


谢谢您的输入。但是我应该把单个控件放在哪里?在主视图内部吗?但是我一开始就想让主视图保持轻量级。 - Daniel McClelland
我找到了一张可能详细说明我的问题的图片。你觉得这个图形足以解决我的问题吗?http://www.diskordia.ch/blog/wp-content/uploads/2009/03/subview.png - Daniel McClelland
没有什么阻止你将子视图放在主视图中 - 逻辑上,你的主视图仍然是轻量级的,即它的唯一责任是维护它们之间的布局并在视图之间传递委托消息;它不会控制它们的任何内容。 - Isaac Abraham

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