Шаблоны слабых событий

В приложениях возможно возникновение ситуаций, когда обработчики, присоединенные к источникам событий, не уничтожаются вместе с объектом прослушивателя, который присоединил обработчик к источнику. Они могут привести к утечкам памяти. В Windows Presentation Foundation (WPF) представлен шаблон проектирования, который можно использовать для решения данной проблемы, используя выделенный класс менеджера для определенных событий и реализовав интерфейс для прослушивателей для этого события. Этот шаблон проектирования называется шаблоном слабых событий.

Зачем реализовывать шаблон слабых событий?

класс Прослушивание событий может привести к утечкам памяти. класс Типичным способом прослушивания события является использование синтаксиса для конкретного языка, который присоединяет обработчик к событию в источнике. Например, в C# этот синтаксис таков: source.SomeEvent += new SomeEventHandler(MyEventHandler).

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

Шаблон слабых событий предназначен для решения этой проблемы утечки памяти. Шаблон слабых событий можно использовать всякий раз, когда прослушивателю требуется зарегистрировать событие, но прослушиватель явно не знает, когда отменять регистрацию. Шаблон слабых событий также можно использовать каждый раз, когда время существования источника превышает полезное время существования прослушивателя. (В этом случае полезно определить вас.) Слабый шаблон событий позволяет прослушивателю регистрироваться и получать событие, не влияя на характеристики времени существования объекта прослушивателя. В действительности подразумеваемая ссылка из источника не определяет, подходит ли прослушиватель для сборки мусора. Эта ссылка является слабой, отсюда и название для термина "Шаблон слабых событий" и связанных API. Прослушиватель отправится в корзину или уничтожится иным образом, а источник может продолжать свое существование, не сохраняя ненужные ссылки обработчика к уже уничтоженному объекту.

Кому следует реализовывать шаблон слабых событий?

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

Некоторые сценарии устроены таким образом, что возникновение шаблона слабых событий неизбежно. Один из таких сценариев — привязка данных. При привязке данных источник обычно полностью независим от объекта прослушивателя, который является целевым объектом привязки. При привязке данных для различных аспектов WPF в методах реализации событий уже предусмотрен шаблон слабых событий.

Способ реализация шаблона слабых событий

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

Подход Момент реализации
Использование существующего класса-менеджера слабых событий Если событие, которое требуется зарегистрировать, имеет соответствующий класс WeakEventManager, используйте существующий менеджер слабых событий. Список менеджеров слабых событий, включенных в WPF, см. в иерархии наследования в классе WeakEventManager. Так как количество предусмотренных менеджеров слабых событий ограничено, вам, вероятно, потребуется выбрать один из других подходов.
Использование универсального класса менеджера слабых событий Используйте универсальный класс WeakEventManager<TEventSource,TEventArgs>, если существующий WeakEventManager недоступен, требуется простой способ реализации, и вы не обеспокоены эффективностью. Универсальный класс WeakEventManager<TEventSource,TEventArgs> менее эффективен, чем существующий или пользовательский менеджер слабых событий. Например, в универсальном классе, чтобы обнаружить событие по имени, требуется больше вычислений. Кроме того, код для регистрации события с помощью универсального класса WeakEventManager<TEventSource,TEventArgs> является более подробным, чем при использовании существующего или настраиваемого класса WeakEventManager.
Создание пользовательского класса менеджера слабых событий Создайте пользовательский класс WeakEventManager, если существующий WeakEventManager недоступен, и вы хотите повысить эффективность. Использование пользовательского класса WeakEventManager для регистрации события будет более эффективным, но изначально вы понесете затраты на написание кода.
Использование стороннего менеджера слабых событий NuGet имеет несколько слабых диспетчеров событий, и многие платформы WPF также поддерживают шаблон.

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

  • Имя события — SomeEvent.

  • Событие вызывается классом EventSource.

  • Обработчик событий имеет тип SomeEventEventHandler (или EventHandler<SomeEventEventArgs>).

  • Событие передает параметр типа SomeEventEventArgs обработчикам событий.

Использование существующего класса менеджера слабых событий

  1. Найдите существующий менеджер слабых событий.

    Список менеджеров слабых событий, включенных в WPF, см. в иерархии наследования в классе WeakEventManager.

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

    Например, если код использует следующий шаблон для подписки на событие:

    source.SomeEvent += new SomeEventEventHandler(OnSomeEvent);
    

    Измените его на шаблон ниже:

    SomeEventWeakEventManager.AddHandler(source, OnSomeEvent);
    

    Аналогичным образом, если код использует следующий шаблон для отмены подписки на событие:

    source.SomeEvent -= new SomeEventEventHandler(OnSomeEvent);
    

    Измените его на шаблон ниже:

    SomeEventWeakEventManager.RemoveHandler(source, OnSomeEvent);
    

