IoC:连接事件处理程序的依赖项

时间:2012-06-15 23:16:56

标签: c# winforms dependency-injection inversion-of-control solid-principles

我正在构建一个WinForms应用程序,其UI只包含NotifyIcon及其动态填充的ContextMenuStrip。有一个MainForm可以将应用程序保存在一起,但这是永远不可见的。

我开始尽可能安全地构建它(使用Autofac来处理对象图),并且对我的成功感到非常满意,即使使用O部分也相处得很好。随着我目前正在实施的扩展,似乎我发现了我的设计中的一个缺陷,需要重新改造一下;我想知道我需要去的方式,但是如何准确定义依赖关系还不清楚。

如上所述,菜单部分在启动应用程序后动态填充。为此,我定义了一个IToolStripPopulator接口:

public interface IToolStripPopulator
{
    System.Windows.Forms.ToolStrip PopulateToolStrip(System.Windows.Forms.ToolStrip toolstrip, EventHandler itemclick);
}

将此实现注入MainFormLoad()方法使用PopulateToolStrip()调用ContextMenuStrip并在表单中定义处理程序。 populator的依赖项仅与获取用于菜单项的数据有关。

这种抽象通过一些进化步骤很好地工作,但是当我需要多个事件处理程序时,这种抽象就不够了,例如:因为我正在创建几个不同的菜单项组 - 仍然隐藏在单个IToolStripPopulator界面后面,因为表单根本不应该关注它。

正如我所说的,我想我知道一般结构应该是什么样的 - 我将IToolStripPopulator接口重命名为更具体的东西*并创建了一个新的PopulateToolStrip()方法不接受{ {1}}参数,而不是注入对象(也允许实现所需的处理程序数量等更多的灵活性)。这样,我的“最前面的”EventHandler可以非常容易地成为任何特定数量的适配器。

现在我不清楚的是我应该如何解决EventHandler依赖关系。我认为处理程序应该都在IToolStripPopulator中定义,因为它具有正确响应菜单事件所需的所有其他依赖项,并且它还“拥有”菜单。这意味着最终注入到MainForm中的MainForm个对象的依赖关系需要使用IToolStripPopulator依赖于MainForm对象本身。

我的第一个想法是定义Lazy<T>界面:

IClickHandlerSource

这是由我的public interface IClickHandlerSource { EventHandler GetClickHandler(); } 实现的,我的具体MainForm实现依赖IToolStripPopulator。虽然这有效,但它是不灵活的。我要么必须为可能越来越多的处理程序定义单独的接口(严重违反使用Lazy<IClickHandlerSource>类的OCP),要么不断扩展MainForm(主要是违反ISP)。 直接依赖事件处理程序在消费者方面看起来是一个好主意,但是通过惰性实例(或类似)的属性单独连接构造函数似乎非常混乱 - 如果可能的话。

我最好的选择似乎是:

IClickHandlerSource

接口仍然由public interface IEventHandlerSource { EventHandler Get(EventHandlerType type); } 实现,并作为延迟单例注入,MainForm将是一个自定义枚举,具有我需要的不同类型。这仍然不是非常符合OCP,但相当灵活。除了新的事件处理程序本身和(可能)新编写的{{{{ 1}}。

或.... EventHandlerType的单独实现(作为唯一对象)依赖EventHandlerType并将MainForm选项解析为{{}中定义的特定处理程序1}}?

我正试图想出一种以可行的方式实际从IToolStripPopulator中获取事件处理程序的方法,但现在看起来不太可能。

这里我最好的选择是什么,为不同的事件处理程序提供最宽松的耦合和最优雅的分辨率?

[*是的,我可能应该单独留下名字来真正遵守OCP,但它看起来更好。]

4 个答案:

答案 0 :(得分:2)

  

这里我最好的选择是什么,提供最松散的耦合和最多   优雅解决不同的事件处理程序?

通用解决方案不存在,它取决于全局应用程序架构。

如果您想要最宽松耦合,EventAggregator模式可以在这种情况下帮助您(您的IEventHandlerSource与此相似):

但是,全球事件应该与非常谨慎一起使用 - 他们可以涂抹架构,因为可以在任何地方订阅该事件。

DI和IoC中的重要事项:较低的依赖性不应该知道更高的依赖性。 我认为,正如Leon先前所说,最好创建一些类似ITool<T>的界面并在MainForm中存储工具列表。在某些操作后,MainForm将调用此工具中的某些方法。

答案 1 :(得分:1)

首先,我认为您不应声称您的主变形必须包含所有事件处理程序。你明显地遇到了这样一个事实:有不同的处理程序有不同的需求(可能还有不同的依赖关系),那么为什么它们都应该被安装到同一个类中呢? 您可以从其他语言处理事件的方式中获得灵感。 WPF使用命令,Java具有监听器。两者都是对象,而不仅仅是一个委托,使它们更容易在IOC场景中处理。模拟这样的东西相当容易。您可以滥用工具栏项上的标记,如下所示:Binding to commands in WinForms或使用PopulateToolbar中的lambda表达式(请参阅Is there anything wrong with using lambda for winforms event?)将工具栏项与正确的命令相关联。假设由于PopulateToolbar知道需要创建哪些项目,因此它也知道哪个动作/命令属于每个项目。

表示操作的对象可以独立于主窗体或其他操作注入自己的依赖项。然后可以在不影响主表单或任何其他操作的情况下添加或删除具有自己操作的工具栏项目,每个操作都可以独立测试和重构。

从底线开始,不再考虑EventHandlers,开始将Action / Commands视为一个实体,并且更容易想出合适的模式。确保您了解Command Pattern,因为这非常符合您的需求。

答案 2 :(得分:1)

您是否尝试过使用事件聚合器?请参阅:Caliburn framework, event Aggregator 事件聚合器将工具条与您的主表单分离。

public interface IToolStripPopulator
{
    ToolStrip PopulateToolStrip(ToolStrip toolstrip);
}

为方便起见包装聚合器,如下所示:

public static class Event
{
    private static readonly IEventAggregator aggregator = new EventAggregator();
    public static IEventAggregator Aggregator
    {
        get
        {
            return aggregator;   
        }
    }
}

您可以定义一个或多个事件类,例如:

public class EventToolStripClick {
    public object Sender {get;set;}
    public EventArgs Args {get;set;}   
}

在创建工具条的控制器中,在Click处理程序中发布自定义事件:

public void ControllerToolStripClick(object sender, EventArgs args )
{
    Event.Aggregator.Publish(new EventToolStripClick(){Sender=sender,Args=args)});
}

在mainForm中实现接口IHandle

public class MainForm : Form, IHandle<EventToolStripClick>
{
    ...
    public void Handle(EventToolStripClick evt)
    {
        //your implementation here
    }
}

答案 3 :(得分:0)

如果您在表单中托管子组件,他们可以填充主应用程序“shell”。

您可以拥有一个继承自System.Windows.Forms.ContainerControl的基类ShellComponent。这也将为您提供设计表面。而不是依赖IToolStripPopulator,你可以像这样使用ITool。

public inteface ITool<T>
{
   int ToolIndex { get; }
   string Category { get; }
   Action OnClick(T eventArgs);
}

ShellComponent中,您可以在每次加载视图时从MainForm调用public List<ITool> OnAddTools(ToolStrip toolStrip)。这样,组件将负责填充工具条。

然后,'ShellComponent'会向IoC容器询问实现ITool<T>的处理程序。这样,您的ITool<T>提供了一种分隔事件的方法(在MainFormContainerControl中)并将其推送到任何类。它还允许您准确定义要为参数传递的内容(与MouseClickEventArgs等相反)。