次の方法で共有


弱いイベント パターン (WPF .NET)

アプリケーションでは、イベント ソースにアタッチされたハンドラーが、そのハンドラーをソースにアタッチしたリスナー オブジェクトと連携して破棄されない可能性があります。 この状況により、メモリ リークが発生する可能性があります。 Windows Presentation Foundation (WPF) では、この問題に対処するために使用できるデザイン パターンが導入されています。 デザイン パターンは、特定のイベントに専用のマネージャー クラスを提供し、そのイベントのリスナーにインターフェイスを実装します。 この設計パターンは、弱いイベント パターンと呼ばれます。

前提 条件

この記事では、ルーティング イベントに関する基本的な知識と、ルーティング イベントの概要読んだことを前提としています。 この記事の例に従うと、拡張アプリケーション マークアップ言語 (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よりも効率的ですが、カスタム 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))
    

参照