Modelli di eventi deboli (WPF .NET)

Nelle applicazioni è possibile che i gestori collegati alle origini eventi non vengano eliminati definitivamente in coordinamento con l'oggetto listener che ha collegato il gestore all'origine. Questa situazione può causare perdite di memoria. Windows Presentation Foundation (WPF) introduce un modello di progettazione che può essere usato per risolvere questo problema. Il modello di progettazione fornisce una classe di gestione dedicata per eventi specifici e implementa un'interfaccia nei listener per tale evento. Questo modello di progettazione è noto come modello di evento debole.

Importante

La documentazione di Desktop Guide per .NET 7 e .NET 6 è in fase di costruzione.

Prerequisiti

L'articolo presuppone una conoscenza di base degli eventi indirizzati e di aver letto panoramica degli eventi indirizzati. Per seguire gli esempi in questo articolo, è utile se si ha familiarità con Extensible Application Markup Language (XAML) e si sa come scrivere applicazioni Windows Presentation Foundation (WPF).

Perché implementare il modello di evento debole?

L'ascolto degli eventi può causare perdite di memoria. La tecnica consueta per l'ascolto di un evento consiste nell'usare la sintassi specifica del linguaggio per associare un gestore a un evento in un'origine. Ad esempio, l'istruzione source.SomeEvent += new SomeEventHandler(MyEventHandler) C# o l'istruzione AddHandler source.SomeEvent, AddressOf MyEventHandlerVB . Tuttavia, questa tecnica crea un riferimento sicuro dall'origine evento al listener di eventi. A meno che il gestore eventi non venga registrato in modo esplicito, la durata dell'oggetto del listener sarà influenzata dalla durata dell'oggetto dell'origine. In determinate circostanze, è possibile che la durata dell'oggetto del listener sia controllata da altri fattori, ad esempio se appartiene attualmente alla struttura ad albero visuale dell'applicazione. Ogni volta che la durata dell'oggetto dell'origine si estende oltre la durata utile dell'oggetto del listener, il listener viene mantenuto attivo più a lungo del necessario. In questo caso, la memoria non allocata corrisponde a una perdita di memoria.

Il modello di evento debole è progettato per risolvere il problema di perdita di memoria. Il modello di evento debole può essere usato quando un listener deve registrarsi per un evento, ma il listener non sa in modo esplicito quando annullare la registrazione. Il modello di evento debole può essere usato anche quando la durata dell'oggetto dell'origine supera la durata utile dell'oggetto del listener. In questo caso, utile è determinato dall'utente. Il modello di evento debole consente al listener di registrarsi e ricevere l'evento senza influire sulle caratteristiche di durata dell'oggetto del listener in alcun modo. In effetti, il riferimento implicito dall'origine non determina se il listener è idoneo per Garbage Collection. Il riferimento è un riferimento debole, quindi la denominazione del modello di evento debole e delle API correlate. Il listener può essere sottoposto a Garbage Collection o distrutto in altro modo e l'origine può continuare senza conservare riferimenti al gestore noncollebili a un oggetto ora eliminato.

Chi deve implementare il modello di evento debole?

Il modello di evento debole è rilevante principalmente per gli autori di controlli. L'autore del controllo è in gran parte responsabile del comportamento e del contenimento del controllo e dell'impatto sulle applicazioni in cui viene inserito. Ciò include il comportamento della durata degli oggetti del controllo, in particolare la gestione del problema di perdita di memoria descritto.

Alcuni scenari si prestano intrinsecamente all'applicazione del modello di evento debole. Uno di questi scenari è data binding. Nel data binding è comune che l'oggetto di origine sia indipendente dall'oggetto listener, che è una destinazione di un'associazione. Molti aspetti del data binding WPF hanno già il modello di evento debole applicato nel modo in cui vengono implementati gli eventi.

Come implementare il modello di evento debole

