弱事件模式 (WPF .NET)

在应用程序中,附加到事件源的处理程序可能不会与将处理程序附加到源的侦听器对象一同销毁。 这种情况下会导致内存泄漏。 Windows Presentation Foundation (WPF) 引入了可用于解决此问题的设计模式。 设计模式为特定事件提供专用的管理器类,并在该事件的侦听器上实现接口。 此设计模式称为弱事件模式

重要

面向 .NET 7 和 .NET 6 的桌面指南文档正在撰写中。

先决条件

本文假定你对路由事件有基本的了解,并且已阅读路由事件概述。 若要遵循本文中的示例,如果熟悉 Extensible Application Markup Language (XAML) 并知道如何编写 Windows Presentation Foundation (WPF) 应用程序,将会很有帮助。

为什么要实现弱事件模式?

对事件的侦听可能会导致内存泄漏。 侦听事件的常用技术是使用特定于语言的语法,将处理程序附加到源上的事件。 例如,C# 语句 source.SomeEvent += new SomeEventHandler(MyEventHandler) 或 VB 语句 AddHandler source.SomeEvent, AddressOf MyEventHandler。 然而,此技术可创建从事件源到事件侦听器的强引用。 除非显式注销事件处理程序,否则侦听器的对象生存期将受到源的对象生存期的影响。 在某些情况下,你可能希望通过其他因素(例如,当前是否属于应用程序的可视化树)控制侦听器的对象生存期。 每当源的对象生存期超出侦听器的有用对象生存期时,侦听器的存活时间比必要时间要长。 在这种情况下,未分配的内存相当于内存泄漏。

弱事件模式旨在解决内存泄漏问题。 当侦听器需要注册事件时,都可以使用弱事件模式,但侦听器并不明确知晓事件会在何时注销。 当源的对象生存期超过侦听器的有用对象生存期时,也可以使用弱事件模式。 在这种情况下,有用与否将由你来决定。 弱事件模式允许侦听器注册事件和接收事件,而不会以任何方式影响侦听器的对象生存期特征。 实际上,对源的隐式引用并不能确定侦听器是否有资格执行垃圾回收。 由于是弱引用,因而引用是对弱事件模式和相关 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. 重命名 SomeEventWeakEventManagerSomeEventSomeEventSourceSomeEventArgs 以匹配事件名称。

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

另请参阅