Modelos de eventos débiles (WPF .NET)

En las aplicaciones, es posible que los controladores asociados a orígenes de eventos no se destruyan en coordinación con el objeto de escucha que adjuntó el controlador al origen. Esta situación puede provocar fugas de memoria. Windows Presentation Foundation (WPF) introduce un modelo de diseño que se puede usar para solucionar este problema. El modelo de diseño proporciona una clase de administrador dedicada para eventos concretos e implementa una interfaz en los clientes de escucha para ese evento. Este modelo de diseño se conoce como modelo de eventos débil.

Importante

La documentación de la guía de escritorio para .NET 7 y .NET 6 está en proceso de elaboración.

Requisitos previos

En el artículo se da por supuesto un conocimiento básico de los eventos enrutados y que ha leído Información general sobre eventos enrutados. Para seguir los ejemplos de este artículo, le ayudará estar familiarizado con el lenguaje XAML y saber cómo escribir aplicaciones de Windows Presentation Foundation (WPF).

¿Por qué implementar el modelo de eventos débil?

La escucha de eventos puede provocar fugas de memoria. La técnica habitual para escuchar un evento es usar la sintaxis específica del lenguaje para adjuntar un controlador a un evento en un origen. Por ejemplo, la instrucción de C# source.SomeEvent += new SomeEventHandler(MyEventHandler) o la instrucción de VB AddHandler source.SomeEvent, AddressOf MyEventHandler. Sin embargo, esta técnica crea una referencia segura desde el origen del evento al cliente de escucha del evento. A menos que el controlador de eventos se anule explícitamente del registro, la duración del objeto del cliente de escucha se verá afectada por la duración del objeto del origen. En determinadas circunstancias, puede que quiera que la duración del objeto del cliente de escucha se controle por otros factores como, por ejemplo, si pertenece actualmente al árbol visual de la aplicación. Siempre que la duración del objeto del origen se extienda más allá de la duración útil del objeto del cliente de escucha, dicho cliente se mantiene activo más tiempo del necesario. En este caso, la memoria sin asignar equivale a una fuga de memoria.

El modelo de eventos débil está diseñado para resolver el problema de la fuga de memoria. El modelo de eventos débil se puede usar cuando un cliente de escucha necesita registrarse para un evento, pero este cliente no sabe explícitamente cuándo anular el registro. El modelo de eventos débil también se puede usar siempre que la duración del objeto del origen supere la duración útil del objeto del cliente de escucha. En este caso, la utilidad la determina usted. El modelo de eventos débil permite que el agente de escucha se registre y reciba el evento sin afectar a las características de duración del objeto del agente de escucha de ninguna manera. En efecto, la referencia implícita del origen no determina si el cliente de escucha es apto para la recolección de elementos no utilizados. La referencia es una referencia débil, de ahí la nomenclatura del modelo de eventos débil y las API relacionadas. El agente de escucha puede experimentar una recolección de elementos no utilizados o destruirse, y el origen puede continuar sin conservar referencias de controlador no recopilables a un objeto ahora destruido.

¿Quién debe implementar el modelo de eventos débil?

El modelo de eventos débil es relevante sobre todo para los creadores de controles. Como creador del control, usted es en gran medida responsable del comportamiento y la independencia del control, así como del impacto que tiene en las aplicaciones en las que se inserta. Esto incluye el comportamiento de duración del objeto del control, en concreto, el tratamiento del problema de fuga de memoria descrito.

Ciertos escenarios se prestan inherentemente a la aplicación del modelo de eventos débil. Uno de estos escenarios es el enlace de datos. En el enlace de datos, es habitual que el objeto de origen sea independiente del objeto de escucha, que es un destino de un enlace. Muchos aspectos del enlace de datos de WPF ya tienen el modelo de eventos débil aplicado en cómo se implementan los eventos.

Cómo implementar el modelo de eventos débil

