如何为MVP WinForms应用程序的主要Presenter调用Application.Run()方法?

4

我正在学习如何将MVP应用于C#中的简单WinForms应用程序(仅有一个表单),在创建主要Presenter的static void Main()时遇到了问题。将View从Presenter中公开以便将其作为参数提供给Application.Run()是否是一个好主意?

目前,我已经实现了一种方法,可以允许我不将View公开为Presenter的属性:

    static void Main()
    {
        IView view = new View();
        Model model = new Model();
        Presenter presenter = new Presenter(view, model);
        presenter.Start();
        Application.Run();
    }

Presenter中的启动和停止方法:
    public void Start()
    {
        view.Start();
    }

    public void Stop()
    {
        view.Stop();
    }

在 Windows Form 中,View 的 Start 和 Stop 方法:
    public void Start()
    {
        this.Show();
    }

    public void Stop()
    {
        // only way to close a message loop called 
        // via Application.Run(); without a Form parameter
        Application.Exit();
    }

Application.Exit()调用似乎是一种不太优雅的关闭窗体(和应用程序)的方式。另一种选择是将视图公开为Presenter的公共属性,以便使用Form参数调用Application.Run()。

    static void Main()
    {
        IView view = new View();
        Model model = new Model();
        Presenter presenter = new Presenter(view, model);
        Application.Run(presenter.View);
    }

在 Presenter 中,Start 和 Stop 方法保持不变。增加了一个附加属性来返回 View 作为 Form:
    public void Start()
    {
        view.Start();
    }

    public void Stop()
    {
        view.Stop();
    }

    // New property to return view as a Form for Application.Run(Form form);
    public System.Windows.Form View
    {
        get { return view as Form(); }
    }

在Windows窗体中,View中的Start和Stop方法应该写成如下形式:

    public void Start()
    {
        this.Show();
    }

    public void Stop()
    {
        this.Close();
    }

有人能建议哪种方法更好,为什么?还是有更好的方法来解决这个问题吗?

我感谢Roger、Heinzi和Nicole的宝贵意见,但最终我选择了Heinzi的答案,因为我已经使用了他的答案并为我的实际应用程序添加了两个额外的接口IMainPresenter和IMainView。 - anonymous
4个回答

9
以下是相关内容:

以下是什么情况:

// view
public void StartApplication() // implements IView.StartApplication
{ 
    Application.Run((Form)this);
}

// presenter
public void StartApplication()
{
    view.StartApplication();
}

// main
static void Main()     
{     
    IView view = new View();     
    Model model = new Model();     
    Presenter presenter = new Presenter(view, model);     
    presenter.StartApplication();     
}     

这样,您无需将视图暴露给外部。此外,视图和Presenter知道此视图已作为“主窗体”启动,这可能是一条有用的信息。


1
在我看来,展示者或视图接口都不应该知道如何启动 Windows 消息泵。如果你在针对虚拟视图编写测试,那么你只需要废话 IView 接口即可,因为这种方法根本就不存在。 - Roger Johansson
1
是否可以为视图和Presenter创建另一个接口,例如IMainView、IMainPresenter,专门处理StartApplication()和StopApplication()方法。因此,View将实现IView和IMainView,Presenter将实现IMainPresenter。 - anonymous
1
@Mr Roys:这是个好主意。你甚至可以将IMainView和IMainPresenter作为IView和IPresenter的子接口,因为IMainView始终是一个IView(同样适用于Presenter)。 - Heinzi

5

我会选择第二种方法。在void Main中,你也可以通过将view强制转换为form来摆脱额外的属性,因为你知道此时它肯定是一个form(我认为没有必要使它比那更通用,因为它只是启动winform应用程序)。

Application.Run(view as Form);

1

如果允许多种方式退出应用程序(例如:退出菜单项),或者在某些条件下阻止关闭应用程序,情况会变得更加复杂。无论哪种情况,实际调用应用程序关闭通常应该是从展示器代码中调用,而不仅仅是关闭具体的视图。可以通过使用Application.Run()或Application.Run(ApplicationContext)重载并通过控制反转公开应用程序退出操作来实现。

注册和使用应用程序退出操作的确切方法取决于您正在使用的IoC机制(例如:服务定位器和/或依赖注入)。由于您没有提到您当前的IoC方法可能是什么,这里是一个独立于任何特定IoC框架的示例:

