Winforms - MVP 模式:使用静态 ApplicationController 来协调应用程序?

Winforms - MVP Pattern: Using static ApplicationController to coordinate application?

背景

我正在构建一个 two-tiered C# .net 应用程序:

  1. 第 1 层:使用 MVP (Model-View-Presenter) 设计模式的 Winforms 客户端应用程序。
  2. 第 2 层:WebAPI RESTful 服务位于 Entity Framework 和 SQL 服务器之上。

目前,我对 Winforms 客户端应用程序的整体架构有疑问。我是编程新手(大约一年),但我在这个应用程序上取得了很好的进展。我想退后一步,re-evaluate 我目前的方法来检查我的总体方向是否正确。

应用程序域

Winforms 应用程序是一个相当简单的安全人员跟踪应用程序。主视图(表单)是应用程序的焦点,具有将内容分组到功能区域的不同部分(例如,用于跟踪人员日程安排的部分,用于跟踪谁被分配到哪里的部分等)。应用程序侧面的菜单可启动辅助视图(例如历史记录、统计信息、联系人等)。这个想法是安全办公室可以使用该应用程序来组织日常操作,然后将所有内容的详细历史记录保存在数据库中以供将来报告。

技术细节

如前所述,Winforms 客户端是使用 MVP 模式(被动视图)构建的,重点是尽可能使用依赖注入(通过 SimpleInjector IoC 容器)。每个视图(表单)都与一个演示者配对。视图实现接口,允许演示者控制视图(不管具体实现如何)。该视图引发事件供演示者订阅。目前,不允许演示者直接与其他演示者交流。

应用程序控制器用于协调应用程序。这是我最不稳定的应用程序架构领域(因此 post 标题)。应用程序控制器当前用于:

  1. 打开新视图(表单)并管理打开的表单。
  2. 通过事件聚合器促进应用程序组件之间的通信。一位演示者发布一个事件,任意数量的演示者都可以订阅该事件。
  3. 主机session信息(即安全context/logon、配置数据等)

IoC 容器已在应用程序 start-up 中注册到应用程序控制器中。这允许应用程序控制器,例如,从容器创建一个展示器,然后让容器自动处理所有后续依赖项(视图、服务等)。

问题

为了让所有演示者都可以访问应用程序控制器,我将控制器创建为静态 class。

public static class ApplicationController
{
    private static Session _session;
    private static INavigationWorkflow _workflow;
    private static EventAggregator _aggregator;

    #region Registrations

    public static void RegisterSession(Session session) {}

    public static void RegisterWorkflow(INavigationWorkflow workflow) {}

    public static void RegisterAggregator(EventAggregator aggregator) {}

    #endregion

    #region Properties

    public static Session Session
    {
        get { return _session; }
    }

    #endregion

    #region Navigation

    public static void NavigateToView(Constants.View view) {}

    #endregion

    #region Events

    public static Subscription<TMessageType> Subscribe<TMessageType>(Action<TMessageType> action) {}

    public static void Publish<TMessageType>(TMessageType message) {}

    public static void Unsubscribe<TMessageType>(Subscription<TMessageType> subscription) {}

    #endregion
}

像这样制作静态 class 是否被认为是一种可接受的做法?我的意思是,它确实有效。只是感觉……不对?根据我所描述的内容,您是否可以在我的架构中看到任何其他漏洞?

-

** 编辑 **

此编辑是为了回应 Ric .Net 在下面的回答post。

我已经阅读了您的所有建议。由于我致力于尽我所能地利用依赖注入,因此我接受了您的所有建议。那是我一开始的计划,但是当我 运行 遇到一些我不明白如何通过注入来完成的事情时,我求助于全局静态控制器 class 来解决我的问题(大神 class 它确实正在成为。哎呀!)。其中一些问题仍然存在:

事件聚合器

