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

背景

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

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

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

应用领域

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

技术细节

如前所述,Winforms客户端是使用MVP模式(被动视图)构建的,重点是尽可能多地使用dependency injection(通过SimpleInjector IoC容器)。 每个视图(表单)都与一个演示者配对。 视图实现接口,允许演示者控制视图(无论具体实现如何)。 该视图会引发演示者订阅的事件。 目前,不允许演示者直接与另一位演示者通信。

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

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

在应用程序启动时,IoC容器已注册到应用程序控制器中。 例如,这允许应用程序控制器从容器创建演示者,然后由容器自动处理所有后续依赖项(视图,服务等)。

为了使所有演示者都可以访问Application 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 Subscribe(Action action) {} public static void Publish(TMessageType message) {} public static void Unsubscribe(Subscription subscription) {} #endregion } 

这被认为是一个可接受的做法,使这样的静态类? 我的意思是,它确实有效。 它只是感觉……关闭? 根据我所描述的内容,您可以在我的架构中看到任何其他漏洞吗?

** 编辑 **

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

我已经阅读了你的所有建议。 因为我致力于尽可能充分利用dependency injection,所以我对你提出了所有的建议。 这是我从一开始的计划,但当我遇到的事情我不明白如何通过注入完成时,我转向全局静态控制器类来解决我的问题(它正在成为一个上帝阶级。确实。哎呀!) 。 其中一些问题仍然存在:

事件聚合器

我认为,这里的定义线应该被认为是可选的。 在概述我的问题之前,我将提供有关我的应用程序的更多上下文。 使用网络术语,我的主要表单通常表现为布局视图 ,托管导航控件和左侧菜单中的通知部分,以及托管在中心的部分视图。 回到winforms术语,部分视图只是定制的UserControls,我把它视为视图,并且每个都与他们自己的演示者配对。 我在我的主表单上托管了6个部分视图,它们作为应用程序的肉和土豆。

例如,一个局部视图列出了可用的安全防护,另一个列出了潜在的巡逻区域。 在典型的使用案例中,用户将可用的保安从可用列表拖动到潜在的巡逻区域之一,实际上被分配到该区域。 然后巡逻区域视图将更新以显示分配的安全防护,并且将从可用列表视图中移除防护。 利用拖放事件,我可以处理这种交互。

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

会话和表格开启者

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

安全

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

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

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

  • 往往会变成像上帝阶级的东西。 你已经看到了这种情况。 所以这个类违反了SRP
  • 静态类不能具有依赖关系,因此需要使用Service Locator反模式来获取它的依赖关系。 如果你认为这个类是组合根的一部分,这不是一个问题,但是,这往往是错误的方式。

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

  1. EventAggregator
  2. 您所谓的Session信息
  3. 用于打开其他视图的服务

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

EventAggregator

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

在这种情况下需要信息的是依赖于IEventListener 。 发布事件的那个依赖于IEventPublisher

 public interface IEventListener { event Action MessageReceived; } public interface IEventPublisher { void Publish(TMessage message); } public class EventPublisher : IEventPublisher { private readonly EventOrchestrator orchestrator; public EventPublisher(EventOrchestrator orchestrator) { this.orchestrator = orchestrator; } public void Publish(TMessage message) => this.orchestrator.Publish(message); } public class EventListener : IEventListener { private readonly EventOrchestrator orchestrator; public EventListener(EventOrchestrator orchestrator) { this.orchestrator = orchestrator; } public event Action MessageReceived { add { orchestrator.MessageReceived += value; } remove { orchestrator.MessageReceived -= value; } } } public class EventOrchestrator { public void Publish(TMessage message) => this.MessageReceived(message); public event Action 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 eventPublisher; public SomeView(IEventPublisher eventPublisher) { this.eventPublisher = eventPublisher; } public void GuardSelectionClick(Guard guard) { this.eventPublisher.Publish(new GuardChanged(guard)); } // other code.. } public class SomeOtherView { public SomeOtherView(IEventListener eventListener) { eventListener.MessageReceived += this.GuardChanged; } private void GuardChanged(GuardChanged changedGuard) { this.CurrentGuard = changedGuard.SelectedGuard; } // other code.. } 

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

会议

在这个问题中,您将几个ambient context变量定义为Session信息。 通过静态类公开这种信息可以促进与这个静态类的紧密耦合,从而使unit testing应用程序的各个部分变得更加困难。 IMO提供的所有信息都是静态的(在应用程序的整个生命周期内它不会改变)数据,这些数据可以很容易地注入到实际需要这些数据的那些部分中。 所以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(() => config, container); container.AddRegistration(typeof(IMailSettings),registration); container.AddRegistration(typeof(IFtpInformation),registration); container.AddRegistration(typeof(IFlowerServiceInformation),registration); } 

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

这也使您可以使用不同的配置值测试组件,如果所有配置信息都必须来自静态ApplicationController ,那么这将更难做到。

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

打开其他表格

这实际上是困难的部分。 我通常也会使用一些带有各种方法的大型静态类来打开其他表单。 我不会将IFormOpener从这个答案暴露给我的其他forms,因为他们只需知道,做什么,而不是为他们做什么forms的任务。 所以我的静态类暴露了这种方法:

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

然而….

我对这种方法并不满意,因为随着时间的推移,这门课程会不断发展壮大。 使用WPF我使用另一种机制,我认为也可以与WinForms一起使用。 这种方法基于这个和这个很棒的博客中描述的基于消息的体系结构。 虽然起初信息看起来并不是完全相关的,但是基于消息的概念让这些模式摇滚!

我的所有WPF窗口都实现了一个开放的通用接口,例如IEditView。 如果某些视图需要编辑客户,则只需注入此IEditView即可。 装饰器用于实际显示视图,其方式与FormOpener 。 在这种情况下,我使用了一个特定的Simple Injectorfunction,称为装饰工厂装饰器 ,您可以在需要时使用它来创建表单,就像FormOpener在需要时直接使用容器创建表单一样。

所以我没有真正测试过这个,所以WinForms可能会有一些陷阱,但是这个代码似乎可以在第一次和单次运行时运行。

 public class EditViewShowerDecorator : IEditView { private readonly Func> viewCreator; public EditViewShowerDecorator(Func> 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事件中执行此操作。

因为我使用这里描述的命令/处理程序模式来处理我的表单/视图外部的所有操作,所以我使用装饰器来检查用户是否有权执行此特定命令(或查询)。

我会建议你阅读这篇文章几次,直到你真正掌握它为止。 一旦你熟悉这种模式,你将不会做任何其他事情!

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