internal static class Program
{
    [STAThread]
    private static void Main()
    {
        ApplicationActions.ExitApplication = Application.Exit;

        MainPresenter mainPresenter = new MainPresenter(new MainView(), new Model());
        mainPresenter.Start();

        Application.Run(); 
    }
}

public static class ApplicationActions
{
    public static Action ExitApplication { get; internal set; }
}

public class MainPresenter : Presenter
{
    //...

    public override void Stop()
    {
        base.Stop();

        ApplicationActions.ExitApplication();
    }
}

这种基本方法可以很容易地适应您喜欢的IoC方法。例如,如果您正在使用服务定位器,您可能希望考虑至少删除ApplicationActions.ExitApplication属性上的setter,并将委托存储在服务定位器中。如果ExitApplication getter仍然存在,它将为服务定位器实例检索器提供一个简单的外观。例如:

public static Action ExitApplication
{
    get
    {
        return ServiceLocator.GetInstance<Action>("ExitApplication");
    }
}

最初,我打算将Application.Exit()放在presenter.Stop()中,但后来决定不这样做,因为那会在presenter中创建对实现视图的技术(例如WinForms)的依赖。与此同时,我会研究IoC - 有没有办法用一些伪代码来说明它? - anonymous
您想要哪些实现部分的示例? - Nicole Calinoiu
应用程序退出(Application.Exit())部分的IoC部分会很好。谢谢! - anonymous
我已经在上面的答案中添加了代码示例。鉴于您之前提到选择使用MainPresenter和IMainView方法,我已经将这个选择纳入考虑,允许Presenter知道作为其停止的一部分,它应该退出应用程序。 - Nicole Calinoiu
嗨,Nicole,很抱歉我没有等你的示例代码就决定接受答案,因为那天我必须现场。谢谢您提供的示例代码,非常感谢 :) - anonymous

0

有很多方法可以实现关注点分离的最终目标,没有硬性规定,基本思想是Presenter处理视图的呈现逻辑,而View只具有其自身GUI特定类和内容的愚蠢知识。我能想到的一些方法(大致上):

1)View启动并决定其Presenter。你可以这样开始:new View().Start();

// your reusable MVP framework project 
public interface IPresenter<V>
{
    V View { get; set; }
}
public interface IView<P>
{
    P Presenter { get; }
}
public static class PresenterFactory
{
    public static P Presenter<P>(this IView<P> view) where P : new()
    {
        var p = new P();
        (p as dynamic).View = view;
        return p;
    }
}

// your presentation project
public interface IEmployeeView : IView<EmployeePresenter>
{
    void OnSave(); // some view method
}
public class EmployeePresenter : IPresenter<IEmployeeView>
{
    public IEmployeeView View { get; set; } // enforced

    public void Save()
    {
        var employee = new EmployeeModel
        {
            Name = View.Bla // some UI element property on IEmployeeView interface
        };
        employee.Save();
    }
}

// your view project
class EmployeeView : IEmployeeView
{
    public EmployeePresenter Presenter { get; } // enforced

    public EmployeeView()
    {
        Presenter = this.Presenter(); // type inference magic
    }

    public void OnSave()
    {
        Presenter.Save();
    }
}

上述方法的一种变体是对视图和Presenter强制实施更强的通用约束,但我认为复杂性不如收益。类似这样的东西:
// your reusable MVP framework project 
public interface IPresenter<P, V> where P : IPresenter<P, V> where V : IView<P, V>
{
    V View { get; set; }
}
public interface IView<P, V> where P : IPresenter<P, V> where V : IView<P, V>
{
    P Presenter { get; }
}
public static class PresenterFactory
{
    public static P Presenter<P, V>(this IView<P, V> view)
        where P : IPresenter<P, V>, new() where V : IView<P, V>
    {
        return new P { View = (V)view };
    }
}

// your presentation project
public interface IEmployeeView : IView<EmployeePresenter, IEmployeeView>
{
    //...
}
public class EmployeePresenter : IPresenter<EmployeePresenter, IEmployeeView>
{
    //...
}

缺点

  • 我对表单之间的交互不太直观。

涉及的步骤:

  • 实现IEmployeeView
  • 通过从视图构造函数传递this来调用PresenterFactory并实例化presenter
  • 确保视图事件连接到其相应的presenter方法
  • 开始,像new EmployeeView()...

2) Presenter启动并决定其视图。您可以像这样开始:new Presenter().Start();

