简体   繁体   中英

IoC: Wiring up dependencies on event handlers

I am building an WinForms application with a UI that only consists of a NotifyIcon and its dynamically populated ContextMenuStrip . There is a MainForm to hold the application together, but that is never visible.

I set out to build this as SOLIDly as possible (using Autofac to handle the object graph) and am quite pleased with my success, mostly getting along pretty well even with the O part. With the extension I am currently implementing it seems I have discovered a flaw in my design and need to remodel a bit; I think know the way I need to go but am a bit unclear as to how to exactly define the dependencies.

As mentioned above, the menu is in part populated dynamically after starting the application. For this purpose, I defined an IToolStripPopulator interface:

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

An implementation of this is injected into the MainForm , and the Load() method calls PopulateToolStrip() with the ContextMenuStrip and a handler defined in the form. The populator's dependencies are only related to obtaining the data to use for the menu items.

This abstraction has worked nicely through a few evolutionary steps but isn't sufficient anymore when I need more than one event handler, eg because I am creating several different groups of menu items - still hidden behind a single IToolStripPopulator interface because the form shouldn't be concerned with that at all.

As I said, I think I know what the general structure should be like - I renamed the IToolStripPopulator interface to something more specific* and created a new one whose PopulateToolStrip() method does not take an EventHandler parameter, which is instead injected into the object (also allowing for much more flexibility regarding the number of handlers required by an implementation etc.). This way my "foremost" IToolStripPopulator can very easily be an adapter for any number of specific ones.

Now what I am unclear on is the way I should resolve the EventHandler dependencies. I think the handlers should all be defined in the MainForm , because that has all the other dependencies needed to properly react to the menu events, and it also "owns" the menu. That would mean my dependencies for IToolStripPopulator objects eventually injected into the MainForm would need to take dependencies on the MainForm object itself using Lazy<T> .

My first thought was defining an IClickHandlerSource interface:

public interface IClickHandlerSource
{
    EventHandler GetClickHandler();
}

This was implemented by my MainForm , and my specific IToolStripPopulator implementation took a dependency on Lazy<IClickHandlerSource> . While this works, it is inflexible. I would either have to define separate interfaces for a potentially growing number of handlers (severely violating OCP with the MainForm class) or continuously extend IClickHandlerSource (primarily violating ISP). Directly taking dependencies on the event handlers looks like a nice idea on the consumers' side, but individually wiring up the constructors via properties of lazy instance (or the like) seems pretty messy - if possible at all.

My best bet currently seems to be this:

public interface IEventHandlerSource
{
    EventHandler Get(EventHandlerType type);
}

The interface would still be implemented by MainForm and injected as a lazy singleton, and EventHandlerType would be a custom enum with the different types I need. This would still not be very OCP compliant, but reasonably flexible. EventHandlerType would obviously have a change for each new type of event handler, as would the resolution logic in MainForm , in addition to the new event handler itself and the (probably) newly written additional implementation of IToolStripPopulator .

Or.... a separate implementation of IEventHandlerSource that (as the only object) takes a dependency on Lazy<MainForm> and resolves the EventHandlerType options to the specific handlers defined in MainForm ?

I'm trying to think of a way of actually getting the event handlers out of MainForm in a feasible way, but can't quite seem to right now.

What is my best option here, providing the loosest coupling and most elegant resolution of the different event handlers?

[*Yes, I probably should have left the name alone to really comply with OCP, but it looked better that way.]

What is my best option here, providing the loosest coupling and most elegant resolution of the different event handlers?

Common solution are not exist and it depends on the global application architecture.

If you want a loosest coupling, EventAggregator pattern can help you in such case (your IEventHandlerSource similar to that):

But, global events should be used with great caution - they can smudge architecture, because subscribe to the event will be possible anywhere.

Important thing in DI and IoC: lower dependency should not to know about higher dependency. I think, and as Leon said earlier, will be better to create some interface like ITool<T> and store list of tools in the MainForm . After some action MainForm will invoke certain methods in this tools.

Firstly, I think you shouldn't claim your mainform must contain all event handlers. You clearly ran into the fact that there are different handlers with different needs (and probably different dependencies), so why should they all be fitted into the same class? You can probably take some inspiration from the way events are handled in other languages. WPF uses Commands, Java has Listeners. Both are objects, not just a delegate, making them easier to deal with in an IOC scenario. It's fairly easy to simulate something like that. You could abuse the tag on your toolbar items, like this: Binding to commands in WinForms or use lambda expressions inside PopulateToolbar (see Is there anything wrong with using lambda for winforms event? ) to associate the toolbar item with the correct command. That's assuming that since PopulateToolbar knows which items need to be created it also know which action/command belongs to each item.

The object representing the action can have their own dependencies injected, independently of the main form or other actions. Toolbar items with their own actions can then be added or removed later without affecting your main form or any of the other actions, each action can independently be tested and refactored.

Bottom line, stop thinking about EventHandlers, start thinking about Actions/Commands as an entity in their own right and it will become easier to come up with a suitable pattern. Make sure you understand the Command Pattern , because that's pretty much what you need here.

Have you tried to use an event aggregator? See: Caliburn framework, event Aggregator An event aggregator will decouple the toolstrip from you main form.

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

Wrap the aggregator for convenience like this:

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

You define one or more event classes, for example:

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

In the controller that creates the toolstrip, publish the custom event in the Click handler write:

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

In the mainForm implement the interface IHandle

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

If you are hosting child components within your form, and they can populate the main application "shell".

You could have a base class ShellComponent , that inherits from System.Windows.Forms.ContainerControl. This will give you a design surface as well. Instead of relying IToolStripPopulator, you could have ITool like such.

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

In ShellComponent you could call, public List<ITool> OnAddTools(ToolStrip toolStrip) from the MainForm each time a view is loaded. This way the component would be responsible for populating the toolstrip.

The 'ShellComponent' would then ask the IoC container for handlers that implement ITool<T> . This way your ITool<T> provides a way to separate the event (in the MainForm , or the ContainerControl ) and push this out to any class. It also allows you to define exactly what you want to pass through for arguments (as opposed to MouseClickEventArgs ect).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM