Schwache Ereignismuster (WPF .NET)

In Anwendungen ist es möglich, dass Handler, die an Ereignisquellen angefügt sind, nicht in Koordination mit dem Listenerobjekt zerstört werden, das den Handler an die Quelle angefügt hat. Diese Situation kann zu Speicherlecks führen. Windows Presentation Foundation (WPF) führt ein Entwurfsmuster ein, mit dem dieses Problem behoben werden kann. Das Entwurfsmuster stellt eine dedizierte Managerklasse für bestimmte Ereignisse zur Verfügung und implementiert eine Schnittstelle für Listener für dieses Ereignis. Dieses Entwurfsmuster wird als schwaches Ereignismuster bezeichnet.

Wichtig

Der Desktopleitfaden zu .NET 7 und .NET 6 ist in Bearbeitung.

Voraussetzungen

Im Artikel wird davon ausgegangen, dass Sie grundlegende Kenntnisse über Routingereignisse besitzen und die Übersicht über Routingereignisse gelesen haben. Um den Beispielen in diesem Artikel zu folgen, ist es hilfreich, wenn Sie mit Extensible Application Markup Language (XAML) vertraut sind und wissen, wie Windows Presentation Foundation-Anwendungen (WPF-Anwendungen) geschrieben werden.

Warum sollen wir das schwache Ereignismuster implementieren?

Das Überwachen von Ereignissen kann zu Arbeitsspeicherverlusten führen. Die übliche Technik zum Überwachen eines Ereignisses besteht darin, eine sprachspezifische Syntax zu verwenden, die einen Handler an ein Ereignis in einer Quelle anfügt. Beispielsweise die C#-Anweisung source.SomeEvent += new SomeEventHandler(MyEventHandler) oder die VB-Anweisung AddHandler source.SomeEvent, AddressOf MyEventHandler. Diese Technik erstellt jedoch einen starken Verweis von der Ereignisquelle auf den Ereignislistener. Sofern die Registrierung des Ereignishandlers nicht explizit aufgehoben ist, wird die Objektlebensdauer des Listeners durch die Objektlebensdauer der Quelle beeinflusst. Unter bestimmten Umständen kann die Objektlebensdauer des Listeners von anderen Faktoren gesteuert werden, z. B. ob sie aktuell zur visuellen Struktur der Anwendung gehört. Wenn die Objektlebensdauer der Quelle über die nützliche Objektlebensdauer des Listeners hinausgeht, wird der Listener länger als nötig beibehalten. In diesem Fall stellt der nicht zugeordnete Arbeitsspeicher einen Arbeitsspeicherverlust dar.

Das schwache Ereignismuster wurde entwickelt, um das Problem mit dem Arbeitsspeicherverlust zu lösen. Das schwache Ereignismuster kann verwendet werden, wenn sich ein Listener für ein Ereignis registrieren muss, aber der Listener nicht explizit weiß, wann die Registrierung aufgehoben werden soll. Das schwache Ereignismuster kann auch verwendet werden, wenn die Objektlebensdauer der Quelle die nützliche Objektlebensdauer des Listeners überschreitet. In diesem Fall wird nützlich von Ihnen bestimmt. Das schwache Ereignismuster ermöglicht es dem Listener, das Ereignis zu registrieren und zu empfangen, ohne dass sich die Objektlebensdauer des Listeners in beliebiger Form auswirkt. Tatsächlich bestimmt der implizite Verweis aus der Quelle nicht, ob der Listener für die Garbage Collection berechtigt ist. Der Verweis ist ein schwacher Verweis, also die Benennung des schwachen Ereignismusters und der zugehörigen APIs. Der Listener kann gesammelt oder anderweitig zerstört werden, und die Quelle kann fortgesetzt werden, ohne dass nicht erfasste Handlerverweise auf ein jetzt zerstörtes Objekt aufbewahrt werden.

Wer sollte das schwache Ereignismuster implementieren?