Esistono quattro modi per implementare il modello di evento debole e ogni approccio usa un gestore eventi diverso. Selezionare il gestore eventi più adatto allo scenario.

  • Gestore eventi deboli esistente:

    Usare una classe di gestione eventi debole esistente quando l'evento che si vuole sottoscrivere ha un oggetto corrispondente WeakEventManager. Per un elenco di gestori eventi deboli inclusi in WPF, vedere la gerarchia di ereditarietà nella WeakEventManager classe . Poiché i gestori eventi deboli inclusi sono limitati, è probabile che sia necessario scegliere uno degli altri approcci.

  • Gestore eventi debole generico:

    Usare un generico WeakEventManager<TEventSource,TEventArgs> quando un esistente WeakEventManager non è disponibile e si sta cercando il modo più semplice per implementare eventi deboli. Tuttavia, il generico WeakEventManager<TEventSource,TEventArgs> è meno efficiente del gestore eventi debole esistente o personalizzato perché usa la reflection per individuare l'evento dal nome. Inoltre, il codice necessario per registrare l'evento usando il generico WeakEventManager<TEventSource,TEventArgs> è più dettagliato rispetto all'uso di un oggetto esistente o personalizzato WeakEventManager.

  • Gestore eventi debole personalizzato:

    Creare un'istanza personalizzata WeakEventManager quando un esistente WeakEventManager non è disponibile ed è fondamentale l'efficienza. Sebbene sia più efficiente di un generico WeakEventManager, un oggetto personalizzato WeakEventManager richiede di scrivere codice più anticipato.

  • Gestore eventi deboli di terze parti:

    Usare un gestore eventi debole di terze parti quando sono necessarie funzionalità non fornite dagli altri approcci. NuGet ha alcuni gestori eventi deboli. Molti framework WPF supportano anche il modello.

Le sezioni seguenti descrivono come implementare il modello di evento debole tramite l'uso dei diversi tipi di gestione eventi. Per gli esempi di gestore eventi deboli generici e personalizzati, l'evento da sottoscrivere presenta le caratteristiche seguenti.

  • Il nome dell'evento è SomeEvent.
  • L'evento viene generato dalla SomeEventSource classe .
  • Il gestore eventi ha il tipo EventHandler<SomeEventArgs>.
  • L'evento passa un parametro di tipo SomeEventArgs ai gestori eventi.

Usare una classe di gestione eventi debole esistente

  1. Trovare un gestore eventi debole esistente. Per un elenco di gestori eventi deboli inclusi in WPF, vedere la gerarchia di ereditarietà della WeakEventManager classe .

  2. Usare il nuovo gestore eventi debole invece del normale hook degli eventi.

    Ad esempio, se il codice usa il modello seguente per sottoscrivere un evento:

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

    Impostarlo sul modello seguente:

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

    Analogamente, se il codice usa il modello seguente per annullare la sottoscrizione a un evento:

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

    Impostarlo sul modello seguente:

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

Usare la classe di gestione eventi debole generica

Usare la classe generica WeakEventManager<TEventSource,TEventArgs> anziché l'hook di eventi normale.

Quando si usano WeakEventManager<TEventSource,TEventArgs> per registrare listener di eventi, specificare l'origine evento e EventArgs il tipo come parametri di tipo alla classe . Chiamare AddHandler come illustrato nel codice seguente:

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

Creare una classe di gestione eventi debole personalizzata

  1. Copiare il modello di classe seguente nel progetto. La classe seguente eredita dalla WeakEventManager classe :

    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. Rinominare SomeEventWeakEventManager, SomeEventSourceSomeEvent, e SomeEventArgs in modo che corrisponda al nome dell'evento.

  3. Impostare i modificatori di accesso per la classe di gestione eventi debole in modo che corrisponda all'accessibilità dell'evento gestito.

  4. Usare il nuovo gestore eventi debole invece del normale hook degli eventi.

    Ad esempio, se il codice usa il modello seguente per sottoscrivere un evento:

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

    Impostarlo sul modello seguente:

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

    Analogamente, se il codice usa il modello seguente per annullare la sottoscrizione a un evento:

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

    Impostarlo sul modello seguente:

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

Vedi anche