Подтвердить что ты не робот

IoC: подключение зависимостей от обработчиков событий

Я создаю приложение WinForms с пользовательским интерфейсом, который состоит только из NotifyIcon и его динамически заполненного ContextMenuStrip. Существует MainForm, чтобы удерживать приложение вместе, но это никогда не отображается.

Я решил построить это как SOLIDly, насколько это возможно (используя Autofac для обработки графа объектов), и я очень доволен своим успехом, в основном хорошо справляясь даже с O-частью. С расширением, которое я в настоящее время реализую, кажется, я обнаружил недостаток в моем дизайне и немного нуждаюсь в переделке; Я думаю, знаю, как мне нужно идти, но немного неясно, как точно определить зависимости.

Как уже упоминалось выше, меню запускается динамически динамически после запуска приложения. Для этой цели я определил интерфейс IToolStripPopulator:

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

Реализация этого вводится в MainForm, а метод Load() вызывает PopulateToolStrip() с помощью ContextMenuStrip и обработчика, определенного в форме. Зависимости между populator связаны только с получением данных, которые будут использоваться для элементов меню.

Эта абстракция хорошо работала через несколько этапов эволюции, но этого недостаточно, когда мне нужно больше одного обработчика события, например. потому что я создаю несколько разных групп элементов меню, которые все еще скрыты за единственным интерфейсом IToolStripPopulator, потому что форма не должна касаться этого вообще.

Как я уже сказал, я думаю, что знаю, какова должна быть общая структура - я переименовал интерфейс IToolStripPopulator в нечто более конкретное * и создал новый, метод PopulateToolStrip() не принимает параметр EventHandler который вместо этого вводится в объект (что также обеспечивает гораздо большую гибкость в отношении количества обработчиков, необходимых для реализации и т.д.). Таким образом, мой "передовой" IToolStripPopulator может быть легко адаптером для любого числа конкретных.

Теперь я не понимаю, как мне разрешить зависимости EventHandler. Я думаю, что обработчики должны быть определены в MainForm, потому что у этого есть все другие зависимости, необходимые для правильного реагирования на события меню, а также "владеет" меню. Это означало бы, что мои зависимости для объектов IToolStripPopulator, которые в конечном итоге были введены в MainForm, должны были бы зависеть от самого объекта MainForm, используя Lazy<T>.

Моя первая мысль заключалась в определении интерфейса IClickHandlerSource:

public interface IClickHandlerSource
{
    EventHandler GetClickHandler();
}

Это было реализовано моим MainForm, и моя конкретная реализация IToolStripPopulator зависела от Lazy<IClickHandlerSource>. Хотя это работает, оно негибкое. Я либо должен был бы определить отдельные интерфейсы для потенциально растущего числа обработчиков (строго нарушая OCP с классом MainForm), либо постоянно расширяя IClickHandlerSource (в первую очередь, нарушая ISP). Непосредственное использование зависимостей от обработчиков событий выглядит как хорошая идея со стороны потребителей, но индивидуальная проводка конструкторов через свойства ленивого экземпляра (или тому подобного) кажется довольно грязной - если возможно вообще.

Моя лучшая ставка в настоящее время выглядит так:

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

Интерфейс по-прежнему будет реализован MainForm и вводится как ленивый синглтон, а EventHandlerType будет настраиваемым перечислением с различными типами, которые мне нужны. Это все равно не будет очень совместимым с OCP, но достаточно гибким. EventHandlerType, очевидно, будет иметь изменение для каждого нового типа обработчика событий, равно как и логику разрешения в MainForm, в дополнение к самому новому обработчику событий и (возможно) вновь написанную дополнительную реализацию IToolStripPopulator.

Или.... отдельная реализация IEventHandlerSource, которая (как единственный объект) принимает зависимость от Lazy<MainForm> и разрешает опции EventHandlerType для определенных обработчиков, определенных в MainForm?

