弱式事件模式 (WPF .NET)
在應用程式中,附加至事件來源的處理常式可能不會與將處理常式附加至來源的接聽程式物件協調終結。 這種情況可能會導致記憶體流失。 Windows Presentation Foundation (WPF) 引進了可用來解決此問題的設計模式。 設計模式提供特定事件的專用管理員類別,並在該事件的接聽程式上實作介面。 此設計模式稱為弱式事件模式。
必要條件
本文假設您已基本了解路由事件,而且您已閱讀路由事件概觀。 若要遵循本文中的範例,建議您先熟悉 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 無法使用而您正在尋找最簡單的方式來實作弱式事件時,請使用泛型 WeakEventManager<TEventSource,TEventArgs>。 不過,泛型
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))