gpt4 book ai didi

c# - Winforms - MVP 模式 : Using static ApplicationController to coordinate application?

转载 作者:行者123 更新时间:2023-12-01 16:05:22 26 4
gpt4 key购买 nike

背景

我正在构建一个两层的 C# .net 应用程序:

  • 第 1 层:使用 MVP(模型- View -演示者)设计模式的 Winforms 客户端应用程序。
  • 第 2 层:位于 Entity Framework 和 SQL Server 之上的 WebAPI RESTful 服务。

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

    应用域

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

    技术细节

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

    应用程序 Controller 用于协调应用程序。这是我的应用程序架构中我最不稳定的领域(因此是帖子标题)。应用程序 Controller 目前用于:
  • 打开新 View (表单)并管理打开的表单。
  • 通过事件聚合器促进应用程序组件之间的通信。一个演示者发布一个事件,任何数量的演示者都可以订阅该事件。
  • 主机 session 信息(即安全上下文/登录、配置数据等)

  • IoC 容器在应用程序启动时注册到应用程序 Controller 中。例如,这允许应用程序 Controller 从容器创建展示器,然后让容器自动处理所有后续依赖项( View 、服务等)。

    问题

    为了让所有演示者都可以访问应用程序 Controller ,我将 Controller 创建为静态类。
    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
    }

    这是否被认为是制作这样一个静态类的可接受的做法?我的意思是,它肯定有效。只是感觉……不对劲?根据我的描述,您在我的架构中是否还有其他漏洞?

    ——

    ** 编辑 **

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

    我已经阅读了你所有的建议。由于我致力于最大程度地利用依赖注入(inject),因此我同意您的所有建议。这是我一开始的计划,但是当我遇到不明白如何通过注入(inject)来完成的事情时,我转向全局静态 Controller 类来解决我的问题(它正在成为一个神类,确实如此。哎呀!) .其中一些问题仍然存在:

    事件聚合器

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

    例如,一个局部 View 列出了可用的保安人员,另一个列出了潜在的巡逻区域。在典型的用例中,用户会将可用的保安人员从可用列表拖到潜在的巡逻区域之一,从而有效地分配到该区域。然后巡逻区域 View 将更新以显示指定的保安,并且保安将从可用 ListView 中移除。利用拖放事件,我可以处理这种交互。

    当我需要处理各种局部 View 之间的其他类型的交互时,我的问题就出现了。例如,双击分配到某个位置的 guard (如一个局部 View 中所示)可以在显示所有人员时间表的另一个局部 View 上突出显示该 guard 的姓名,或者在另一个局部 View 中显示员工详细信息/历史记录。我可以看到哪些部分 View 对其他部分 View 中发生的事件感兴趣的图形/矩阵变得非常复杂,但我不确定如何通过注入(inject)来处理它。有 6 个局部 View ,我不想将其他 5 个局部 View /演示者注入(inject)到每个 View 中。我计划通过事件聚合器来完成这个。我能想到的另一个例子是需要根据发生在主窗体上的一个局部 View 上的事件更新单独 View (它自己的窗体)上的数据。

    session 和表格开场白

    我真的很喜欢你在这里的想法。我将接受这些想法并与它们一起运行,看看我最终会在哪里!

    安全

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

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

    最佳答案

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

  • 倾向于成长为类似神级的东西。你已经看到这种情况发生了。所以这个类违反了SRP
  • 静态类不能有依赖项,因此它需要使用 Service Locator anti pattern获取它的依赖项。如果你认为这个类是 composition root 的一部分,这本身就不是问题。 ,但尽管如此,这往往是错误的。

  • 在提供的代码中,我看到了这个类的三个职责。
  • 事件聚合器
  • 你叫什么Session信息
  • 打开其他 View 的服务

  • 关于这三个部分的一些反馈:

    事件聚合器

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

    在这种情况下需要信息的那些取决于 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)提取到它自己的类中, 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..
    }

    如果另一个 View 将接收大量事件,您可以始终将该 View 的所有 IEventListeners 包装在特定的 EventHandlerForViewX 中。得到所有重要的类 IEventListener<>注入(inject)。

    专场

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

    配置值

    组合根负责从配置源(例如您的 app.config 文件)读取所有信息。此信息可以存储在为其使用而设计的 POCO 类中。
    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除了在运行时从静态类获取用户之外,还可以在 MVC 应用程序中使用相同的组件,您可以在其中替换 WindowsUserContextHttpUserContext执行。

    开通其他表格

    这实际上是最困难的部分。我通常也会使用一些带有各种方法的大型静态类来打开其他表单。我不暴露 IFormOpener来自 this answer到我的其他形式,因为他们只需要知道要做什么,而不是哪种形式为他们完成这项任务。所以我的静态类公开了这种方法:
    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 中描述的基于消息的架构。和 this很棒的博文。虽然起初信息看起来完全不相关,但正是基于消息的概念让这些模式摇摆不定!

    我所有的 WPF 窗口都实现了一个开放的通用接口(interface),例如编辑 View 。如果某些 View 需要编辑客户,则只需注入(inject)此 IEditView。装饰器用于以与上述 FormOpener 几乎相同的方式实际显示 View 。可以。在这种情况下,我使用了一个特定的 Simple Injector 功能,称为 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必须是所有安全的基础。

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

    因为我使用了如 here 所述的命令/处理程序模式对于我的表单/ View 外部的所有操作,我使用装饰器来检查用户是否有权执行此特定命令(或查询)。

    我建议你多读几遍这篇文章,直到你真正掌握它的窍门。一旦你熟悉了这个模式,你就不会再做任何其他事情了!

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

    关于c# - Winforms - MVP 模式 : Using static ApplicationController to coordinate application?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/38471351/

    26 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com