Я пытаюсь думать о способе фактического получения обработчиков событий из MainForm допустимым способом, но сейчас это не совсем похоже.

Каков мой лучший вариант здесь, обеспечивающий самое слабое соединение и наиболее элегантное разрешение различных обработчиков событий?

[* Да, я, вероятно, должен был оставить только имя, чтобы действительно соответствовать OCP, но это выглядело лучше.]

4b9b3361

Ответ 1

Каков мой лучший вариант здесь, обеспечивающий самое слабое соединение и большинство элегантное разрешение разных обработчиков событий?

Общее решение не существует и зависит от архитектуры глобального приложения.

Если вы хотите использовать слабое, шаблон EventAggregator может помочь вам в таком случае (ваш IEventHandlerSource, подобный этому):

Но глобальные события следует использовать с большой осторожностью - они могут смазывать архитектуру, потому что подписка на это событие будет возможно где угодно.

Важное значение в DI и IoC: более низкая зависимость не должна знать о более высокой зависимости. Я думаю, и, как сказал ранее Леон, лучше создать некоторый интерфейс, например ITool<T>, и сохранить список инструментов в MainForm. После некоторого действия MainForm вызовет определенные методы в этих инструментах.

Ответ 2

Во-первых, я думаю, вы не должны требовать, чтобы ваша основная форма должна содержать все обработчики событий. Вы явно столкнулись с тем, что существуют разные обработчики с разными потребностями (и, возможно, с разными зависимостями), так почему все они должны быть в одном классе? Возможно, вы, возможно, немного вдохновляетесь тем, как события обрабатываются на других языках. WPF использует Commands, Java имеет Listeners. Оба являются объектами, а не только делегатом, что облегчает их работу в сценарии МОК. Это довольно легко имитировать что-то подобное. Вы можете злоупотреблять тегом на элементах панели инструментов, например: привязка к командам в WinForms или использовать выражения лямбда внутри PopulationToolbar (см. Что-то не так с использованием lambda для события winforms?), чтобы связать элемент панели инструментов с правильной командой. Это предполагает, что, поскольку PopulateToolbar знает, какие элементы необходимо создать, он также знает, какое действие/команда принадлежит каждому элементу.

Объект, представляющий действие, может вводить свои собственные зависимости независимо от основной формы или других действий. Элементы панели инструментов со своими собственными действиями затем могут быть добавлены или удалены позже, не затрагивая вашу основную форму или любые другие действия, каждое действие может быть независимо протестировано и реорганизовано.

В нижней строке, перестаньте думать о EventHandlers, начните думать о действиях/командах как сущность в своем собственном праве, и вам станет легче подобрать подходящую модель. Убедитесь, что вы понимаете Command Pattern, потому что это в значительной степени то, что вам нужно здесь.

Ответ 3

Вы пытались использовать агрегатор событий? Смотрите: структура Caliburn, агрегатор событий Агрегатор событий отделяет панель инструментов от вашей основной формы.

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
    }
}

Ответ 4

Если вы размещаете дочерние компоненты в своей форме, и они могут заполнять основное приложение "оболочка".

У вас может быть базовый класс ShellComponent, который наследуется от System.Windows.Forms.ContainerControl. Это также даст вам поверхность для дизайна. Вместо того, чтобы полагаться на IToolStripPopulator, у вас мог бы такой ITool.

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

В ShellComponent вы можете вызывать public List<ITool> OnAddTools(ToolStrip toolStrip) из MainForm каждый раз при загрузке представления. Таким образом, компонент будет отвечать за заполнение инструментальной панели.

Затем "ShellComponent" запросит контейнер IoC для обработчиков, которые реализуют ITool<T>. Таким образом, ваш ITool<T> предоставляет способ разделить событие (в MainForm или ContainerControl) и вывести его в любой класс. Он также позволяет вам точно определить, что вы хотите передать для аргументов (в отличие от ect MouseClickEventArgs).