Das schwache Ereignismuster ist in erster Linie für Steuerelementersteller relevant. Als Steuerelementersteller sind Sie weitgehend verantwortlich für das Verhalten und die Eindämmung Ihres Steuerelements und die Auswirkungen auf Anwendungen, in denen sie eingefügt wird. Dies schließt das Verhalten der Objektlebensdauer des Steuerelements ein, insbesondere die Behandlung des beschriebenen Problems mit dem Arbeitsspeicherverlust.

Bestimmte Szenarien verleihen sich inhärent an die Anwendung des schwachen Ereignismusters. Ein solches Szenario ist die Datenbindung. Bei der Datenbindung ist es üblich, dass das Quellobjekt unabhängig vom Listenerobjekt ist, das Ziel einer Bindung ist. Viele Aspekte der WPF-Datenbindung haben bereits das schwache Ereignismuster angewendet, in das die Ereignisse implementiert werden.

Implementieren des schwachen Ereignismusters

Es gibt vier Möglichkeiten, das schwache Ereignismuster zu implementieren, und jeder Ansatz verwendet einen anderen Ereignismanager. Wählen Sie den Ereignismanager aus, der am besten zu Ihrem Szenario passt.

  • Vorhandener schwacher Ereignismanager:

    Verwenden Sie eine vorhandene schwache Ereignismanagerklasse, wenn das Ereignis, das Sie abonnieren möchten, eine entsprechende WeakEventManager-Instanz aufweist. Eine Liste der schwachen Ereignismanager, die in WPF enthalten sind, finden Sie in der Vererbungshierarchie in der WeakEventManager-Klasse. Da die eingeschlossenen schwachen Ereignismanager eingeschränkt sind, müssen Sie wahrscheinlich einen der anderen Ansätze auswählen.

  • Generischer schwacher Ereignismanager:

    Verwenden Sie eine generische WeakEventManager<TEventSource,TEventArgs>-Instanz, wenn eine vorhandene WeakEventManager-Instanz nicht verfügbar ist und Sie nach der einfachsten Möglichkeit suchen, schwache Ereignisse zu implementieren. Die generische WeakEventManager<TEventSource,TEventArgs>-Instanz ist jedoch weniger effizient als der vorhandene oder benutzerdefinierte schwache Ereignismanager, da sie die Reflexion verwendet, um das Ereignis anhand seines Namens zu ermitteln. Außerdem ist der Code zum Registrieren des Ereignisses mithilfe des generischen WeakEventManager<TEventSource,TEventArgs>-Ereignisses ausführlicher als die Verwendung einer vorhandenen oder benutzerdefinierten WeakEventManager.

  • Benutzerdefinierter schwacher Ereignismanager:

    Erstellen Sie einen benutzerdefinierten WeakEventManager, wenn ein vorhandener WeakEventManager nicht verfügbar ist und die Effizienz von entscheidender Bedeutung ist. Obwohl dies effizienter ist als ein generischer WeakEventManager, müssen Sie für einen benutzerdefinierten WeakEventManager im Vorfeld mehr Code schreiben.

  • Schwacher Ereignismanager von Drittanbietern:

    Verwenden Sie einen schwachen Ereignismanager eines Drittanbieters, wenn Sie Funktionen benötigen, die von den anderen Ansätzen nicht bereitgestellt werden. NuGet verfügt über einige schwache Ereignismanager. Viele WPF-Frameworks unterstützen dieses Muster ebenfalls.

In den folgenden Abschnitten wird beschrieben, wie Sie das schwache Ereignismuster mithilfe der verschiedenen Ereignismanagertypen implementieren. Für die Beispiele des generischen und benutzerdefinierten schwachen Ereignismanagers hat das zu abonnierende Ereignis die folgenden Merkmale.

  • Der Ereignisname ist SomeEvent.
  • Das Ereignis wird von der SomeEventSource-Klasse ausgelöst.
  • Der Ereignishandler hat den Typ EventHandler<SomeEventArgs>.
  • Das Ereignis übergibt einen Parameter vom Typ SomeEventArgs an die Ereignishandler.

