Слабые шаблоны событий (WPF .NET)

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

Важно!

Документация по рабочему столу для .NET 7 и .NET 6 находится в стадии разработки.

Необходимые компоненты

Для понимания статьи нужно иметь общее представление о перенаправленных событиях и прочитать статью Общие сведения о перенаправленных событиях. Чтобы понимать примеры в этой статье полезно познакомиться с языком XAML и знать, как создавать приложения Windows Presentation Foundation (WPF).

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

класс Прослушивание событий может привести к утечкам памяти. Обычный способ прослушивания события — использовать синтаксис для конкретного языка для присоединения обработчика к событию в источнике. Например, оператор source.SomeEvent += new SomeEventHandler(MyEventHandler) C# или оператор AddHandler source.SomeEvent, AddressOf MyEventHandlerVB. Однако этот метод создает сильную ссылку из источника событий на прослушиватель событий. Если обработчик событий явно не зарегистрирован, время существования объекта прослушивателя будет зависеть от времени существования объекта источника. В определенных обстоятельствах может потребоваться, чтобы время существования объекта прослушивателя контролировалось другими факторами, например, принадлежит ли оно в настоящее время визуальному дереву приложения. Всякий раз, когда время существования объекта источника выходит за рамки времени существования полезного объекта прослушивателя, прослушиватель сохраняется дольше, чем необходимо. В этом случае нераспределенный объем памяти составляет утечку памяти.

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

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

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

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

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

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

  • Существующий слабый диспетчер событий:

    Используйте существующий слабый класс диспетчера событий, если событие, которое вы хотите подписаться на соответствующее WeakEventManager. Список менеджеров слабых событий, включенных в WPF, см. в иерархии наследования в классе WeakEventManager. Так как включенные слабые диспетчеры событий ограничены, вам, вероятно, потребуется выбрать один из других подходов.

  • Универсальный диспетчер слабых событий:

    Используйте универсальный WeakEventManager<TEventSource,TEventArgs> тип, если существующий WeakEventManager недоступен, и вы ищете самый простой способ реализации слабых событий. Однако универсальный WeakEventManager<TEventSource,TEventArgs> шаблон менее эффективен, чем существующий или пользовательский слабый диспетчер событий, так как он использует отражение для обнаружения события из его имени. Кроме того, код, необходимый для регистрации события с помощью универсального, WeakEventManager<TEventSource,TEventArgs> является более подробным, чем использование существующего или пользовательского WeakEventManager.

  • Пользовательский диспетчер слабых событий:

    Создайте пользователь, WeakEventManager если существующий WeakEventManager недоступен, и эффективность имеет решающее значение. Хотя и более эффективным, чем универсальный WeakEventManager, пользователь WeakEventManager требует, чтобы написать дополнительный предварительный код.

  • Сторонний слабый диспетчер событий:

    Используйте сторонний слабый диспетчер событий, если вам нужны функциональные возможности, которые не предоставляются другими подходами. NuGet имеет некоторые слабые диспетчеры событий. Многие платформы WPF также поддерживают шаблон.

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

  • Имя события — SomeEvent.
  • Событие вызывается классом SomeEventSource.
  • Обработчик событий имеет тип EventHandler<SomeEventArgs>.
  • Событие передает параметр типа SomeEventArgs обработчикам событий.

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

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

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

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

    source.LostFocus += new RoutedEventHandler(Source_LostFocus);
    
    AddHandler source.LostFocus, New RoutedEventHandler(AddressOf Source_LostFocus)
    

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

    LostFocusEventManager.AddHandler(source, Source_LostFocus);
    
    LostFocusEventManager.AddHandler(
        source, New EventHandler(Of RoutedEventArgs)(AddressOf Source_LostFocus))
    

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

    source.LostFocus -= new RoutedEventHandler(Source_LostFocus);
    
    RemoveHandler source.LostFocus, New RoutedEventHandler(AddressOf Source_LostFocus)
    

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

    LostFocusEventManager.RemoveHandler(source, Source_LostFocus);
    
    LostFocusEventManager.RemoveHandler(
        source, New EventHandler(Of RoutedEventArgs)(AddressOf Source_LostFocus))
    

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

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

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

