弱いイベント パターン (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 のパラメーターが渡されます。

既存の弱いイベント マネージャー クラスを使用する

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

関連項目