弱いイベント パターン (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 の名前が付けられます。 ガベージ コレクションまたはその他の方法でリスナーを破棄することができます。また、破棄されたオブジェクトに対する収集不可能なハンドラー参照を保持することなく、ソースを継続できます。

弱いイベント パターンを実装する必要があるのは誰ですか?

弱いイベント パターンは、主にコントロールの作成者に関連します。 コントロールの作成者は、コントロールの動作とコンテインメイトに加え、コントロールが挿入されるアプリケーションに及ぼす影響に主に責任を負います。 これには、コントロール オブジェクトの有効期間の動作の中でも、特にここで説明するメモリ リークの問題の処理が含まれます。

弱いイベント パターンの適用に本質的に適しているシナリオがあります。 このようなシナリオの 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))
    

関連項目