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



背景

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

  1. Tier 1:Winforms客户端应用程序使用MVP(模型视图演示者)设计模式
  2. 第2层:位于实体框架和SQL Server之上的WebAPI RESTful服务

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

应用程序域

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

技术细节

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

应用程序控制器用于协调应用程序。这是我的应用程序体系结构中最稳定的领域(因此才有了这个标题)。应用程序控制器当前用于:

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

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

问题

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

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的回答如下。

你的所有建议我都读过了。由于我致力于最大限度地利用依赖注入,我赞同您的所有建议。这是我从一开始的计划,但当我遇到不知道如何通过注入完成的事情时,我转向全局静态控制器类来解决我的问题(它确实正在成为一个神类。哎呀!)。其中一些问题仍然存在:

事件聚合器

我认为,这里的定义线是什么应该被视为可选的。在概述我的问题之前,我将提供更多关于我的应用程序的上下文。使用web术语,我的主窗体通常充当布局视图,在左侧菜单中托管导航控件和通知部分,部分视图托管在中心。回到winforms术语,部分视图只是自定义的UserControls,我将其视为视图,并且每个视图都与自己的演示者配对。我的主表单上有6个部分视图,它们是应用程序的核心。

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

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

会话&Form Opener

我真的很喜欢你的想法。我将接受这些想法,并与它们一起奔跑,看看我的结局如何!

安全

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

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

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

  • 倾向于成长为一个类似上帝的阶层。你已经看到这种情况发生了。所以这个类违反了SRP
  • 静态类不能有依赖项,因此它需要使用ServiceLocator反模式来获取它的依赖项。如果你认为这个类是composition根的一部分,这就不是问题,但尽管如此,这往往会导致错误的方向

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

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

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

事件聚合器

虽然这是一种广泛使用的模式,有时它可能非常强大,但我自己并不喜欢这种模式。我认为这个模式提供了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)提取到它自己的类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..
}

如果另一个视图将接收到大量事件,则可以始终将该视图的所有IEventListener封装在特定的EventHandlerForViewX类中,从而注入所有重要的IEventListener<>

会话

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

配置值

composition root负责从配置源(例如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实现,并在composition root中使用登录用户填充该实现。因为该组件现在依赖于IUserContext,而不是在运行时从静态类获取用户,所以在MVC应用程序中也可以使用相同的组件,在该应用程序中,您可以用HttpUserContext实现替换WindowsUserContext

打开其他表单

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

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

然而。。。。

我对这种方法一点也不满意,因为随着时间的推移,这个班越来越大。对于WPF,我使用了另一种机制,我认为它也可以与WinForms一起使用。这种方法基于一个基于消息的体系结构,在这篇和这篇精彩的博客文章中有描述。尽管一开始这些信息看起来根本不相关,但正是基于消息的概念让这些模式摇摆不定!

我所有的WPF窗口都实现了一个开放的通用接口,例如IEditView。如果某个视图需要编辑一个客户,它只需要注入这个IEditView。decorator用于实际显示视图,其方式与前面提到的FormOpener几乎相同。在这种情况下,我使用了一个特定的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事件中执行此操作。

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

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

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

相关内容

  • 没有找到相关文章

最新更新