Слабые шаблоны событий (WPF .NET)
В приложениях возможно, что обработчики, подключенные к источникам событий, не будут удалены одновременно с объектом слушателя, который прикрепил эти обработчики к источнику. Эта ситуация может привести к утечкам памяти. Windows Presentation Foundation (WPF) представляет шаблон проектирования, который можно использовать для решения этой проблемы. Шаблон конструктора предоставляет выделенный класс диспетчера для определенных событий и реализует интерфейс для прослушивателей для этого события. Этот шаблон проектирования называется шаблоном слабых событий.
Необходимые условия
В статье предполагается, что у вас есть базовые знания о событиях маршрутизации и что вы прочитали обзор событий маршрутизации. Чтобы следовать примерам в этой статье, это поможет вам, если вы знакомы с языком разметки расширяемых приложений (XAML) и узнаете, как писать приложения Windows Presentation Foundation (WPF).
Зачем реализовать шаблон слабых событий?
Прослушивание событий может привести к утечкам памяти. Обычный способ прослушивания события — использовать синтаксис языка программирования для присоединения обработчика к событию источника. Например, оператор C# source.SomeEvent += new SomeEventHandler(MyEventHandler)
или оператор VB AddHandler source.SomeEvent, AddressOf MyEventHandler
. Однако этот метод создаёт сильную ссылку из источника событий на слушатель событий. Если обработчик событий явно не зарегистрирован, время существования объекта прослушивателя будет зависеть от времени существования объекта источника. В определенных обстоятельствах может потребоваться, чтобы время существования объекта прослушивателя контролировалось другими факторами, например, принадлежит ли оно в настоящее время визуальному дереву приложения. Всякий раз, когда время существования объекта источника выходит за рамки времени существования полезного объекта прослушивателя, прослушиватель сохраняется дольше, чем необходимо. В этом случае нераспределенный объем памяти составляет утечку памяти.
Шаблон слабых событий предназначен для решения проблемы утечки памяти. Шаблон слабых событий можно использовать, если прослушивателю нужно зарегистрироваться для участия в событии, но он не знает точно, когда отменять регистрацию. Шаблон слабых событий также можно использовать, если время существования объекта источника превышает время существования полезного объекта прослушивателя. В этом случае полезность для
Кто должен реализовать слабый шаблон событий?
Слабый шаблон событий в первую очередь относится к авторам элементов управления. Как автор элемента управления, вы в значительной степени отвечаете за поведение и сдерживание элемента управления и влияние на приложения, в которых он вставлен. Это включает в себя поведение времени существования объекта элемента управления, в частности обработку описанной проблемы утечки памяти.
Некоторые сценарии, по сути, позволяют применять слабый шаблон событий. Один из таких сценариев — привязка данных. В привязке данных обычно исходный объект не зависит от объекта прослушивателя, который является целью привязки. Многие аспекты привязки данных 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
обработчикам событий.
Использование существующего слабого класса диспетчера событий
Найдите существующий слабый диспетчер событий. Список слабых диспетчеров событий, включенных в WPF, см. в иерархии наследования класса WeakEventManager.
Используйте новый слабый диспетчер событий вместо обычного подключения событий.
Например, если код использует следующий шаблон для подписки на событие:
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))
Создание пользовательского класса слабого диспетчера событий
Скопируйте следующий шаблон класса в проект. Следующий класс наследует от класса 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
Переименуйте
SomeEventWeakEventManager
,SomeEvent
,SomeEventSource
иSomeEventArgs
в соответствии с именем события.Задайте модификаторы доступа для слабого класса диспетчера событий, чтобы он соответствовал специальным возможностям управляемого события.
Используйте новый слабый диспетчер событий вместо обычной привязки событий.
Например, если код использует следующий шаблон для подписки на событие:
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))
См. также
- WeakEventManager
- IWeakEventListener
- Обзор маршрутизируемых событий
- Обзор привязки данных
.NET Desktop feedback