在这种方法中,Presenter通过某些依赖注入或其他方式实例化自己的视图(如方法1),或者视图可以传递给Presenter的构造函数。例如:

// your reusable MVP framework project 
public abstract class IPresenter<V> // OK may be a better name here
{
    protected V View { get; }

    protected IPresenter()
    {
        View = ...; // dependenchy injection or some basic reflection, or pass in view to ctor
        (View as dynamic).Presenter = this;
    }
}
public interface IView<P>
{
    P Presenter { get; set; }
}

// your presentation project
public interface IEmployeeView : IView<EmployeePresenter>
{
    void OnSave(); // some view method
}
public class EmployeePresenter : IPresenter<IEmployeeView>
{
    public void Save()
    {
        var employee = new EmployeeModel
        {
            Name = View.Bla // some UI element property on IEmployeedView interface
        };
        employee.Save();
    }
}

// your view project
class EmployeeView : IEmployeeView
{
    public EmployeePresenter Presenter { get; set; } // enforced

    public void OnSave()
    {
        Presenter.Save();
    }
}

步骤如下:

  • 实现 IEmployeeView
  • 确保视图事件与其对应的 Presenter 方法相连
  • 开始,像 new EmployeePresenter(...

3) 基于事件、观察者模式

在这种方式中,你可以像第一种方法一样将 Presenter 封装在 View 中(在 View 中实例化 Presenter),也可以像第二种方法一样将 View 封装在 Presenter 中(在 Presenter 中实例化 View)。但是根据我的经验,后者总是更加清晰易懂的设计。以下是一个例子:

// your reusable MVP framework project
public abstract class IPresenter<V> where V : IView
{
    protected V View { get; }

    protected IPresenter()
    {
        View = ...; // dependenchy injection or some basic reflection, or pass in view to ctor
        WireEvents();
    }

    protected abstract void WireEvents();
}

// your presentation project
public interface IEmployeeView : IView
{
    // events helps in observing
    event Action OnSave; // for e.g.
}
public class EmployeePresenter : IPresenter<IEmployeeView>
{
    protected override void WireEvents()
    {
        View.OnSave += OnSave;
    }

    void OnSave()
    {
        var employee = new EmployeeModel
        {
            Name = View.Bla // some UI element property on IEmployeedView interface
        };
        employee.Save();
    }
}

// your view project
class EmployeeView : IEmployeeView
{
    public event Action OnSave;
    void OnClicked(object sender, EventArgs e) // some event handler
    {
        OnSave();
    }
}
// you kick off like new EmployeePresenter()....

缺点:

  • 您必须在视图和Presenter两侧连接事件-工作量加倍

涉及的步骤:

  • 实现IEmployeeView
  • 确保从视图事件处理程序方法调用iview事件
  • 确保iview事件成员从Presenter初始化
  • new EmployeePresenter()...一样开始。

语言的限制有时会使设计模式更加困难。例如,如果C#支持多重继承,那么只需要一个抽象基本视图类,其中包含除UI特定组件以外的所有实现细节,然后由视图类实现即可。没有Presenter,经典的多态和非常简单!不幸的是,这是不可能的,因为.NET中的大多数视图类(如WinForms的Form)已经继承自超级视图类。因此,我们必须实现一个接口并进行组合。此外,C#不允许您在接口实现中拥有非公共成员,因此我们被迫使IEmployeeView中指定的所有成员都是公共的,这打破了视图类的自然封装规则(即视图项目中的其他视图可以看到与它们无关的EmployeeView的详细信息)。无论如何,使用C#扩展方法的强大功能,可以采用更简单但非常有限的方法。

4) 扩展方法方法

这只是愚蠢的。

// your presentation project
public interface IEmployeeView
{
    void OnSave(); // some view method
}
public static class EmployeePresenter // OK may need a better name
{
    public void Save(this IEmployeeView view)
    {
        var employee = new EmployeeModel
        {
            Name = view.Bla // some UI element property on IEmployeedView interface
        };
        employee.Save();
    }
}

// your view project
class EmployeeView : IEmployeeView
{       
    public void OnSave()
    {
        this.Save(); // that's it. power of extensions.
    }
}

缺点:
  • 对于任何稍微复杂的事情都相当难以使用
步骤:
  • 实现IEmployeeView
  • 确保从视图事件中调用this....扩展方法
  • 通过调用new View...启动工作流程

在所有的数字中,2和3看起来最好。


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