弱いイベント パターン (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 の名前が付けられます。 ガベージ コレクションまたはその他の方法でリスナーを破棄することができます。また、破棄されたオブジェクトに対する収集不可能なハンドラー参照を保持することなく、ソースを継続できます。
弱いイベント パターンを実装する必要があるのは誰ですか?
弱いイベント パターンは、主にコントロールの作成者に関連します。 コントロールの作成者は、コントロールの動作とコンテインメイトに加え、コントロールが挿入されるアプリケーションに及ぼす影響に主に責任を負います。 これには、コントロール オブジェクトの有効期間の動作の中でも、特にここで説明するメモリ リークの問題の処理が含まれます。
弱いイベント パターンの適用に本質的に適しているシナリオがあります。 このようなシナリオの 1 つにデータ バインディングがあります。 データ バインディングでは、ソース オブジェクトがバインディングのターゲットであるリスナー オブジェクトから完全に独立しているのが普通です。 WPF データ バインディングの多くの側面には、イベントの実装方法に弱いイベント パターンが既に適用されています。
弱いイベント パターンを実装する方法
弱いイベント パターンを実装するには 4 つの方法があり、各アプローチは異なるイベント マネージャーを使用します。 シナリオに最適なイベント マネージャーを選択します。
-
サブスクライブするイベントに対応する 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))
関連項目
.NET Desktop feedback
フィードバック
https://aka.ms/ContentUserFeedback」を参照してください。
以下は間もなく提供いたします。2024 年を通じて、コンテンツのフィードバック メカニズムとして GitHub の issue を段階的に廃止し、新しいフィードバック システムに置き換えます。 詳細については、「フィードバックの送信と表示