Использование универсального класса менеджера слабых событий

  1. Использование универсального класса WeakEventManager<TEventSource,TEventArgs> вместо обычного подключения события.

    При использовании класса WeakEventManager<TEventSource,TEventArgs> для регистрации прослушивателей событий разработчик передает ему источник событий и тип EventArgs в качестве параметров типа и вызывает метод AddHandler, как показано в коде ниже.

    WeakEventManager<EventSource, SomeEventEventArgs>.AddHandler(source, "SomeEvent", source_SomeEvent);
    

Создание пользовательского класса слабого менеджера событий

  1. Скопируйте следующий шаблон класса в проект.

    Он наследует от класса WeakEventManager.

    class SomeEventWeakEventManager : WeakEventManager
    {
    
        private SomeEventWeakEventManager()
        {
        }
    
        /// <summary>
        /// Add a handler for the given source's event.
        /// </summary>
        public static void AddHandler(EventSource source,
                                      EventHandler<SomeEventEventArgs> handler)
        {
            if (source == null)
                throw new ArgumentNullException("source");
            if (handler == null)
                throw new ArgumentNullException("handler");
    
            CurrentManager.ProtectedAddHandler(source, handler);
        }
    
        /// <summary>
        /// Remove a handler for the given source's event.
        /// </summary>
        public static void RemoveHandler(EventSource source,
                                         EventHandler<SomeEventEventArgs> handler)
        {
            if (source == null)
                throw new ArgumentNullException("source");
            if (handler == null)
                throw new ArgumentNullException("handler");
    
            CurrentManager.ProtectedRemoveHandler(source, handler);
        }
    
        /// <summary>
        /// Get the event manager for the current thread.
        /// </summary>
        private static SomeEventWeakEventManager CurrentManager
        {
            get
            {
                Type managerType = typeof(SomeEventWeakEventManager);
                SomeEventWeakEventManager manager =
                    (SomeEventWeakEventManager)GetCurrentManager(managerType);
    
                // at first use, create and register a new manager
                if (manager == null)
                {
                    manager = new SomeEventWeakEventManager();
                    SetCurrentManager(managerType, manager);
                }
    
                return manager;
            }
        }
    
        /// <summary>
        /// Return a new list to hold listeners to the event.
        /// </summary>
        protected override ListenerList NewListenerList()
        {
            return new ListenerList<SomeEventEventArgs>();
        }
    
        /// <summary>
        /// Listen to the given source for the event.
        /// </summary>
        protected override void StartListening(object source)
        {
            EventSource typedSource = (EventSource)source;
            typedSource.SomeEvent += new EventHandler<SomeEventEventArgs>(OnSomeEvent);
        }
    
        /// <summary>
        /// Stop listening to the given source for the event.
        /// </summary>
        protected override void StopListening(object source)
        {
            EventSource typedSource = (EventSource)source;
            typedSource.SomeEvent -= new EventHandler<SomeEventEventArgs>(OnSomeEvent);
        }
    
        /// <summary>
        /// Event handler for the SomeEvent event.
        /// </summary>
        void OnSomeEvent(object sender, SomeEventEventArgs e)
        {
            DeliverEvent(sender, e);
        }
    }
    
  2. Замените имя SomeEventWeakEventManager на собственное.

  3. Замените три имени, описанные ранее, соответствующими именами для события. (SomeEvent, EventSource, и SomeEventEventArgs)

  4. Задайте видимость (public/ internal / private) класса менеджера слабых событий так же, как и события, которым он управляет.

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

    Например, если код использует следующий шаблон для подписки на событие:

    source.SomeEvent += new SomeEventEventHandler(OnSomeEvent);
    

    Измените его на шаблон ниже:

    SomeEventWeakEventManager.AddHandler(source, OnSomeEvent);
    

    Аналогичным образом, если код использует следующий шаблон для отмены регистрации события:

    source.SomeEvent -= new SomeEventEventHandler(OnSome);
    

    Измените его на шаблон ниже:

    SomeEventWeakEventManager.RemoveHandler(source, OnSomeEvent);
    

См. также