我认为这里的定义行应该被认为是可选的。在概述我的问题之前,我将提供更多关于我的应用程序的背景信息。使用网络术语,我的主窗体通常像 layout view,在左侧菜单中托管导航控件和通知部分,在中心托管部分视图。回到 winforms 术语,部分视图只是自定义的用户控件,我将其视为视图,并且每个视图都与自己的演示者配对。我有 6 个这样的部分视图托管在我的主窗体上,它们充当应用程序的主要部分。

例如,一个部分视图列出了可用的保安人员,另一个列出了可能的巡逻区域。在典型的用例中,用户会将可用的保安人员从可用列表拖到一个潜在的巡逻区域,从而有效地分配到该区域。然后巡逻区域视图将更新以显示分配的保安人员,并且保安人员将从可用列表视图中删除。利用 drag-and-drop 个事件,我可以处理此交互。

当我需要处理各种局部视图之间的其他类型的交互时,我的问题就来了。例如,双击 guard that i分配到一个位置(如在一个局部视图中所见)可以在另一个局部视图上突出显示该警卫的名字,显示所有人员时间表,或在另一个局部视图中调出员工 details/history。我可以看到 graph/matrix 哪些部分视图对其他部分视图中发生的事件感兴趣变得非常复杂,我不确定如何通过注入来处理它。有 6 个部分视图,我不想将其他 5 个部分 views/presenters 注入到每个视图中。我正计划通过事件聚合器来完成这个。我能想到的另一个例子是需要根据主窗体的一个局部视图上发生的事件更新单独视图(它自己的窗体)上的数据。

Session & 开窗器

我真的很喜欢你的想法。我将把这些想法和 运行 结合起来,看看我最终会得到什么!

安全

对于根据用户拥有的帐户类型来控制用户对某些功能的访问权限,您有何看法?我一直在网上阅读的建议说,可以通过根据他们的帐户类型修改视图来实现安全性。这个想法是,如果用户不能与 UI 元素交互来启动某个任务,那么演示者将永远不会被要求执行该任务。我很好奇你是否将 WindowsUserContext 注入每个演示者并进行额外的检查,尤其是对于 http 服务绑定请求?

我还没有在服务方面做太多开发,但是对于 http 服务绑定请求,我想您需要随每个请求一起发送安全信息,以便服务可以对请求进行身份验证。我的计划是将 WindowsUserContext 直接注入到最终发出服务请求的 winforms 服务代理中(即安全验证不会来自演示者)。在这种情况下,服务代理可能会在发送请求之前进行最后一分钟的安全检查。

静态 class 在某些情况下当然很方便,但这种方法有很多缺点。

  • 倾向于成长为神一样的东西class。你已经看到这种情况发生了。所以这个 class 违反了 SRP
  • 静态 class 不能有依赖关系,因此它需要使用 Service Locator anti pattern to get it's dependencies. This is not a problem perse if you consider this class to be part of the composition root,但尽管如此,这通常会导致错误的方式。

在提供的代码中,我看到了这个 class 的三个职责。

  1. 事件聚合器
  2. 你叫什么Session信息
  3. 打开其他视图的服务

对这三个部分的一些反馈:

EventAggregator

虽然这是一个广泛使用的模式,有时它可能非常强大,但我自己并不喜欢这种模式。我将此模式视为提供 optional runtime data 的东西,在大多数情况下,此 运行 时间数据根本不是可选的。换句话说,仅将此模式用于真正可选的数据。对于所有不是真正可选的东西,使用硬依赖,使用构造函数注入。

在那种情况下需要信息的人取决于 IEventListener<TMessage>。发布事件的那个,取决于IEventPublisher<TMessage>.

public interface IEventListener<TMessage> 
{
    event Action<TMessage> MessageReceived;
}

public interface IEventPublisher<TMessage> 
{
    void Publish(TMessage message);
}

public class EventPublisher<TMessage> : IEventPublisher<TMessage> 
{
    private readonly EventOrchestrator<TMessage> orchestrator;

    public EventPublisher(EventOrchestrator<TMessage> orchestrator) 
    {
        this.orchestrator = orchestrator;
    }

    public void Publish(TMessage message) => this.orchestrator.Publish(message);
}

public class EventListener<TMessage> : IEventListener<TMessage> 
{
    private readonly EventOrchestrator<TMessage> orchestrator;

    public EventListener(EventOrchestrator<TMessage> orchestrator) 
    {
        this.orchestrator = orchestrator;
    }

    public event Action<TMessage> MessageReceived 
    {
        add { orchestrator.MessageReceived += value; }
        remove { orchestrator.MessageReceived -= value; }
    }
}

public class EventOrchestrator<TMessage> 
{
    public void Publish(TMessage message) => this.MessageReceived(message);
    public event Action<TMessage> MessageReceived = (e) => { };
}

为了能够保证事件存储在一个位置,我们将该存储(event)提取到它自己的 class、EventOrchestrator.

报名情况如下:

container.RegisterSingleton(typeof(IEventListener<>), typeof(EventListener<>));
container.RegisterSingleton(typeof(IEventPublisher<>), typeof(EventPublisher<>));
container.RegisterSingleton(typeof(EventOrchestrator<>), typeof(EventOrchestrator<>));

用法很简单:

public class SomeView
{
    private readonly IEventPublisher<GuardChanged> eventPublisher;

    public SomeView(IEventPublisher<GuardChanged> eventPublisher)
    {
        this.eventPublisher = eventPublisher;
    }

    public void GuardSelectionClick(Guard guard)
    {
        this.eventPublisher.Publish(new GuardChanged(guard));
    }
    // other code..
}

public class SomeOtherView
{
    public SomeOtherView(IEventListener<GuardChanged> eventListener)
    {
        eventListener.MessageReceived += this.GuardChanged;
    }

    private void GuardChanged(GuardChanged changedGuard)
    {
        this.CurrentGuard = changedGuard.SelectedGuard;
    }
    // other code..
}

如果另一个视图将收到大量事件,您始终可以将该视图的所有 IEventListeners 包装在特定的 EventHandlerForViewX class 中,这会注入所有重要的 IEventListener<>

会话

在问题中你定义了几个ambient context变量作为Session信息。通过静态 class 公开此类信息会促进与此静态 class 的紧密耦合,从而使对应用程序的各个部分进行单元测试变得更加困难。 IMO Session 提供的所有信息都是静态的(从某种意义上说,它在应用程序的整个生命周期中都不会改变)数据可以很容易地注入到实际需要这些数据的那些部分。所以 Session 应该完全从静态 class 中删除。一些如何以 SOLID 方式解决此问题的示例:

配置值

组合根负责从配置源(例如您的 app.config 文件)读取所有信息。此信息可以存储在为其使用而制作的 POCO class 中。

public interface IMailSettings
{
    string MailAddress { get; }
    string DefaultMailSubject { get; }
}

public interface IFtpInformation
{
    int FtpPort { get; }
}

public interface IFlowerServiceInformation
{
    string FlowerShopAddress { get; }
}

public class ConfigValues :
    IMailSettings, IFtpInformation, IFlowerServiceInformation
{
    public string MailAddress { get; set; }
    public string DefaultMailSubject { get; set; }

    public int FtpPort { get; set; }

    public string FlowerShopAddress { get; set; }
}
// Register as
public static void RegisterConfig(this Container container)
{
    var config = new ConfigValues
    {
        MailAddress = ConfigurationManager.AppSettings["MailAddress"],
        DefaultMailSubject = ConfigurationManager.AppSettings["DefaultMailSubject"],
        FtpPort = Convert.ToInt32(ConfigurationManager.AppSettings["FtpPort"]),
        FlowerShopAddress = ConfigurationManager.AppSettings["FlowerShopAddress"],
    };

    var registration = Lifestyle.Singleton.CreateRegistration<ConfigValues>(() => 
                                config, container);
    container.AddRegistration(typeof(IMailSettings),registration);
    container.AddRegistration(typeof(IFtpInformation),registration);
    container.AddRegistration(typeof(IFlowerServiceInformation),registration);
}