Hay cuatro formas de implementar el modelo de eventos débil y cada enfoque usa un administrador de eventos distinto. Seleccione el administrador de eventos que mejor se adapte a su escenario.

  • Administrador de eventos débil existente:

    Use una clase de administrador de eventos débil existente cuando el evento al que quiera suscribirse tenga un elemento WeakEventManager correspondiente. Para obtener una lista de administradores de eventos débiles que se incluyen con WPF, consulte la jerarquía de herencia en la clase WeakEventManager. Dado que los administradores de eventos débiles incluidos son limitados, probablemente deba elegir uno de los otros enfoques.

  • Administrador de eventos débil genérico:

    Use un objeto WeakEventManager<TEventSource,TEventArgs> genérico cuando no haya disponible un elemento WeakEventManager existente y esté buscando la forma más fácil de implementar eventos débiles. Sin embargo, el objeto WeakEventManager<TEventSource,TEventArgs> genérico es menos eficaz que un administrador de eventos débil existente o personalizado porque usa la reflexión para detectar el evento a partir de su nombre. Además, el código necesario para registrar el evento mediante el objeto WeakEventManager<TEventSource,TEventArgs> genérico es más detallado que el uso de un elemento WeakEventManager existente o personalizado.

  • Administrador de eventos débil personalizado:

    Cree un objeto WeakEventManager personalizado cuando no haya disponible un elemento WeakEventManager existente y la eficacia sea fundamental. Aunque es más eficaz que un objeto WeakEventManager genérico, un elemento WeakEventManager personalizado requiere escribir más código inicial.

  • Administrador de eventos débil de terceros:

    Use un administrador de eventos débil de terceros cuando se necesite una funcionalidad que no proporcionen el resto de enfoques. NuGet tiene algunos administradores de eventos débiles. Muchos marcos de WPF también admiten el patrón.

En las secciones siguientes se describe cómo implementar el modelo de eventos débil mediante el uso de los distintos tipos de administradores de eventos. En el caso de los ejemplos de administradores de eventos débiles genéricos y personalizados, el evento al que suscribirse tiene las características siguientes.

  • El nombre del evento es SomeEvent.
  • La clase SomeEventSource genera el evento.
  • El controlador de eventos tiene el tipo: EventHandler<SomeEventArgs>.
  • El evento pasa un parámetro de tipo SomeEventArgs a los controladores de eventos.

Uso de una clase de administrador de eventos débil existente

  1. Busque un administrador de eventos débil existente. Para obtener una lista de los administradores de eventos débiles que se incluyen con WPF, consulte la jerarquía de herencia de la clase WeakEventManager.

  2. Use el nuevo administrador de eventos débil en lugar del enlace de eventos normal.

    Por ejemplo, si el código usa el siguiente modelo para suscribirse a un evento:

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

    Cámbielo por el siguiente modelo:

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

    Del mismo modo, si el código usa el siguiente modelo para cancelar la suscripción de un evento:

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

    Cámbielo por el siguiente modelo:

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

Uso de la clase genérica de administrador de eventos débil

Use la clase WeakEventManager<TEventSource,TEventArgs> genérica en lugar del enlace de eventos normal.

Cuando se usa WeakEventManager<TEventSource,TEventArgs> para registrar clientes de escucha de eventos, se proporcionan el origen del evento y el tipo EventArgs como parámetros de tipo a la clase. Llame a AddHandler como se muestra en el código siguiente:

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

Creación de una clase personalizada de administrador de eventos débil

  1. Copie la siguiente plantilla de clase en el proyecto. La clase siguiente hereda de la clase 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. Cambie el nombre de SomeEventWeakEventManager, SomeEvent, SomeEventSource y SomeEventArgs para que coincida con el nombre del evento.

  3. Establezca los modificadores de acceso de la clase de administrador de eventos débil para que coincidan con la accesibilidad del evento que administra.

  4. Use el nuevo administrador de eventos débil en lugar del enlace de eventos normal.

    Por ejemplo, si el código usa el siguiente modelo para suscribirse a un evento:

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

    Cámbielo por el siguiente modelo:

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

    Del mismo modo, si el código usa el siguiente patrón para cancelar la suscripción a un evento:

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

    Cámbielo por el siguiente modelo:

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

Vea también