Цепь событий

Выполнение многоадресного делегата для создания цепи событий, которую может прервать любой обработчик в цепи.

•    Скачать исходники - 11.45 Кб

Введение

Недавно нам понадобился многоадресный делегат (то есть событие), прекращающий вызывать делегаты в списке вызова, когда один из делегатов обработал событие. То есть понадобилась цепь событий, останавливающаяся, как только делегат в цепи показал, что обработал событие. Поиск по данной теме не дал ничего, что удивляет по двум причинам. Первая –  кто-то мог бы реализовать это, и вторая –  мы думали, что она была реализована на C# 3.0. Возможно, мы искали недостаточно упорно. В любом случае, нам была нужна ее реализация для C# 2.0 и каркаса .NET 2.0, поскольку этого требовала наша база кода.

Код

В составе прошлой статьи о накопителе событий был написан код, взятый из наработок Джувела Лови и Эрика Ганнерсона в механизме запуска защитных событий, а Пит О'Хэнлон обновил этот код до .NET 2.0. Приведенный в статье код является модификацией безопасной функции, вызывающей событие.

Замечание: Вас удивит то, что мы убрали блок try-catch(попытка-перехват), поскольку считаем, что исключения должен ловить не этот код, а вызывающая функция, особенно в связи с тем, что в безопасной реализации функции, вызывающей событие, исключение повторно генерируется.

Интерфейс IEventChain

Важнейшей составляющей данной реализации является интерфейс IEventChain, который должен реализовывать параметр аргумента в сигнатуре события. Сигнатура события соблюдает общепринятую практику .NET Handler(object sender, EventArgs args), где свойство "args" унаследовано от EventArgs и реализует IEventChain.

/// <summary>
/// Интерфейс, который должен реализовывать параметр аргумента.
/// </summary>
public interface IEventChain
{
  /// <summary>
  /// Получатель события задает этому свойству истину, если он обрабатывает событие.
  /// </summary>
  bool Handled { get; set; }
}

Этот интерфейс имеет одно свойство, Handled, которому получатель события может задать true(истина), если он обрабатывает событие. Метод вызова события проверяет это свойство после вызова делегата, чтобы узнать, задал ли метод этому свойству true.

Определение EventChainHandlerDlgt

Приложение использует следующее обобщенное определение для задания многоадресного делегата.

/// <summary>
/// Обобщенный делегат для определения цепи событий.
/// </summary>
/// <typeparam name="T">Тип аргумента, должен реализовывать IEventChain.</typeparam>
/// <param name="sender">Экземпляр источника события.</param>
/// <param name="args">Параметр.</param>
public delegate void EventChainHandlerDlgt<T>
    (object sender, T arg) where T : IEventChain;

Обобщенный тип <T> задает тип аргумента. Очень эффективное средство, обеспечивающее проверку правильности типа во время компиляции - оператор where T : IEventChain, налагающий ограничение на обобщенный тип <T>, что он должен реализовывать IEventChain.

Пример класса аргумента

Ниже показана самая простая реализация класса аргумента, подходящая для вышеописанного делегата:

public class SomeCustomArgs : EventArgs, IEventChain
{
  protected bool handled;

  public bool Handled
  {
    get { return handled; }
    set { handled = value; }
  }
}

В код были добавлены еще одно свойство и поле для проверки того, что один и тот же экземпляр аргумента передается всем методам в списке вызова (как ожидается).

Определение сигнатуры события

Типичная сигнатура события выглядит так:

public static event EventChainHandlerDlgt<SomeCustomArgs> ChainedEvent;

(Событие является статическим, потому что в тестовом коде оно входит в состав метода Main, являющегося статическим.)

Сигнатура события также определяет сигнатуру обработчика метода. Итак, когда мы говорим:

ChainedEvent += new EventChainHandlerDlgt<SomeCustomArgs>(Handler1);

Ожидается, что сигнатура обработчика тоже совпадает с делегатом (пример):

public static void Handler1(object sender, SomeCustomArgs args)
{
  Console.WriteLine("Handler1");
}

Класс EventChain

Класс EventChain является статическим классом, реализующим один метод: Fire. Комментарии в коде объясняют работу этого метода, являющуюся весьма простой.

/// <summary>
/// Этот класс вызывает каждого получателя события в списке вызова события,
/// пока получатель события не задаст свойству Handled(обработано) истину.
/// </summary>
public static class EventChain
{
  /// <summary>
  /// Запускает каждое событие в списке вызова в порядке, в котором
  /// события были добавлены, пока обработчик события не задаст свойству обработано
  /// истину.
  /// Любое исключение, сгенерированное событием, должно быть перехвачено вызывающей ///функцией.
  /// </summary>
  /// <param name="del">Многоадресный делегат (событие).</param>
  /// <param name="sender">Экземпляр источника события.</param>
  /// <param name="arg">Аргумент события.</param>
  /// <returns>Возвращает истину, если получатель события обработал событие,
  /// иначе ложь.</returns>
  public static bool Fire(MulticastDelegate del, object sender, IEventChain arg)
  {
    bool handled = false;

    // При условии, что многоадресный делегат не нулевой...
    if (del != null)
      {
      // Получаем делегаты в списке вызова.
      Delegate[] delegates = del.GetInvocationList();

      // Вызываем методы, пока один из них не обработает событие
      // или все методы в списке делегата не обработаются.
      for (int i=0; i<delegates.Length && !handled; i++)
      {
        // Здесь возможна оптимизация
        // с помощью методов кеширования.
        delegates[i].DynamicInvoke(sender, arg);
        handled = arg.Handled;
      }
    }

    // Возвращаем флаг, показывающий, обработал ли событие
    // получатель события.
    return handled;
  }
}

Имейте в виду, что этот метод возвращает логическое значение, показывающее, обработал ли событие один из получателей события в цепи.

Пример

Снимок экрана выше является результатом этого примера кода (часть скачиваемых исходников):

public static void Main()
{
  ChainedEvent += new EventChainHandlerDlgt<SomeCustomArgs>(Handler1);
  ChainedEvent += new EventChainHandlerDlgt<SomeCustomArgs>(Handler2);
  ChainedEvent += new EventChainHandlerDlgt<SomeCustomArgs>(Handler3);

  SomeCustomArgs args=new SomeCustomArgs();
  bool ret = EventChain.Fire(ChainedEvent, null, args);
  Console.WriteLine(args.N);
  Console.WriteLine(ret);
}

public static void Handler1(object sender, SomeCustomArgs args)
{
  ++args.N;
  Console.WriteLine("Handler1");
}

public static void Handler2(object sender, SomeCustomArgs args)
{
  ++args.N;
  Console.WriteLine("Handler2");
  args.Handled = true;
}

public static void Handler3(object sender, SomeCustomArgs args)
{
  ++args.N;
  Console.WriteLine("Handler3");
}

Смысл этого кода в том, что Handler3 вообще не вызывается, потому что Handler2 показывает, что он обрабатывает событие.

Заметьте, что все эти методы статические лишь потому, что в коде все они входят в состав метода Main.

Вывод

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

Завершающие мысли

С приведенным кодом можно сделать ряд интересных вещей. Во-первых, можно оптимизировать вызов. Также можно изменить порядок, в котором вызывается список вызова. Например, можно изменить порядок на обратный: он может быть основан на каком-то приоритете, определяемом обработчиком, и т.д. Обработчики могут вызываться как рабочие потоки. Разнообразные интересные варианты доступны, когда есть доступ к списку вызова.