WeakEventManager<SomeEventSource, SomeEventArgs>.AddHandler(source, "SomeEvent", Source_SomeEvent);
WeakEventManager(Of SomeEventSource, SomeEventArgs).AddHandler(
    source, "SomeEvent", New EventHandler(Of SomeEventArgs)(AddressOf Source_SomeEvent))

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

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

    class SomeEventWeakEventManager : WeakEventManager
    {
        private SomeEventWeakEventManager()
        {
        }
    
        /// <summary>
        /// Add a handler for the given source's event.
        /// </summary>
        public static void AddHandler(SomeEventSource source,
                                      EventHandler<SomeEventArgs> handler)
        {
            if (source == null)
                throw new ArgumentNullException(nameof(source));
            if (handler == null)
                throw new ArgumentNullException(nameof(handler));
    
            CurrentManager.ProtectedAddHandler(source, handler);
        }
    
        /// <summary>
        /// Remove a handler for the given source's event.
        /// </summary>
        public static void RemoveHandler(SomeEventSource source,
                                         EventHandler<SomeEventArgs> handler)
        {
            if (source == null)
                throw new ArgumentNullException(nameof(source));
            if (handler == null)
                throw new ArgumentNullException(nameof(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<SomeEventArgs>();
        }
    
        /// <summary>
        /// Listen to the given source for the event.
        /// </summary>
        protected override void StartListening(object source)
        {
            SomeEventSource typedSource = (SomeEventSource)source;
            typedSource.SomeEvent += new EventHandler<SomeEventArgs>(OnSomeEvent);
        }
    
        /// <summary>
        /// Stop listening to the given source for the event.
        /// </summary>
        protected override void StopListening(object source)
        {
            SomeEventSource typedSource = (SomeEventSource)source;
            typedSource.SomeEvent -= new EventHandler<SomeEventArgs>(OnSomeEvent);
        }
    
        /// <summary>
        /// Event handler for the SomeEvent event.
        /// </summary>
        void OnSomeEvent(object sender, SomeEventArgs e)
        {
            DeliverEvent(sender, e);
        }
    }
    
    Class SomeEventWeakEventManager
        Inherits WeakEventManager
    
        Private Sub New()
        End Sub
    
        ''' <summary>
        ''' Add a handler for the given source's event.
        ''' </summary>
        Public Shared Sub [AddHandler](source As SomeEventSource,
                                       handler As EventHandler(Of SomeEventArgs))
            If source Is Nothing Then Throw New ArgumentNullException(NameOf(source))
            If handler Is Nothing Then Throw New ArgumentNullException(NameOf(handler))
            CurrentManager.ProtectedAddHandler(source, handler)
        End Sub
    
        ''' <summary>
        ''' Remove a handler for the given source's event.
        ''' </summary>
        Public Shared Sub [RemoveHandler](source As SomeEventSource,
                                          handler As EventHandler(Of SomeEventArgs))
            If source Is Nothing Then Throw New ArgumentNullException(NameOf(source))
            If handler Is Nothing Then Throw New ArgumentNullException(NameOf(handler))
            CurrentManager.ProtectedRemoveHandler(source, handler)
        End Sub
    
        ''' <summary>
        ''' Get the event manager for the current thread.
        ''' </summary>
        Private Shared ReadOnly Property CurrentManager As SomeEventWeakEventManager
            Get
                Dim managerType As Type = GetType(SomeEventWeakEventManager)
                Dim manager As SomeEventWeakEventManager =
                    CType(GetCurrentManager(managerType), SomeEventWeakEventManager)
    
                If manager Is Nothing Then
                    manager = New SomeEventWeakEventManager()
                    SetCurrentManager(managerType, manager)
                End If
    
                Return manager
            End Get
        End Property
    
        ''' <summary>
        ''' Return a new list to hold listeners to the event.
        ''' </summary>
        Protected Overrides Function NewListenerList() As ListenerList
            Return New ListenerList(Of SomeEventArgs)()
        End Function
    
        ''' <summary>
        ''' Listen to the given source for the event.
        ''' </summary>
        Protected Overrides Sub StartListening(source As Object)
            Dim typedSource As SomeEventSource = CType(source, SomeEventSource)
            AddHandler typedSource.SomeEvent, New EventHandler(Of SomeEventArgs)(AddressOf OnSomeEvent)
        End Sub
    
        ''' <summary>
        ''' Stop listening to the given source for the event.
        ''' </summary>
        Protected Overrides Sub StopListening(source As Object)
            Dim typedSource As SomeEventSource = CType(source, SomeEventSource)
            AddHandler typedSource.SomeEvent, New EventHandler(Of SomeEventArgs)(AddressOf OnSomeEvent)
        End Sub
    
        ''' <summary>
        ''' Event handler for the SomeEvent event.
        ''' </summary>
        Private Sub OnSomeEvent(sender As Object, e As SomeEventArgs)
            DeliverEvent(sender, e)
        End Sub
    End Class
    
  2. Переименуйте SomeEventWeakEventManager, SomeEventSomeEventSourceи SomeEventArgs в соответствии с именем события.

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

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

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

    source.SomeEvent += new EventHandler<SomeEventArgs>(Source_SomeEvent);
    
    AddHandler source.SomeEvent, New EventHandler(Of SomeEventArgs)(AddressOf Source_SomeEvent)
    

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

    SomeEventWeakEventManager.AddHandler(source, Source_SomeEvent);
    
    SomeEventWeakEventManager.AddHandler(
        source, New EventHandler(Of SomeEventArgs)(AddressOf Source_SomeEvent))
    

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

    source.SomeEvent -= new EventHandler<SomeEventArgs>(Source_SomeEvent);
    
    RemoveHandler source.SomeEvent, New EventHandler(Of SomeEventArgs)(AddressOf Source_SomeEvent)
    

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

    SomeEventWeakEventManager.RemoveHandler(source, Source_SomeEvent);
    
    SomeEventWeakEventManager.RemoveHandler(
        source, New EventHandler(Of SomeEventArgs)(AddressOf Source_SomeEvent))
    

См. также