以及您需要一些特定信息的地方,例如发送电子邮件的信息,您只需将 IMailSettings 放入需要该信息的类型的构造函数中即可。

这也让您可以使用不同的配置值测试组件,如果所有配置信息都必须来自静态 ApplicationController.

,这将更难做到。

用于安全信息,例如登录用户可以使用相同的模式。定义一个 IUserContext 抽象,创建一个 WindowsUserContext 实现,并用组合根中的登录用户填充它。因为该组件现在依赖于 IUserContext 而不是在 运行 时间从静态 class 获取用户,所以相同的组件也可以用在 MVC 应用程序中,您可以在其中替换 WindowsUserContextHttpUserContext 实施。

打开其他表格

这实际上是最难的部分。我通常也使用一些大的静态 class 和各种方法来打开其他表格。我不会将 中的 IFormOpener 公开给我的其他表单,因为他们只需要知道要做什么,而不是哪个表单为他们完成该任务。所以我的静态 class 公开了这种方法:

public SomeReturnValue OpenCustomerForEdit(Customer customer)
{ 
     var form = MyStaticClass.FormOpener.GetForm<EditCustomerForm>();
     form.SetCustomer(customer);
     var result = MyStaticClass.FormOpener.ShowModalForm(form);
     return (SomeReturnValue) result;
}

然而....

我对这种方法一点也不满意,因为随着时间的推移,这种 class 会越来越大。对于 WPF,我使用另一种机制,我认为它也可以用于 WinForms。这种方法基于 this and this 精彩博客post 中描述的基于消息的架构。虽然乍一看这些信息看起来根本不相关,但正是基于消息的概念让这些模式动起来了!

我所有的 WPF windows 都实现了一个开放的通用接口,例如我编辑视图。如果某些视图需要编辑客户,则只需注入此 IEditView。装饰器用于以与上述 FormOpener 几乎相同的方式实际显示视图。在这种情况下,我使用了一个特定的简单注入器功能,称为 decorate factory decorator,您可以在需要时使用它来创建表单,就像 FormOpener 在需要时直接使用容器来创建表单一样到。

所以我没有真正测试过这个,所以 WinForms 可能会有一些缺陷,但这段代码似乎在第一个和单个 运行..

public class EditViewShowerDecorator<TEntity> : IEditView<TEntity>
{
    private readonly Func<IEditView<TEntity>> viewCreator;

    public EditViewShowerDecorator(Func<IEditView<TEntity>> viewCreator)
    {
        this.viewCreator = viewCreator;
    }

    public void EditEntity(TEntity entity)
    {
        // get view from container
        var view = this.viewCreator.Invoke();
        // initview with information
        view.EditEntity(entity);
        using (var form = (Form)view)
        {
            // show the view
            form.ShowDialog();
        }
    }
}

表单和装饰器应注册为:

container.Register(typeof(IEditView<>), new[] { Assembly.GetExecutingAssembly() });
container.RegisterDecorator(typeof(IEditView<>), typeof(EditViewShowerDecorator<>), 
                             Lifestyle.Singleton);

安全

IUserContext 必须是所有安全的基础。

对于用户界面,我通常会隐藏某个用户角色无权访问的所有 controls/buttons。最好的地方是在 Load 事件中执行此操作。

因为我使用 command/handler 描述的模式 here 我在 forms/views 之外的所有操作我使用装饰器来检查用户是否有权执行此特定命令(或查询)。

我建议您多读几遍 post,直到您真正掌握其中的诀窍。一旦你熟悉了这个模式,你就不会做任何其他事情了!

如果您对这些模式以及如何应用(许可)装饰器有任何疑问,请添加评论!