Verwenden einer vorhandenen schwachen Ereignismanagerklasse

  1. Suchen Sie einen vorhandenen schwachen Ereignismanager. Eine Liste der schwachen Ereignismanager, die in WPF enthalten sind, finden Sie in der Vererbungshierarchie der WeakEventManager-Klasse.

  2. Verwenden Sie den neuen schwachen Ereignismanager anstelle des normalen Ereignis-Hookups.

    Wenn Ihr Code beispielsweise das folgende Muster verwendet, um ein Ereignis zu abonnieren:

    source.LostFocus += new RoutedEventHandler(Source_LostFocus);
    
    AddHandler source.LostFocus, New RoutedEventHandler(AddressOf Source_LostFocus)
    

    Ändern Sie es in das folgende Muster:

    LostFocusEventManager.AddHandler(source, Source_LostFocus);
    
    LostFocusEventManager.AddHandler(
        source, New EventHandler(Of RoutedEventArgs)(AddressOf Source_LostFocus))
    

    Wenn Ihr Code das folgende Muster verwendet, um sich von einem Ereignis abzumelden:

    source.LostFocus -= new RoutedEventHandler(Source_LostFocus);
    
    RemoveHandler source.LostFocus, New RoutedEventHandler(AddressOf Source_LostFocus)
    

    Ändern Sie es in das folgende Muster:

    LostFocusEventManager.RemoveHandler(source, Source_LostFocus);
    
    LostFocusEventManager.RemoveHandler(
        source, New EventHandler(Of RoutedEventArgs)(AddressOf Source_LostFocus))
    

Verwenden der generischen, schwachen Ereignismanagerklasse

Verwenden Sie die generische WeakEventManager<TEventSource,TEventArgs>-Klasse anstelle des normalen Ereignis-Hookups.

Wenn Sie WeakEventManager<TEventSource,TEventArgs> zum Registrieren von Ereignislistenern verwenden, geben Sie die Ereignisquelle und den EventArgs-Typ als Typparameter für die Klasse an. Rufen Sie AddHandler wie im folgenden Code gezeigt auf:

WeakEventManager<SomeEventSource, SomeEventArgs>.AddHandler(source, "SomeEvent", Source_SomeEvent);
WeakEventManager(Of SomeEventSource, SomeEventArgs).AddHandler(
    source, "SomeEvent", New EventHandler(Of SomeEventArgs)(AddressOf Source_SomeEvent))

Erstellen einer benutzerdefinierten schwachen Ereignismanagerklasse

  1. Kopieren Sie die folgende Klassenvorlage in Ihr Projekt. Die folgende Klasse erbt von der WeakEventManager-Klasse:

    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. Benennen Sie SomeEventWeakEventManager, SomeEvent, SomeEventSource und SomeEventArgs entsprechend dem Namen Ihres Ereignisses um.

  3. Legen Sie die Zugriffsmodifizierer für die schwache Ereignismanagerklasse fest, um sie an die Barrierefreiheit des Ereignisses anzupassen, das sie verwaltet.

  4. Verwenden Sie den neuen schwachen Ereignismanager anstelle des normalen Ereignis-Hookups.

    Wenn Ihr Code beispielsweise das folgende Muster verwendet, um ein Ereignis zu abonnieren:

    source.SomeEvent += new EventHandler<SomeEventArgs>(Source_SomeEvent);
    
    AddHandler source.SomeEvent, New EventHandler(Of SomeEventArgs)(AddressOf Source_SomeEvent)
    

    Ändern Sie es in das folgende Muster:

    SomeEventWeakEventManager.AddHandler(source, Source_SomeEvent);
    
    SomeEventWeakEventManager.AddHandler(
        source, New EventHandler(Of SomeEventArgs)(AddressOf Source_SomeEvent))
    

    Wenn Ihr Code das folgende Muster verwendet, um das Abonnement eines Ereignisses kündigen zu können:

    source.SomeEvent -= new EventHandler<SomeEventArgs>(Source_SomeEvent);
    
    RemoveHandler source.SomeEvent, New EventHandler(Of SomeEventArgs)(AddressOf Source_SomeEvent)
    

    Ändern Sie es in das folgende Muster:

    SomeEventWeakEventManager.RemoveHandler(source, Source_SomeEvent);
    
    SomeEventWeakEventManager.RemoveHandler(
        source, New EventHandler(Of SomeEventArgs)(AddressOf Source_SomeEvent))
    

Weitere Informationen