¿Por qué este oyente genérico de C# WeakEvent no funciona?

Estaba tratando de ahorrar tiempo al desarrollar un oyente de eventos débil genérico que podía simplemente pasar en una Acción para tomar. Las cosas parecen funcionar bien hasta que uno no está registrado. Esto parece anular el registro de todos ellos. Lo que me confunde es por qué y en qué se diferencia esto de pasar este para el parámetro IWeakEventListener?

  public class GenericWeakEventListener : IWeakEventListener
  {
    #region EventAction

    /// <summary>
    /// Action to take for the event
    /// </summary>
    private Action<Type, object, EventArgs> _eventAction;

    /// <summary>
    /// Gets or sets the action to take for the event
    /// </summary>
    //[DataMember]
    public Action<Type, object, EventArgs> EventAction
    {
      get
      {
        return _eventAction;
      }

      private set
      {
        if (EventAction != value)
        {
          _eventAction = value;
        }
      }
    }

    #endregion EventAction

    #region Constructors

    public GenericWeakEventListener(Action<Type, object, EventArgs> action)
    {
      EventAction = action;
    }

    #endregion Constructors

    #region Public Methods

    public bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
    {
      if (EventAction != null)
      {
        EventAction(managerType, sender, e);
      }

      return true;
    }

    #endregion Public Methods
  }

EDIT:

Este es el código del oyente:

  public class SomeClient
  {
    public int ID { get; set; }

    private Timer timer = null;
    private Timer timer2 = null;

    public SomeClient(int id, SomeService service)
    {
      ID = id;
      //EventHandler<GenericEventArgs<string>> d = (o, s) => Console.WriteLine("Client {0}: {1}", ID, s.Item);
      if (service != null) SomeEventChangedEventManager.AddListener(service, new GenericWeakEventListener((t, s, e) => { Console.WriteLine("SomeEvent: " + ID); }));

      timer = new Timer { AutoReset = true, Interval = 1000 };
      SomeTimerElapsedEventManager.AddListener(timer, new GenericWeakEventListener((t, s, e) => { Console.WriteLine("SomeTimer: " + ID); }));
      timer.Start();
    }
  }

Este es el código del editor:

  public class SomeService
  {
    public event EventHandler<GenericEventArgs<string>> SomeEvent;

    public SomeService()
    {
      System.Timers.Timer timer = new Timer { AutoReset = true, Interval = 1000 };
      timer.Elapsed += (sender, args) => { if (SomeEvent != null) SomeEvent(this, new GenericEventArgs<string>(Guid.NewGuid().ToString())); };
      timer.Start();
    }
  }

Este es el código del método principal:

public static void Main(string[] args)
{
  SomeService service = new SomeService();
  List<SomeClient> clients = new List<SomeClient>();

  // Build clients
  for (int x = 0; x < 5; x++)
  {
    clients.Add(new SomeClient(x + 1, service));
  }

  System.Timers.Timer timer = new Timer { AutoReset = true, Interval = 5000 };
  timer.Elapsed += (s, a) =>
    {
      if (clients.Count == 0)
      {
        return;
      }

      Console.WriteLine("Removing\r\n");
      clients.RemoveAt(0);
      GC.Collect();
    };
  timer.Start();

  Console.ReadLine();
}

Esta es la salida: SomeEvent: 1 SomeEvent: 2 SomeEvent: 3 SomeEvent: 4 SomeEvent: 5 SomeTimer: 2 SomeTimer: 3 SomeTimer: 4 SomeTimer: 1 SomeTimer: 5 SomeEvent: 1 SomeEvent: 2 SomeEvent: 3 SomeEvent: 4 SomeEvent: 5 AlgúnTemporizador: 1 AlgúnTemporizador: 2 AlgúnTemporizador: 3 AlgúnTemporizador: 4 AlgúnTemporizador: 5 AlgúnEvento: 1 AlgúnEvento: 2 AlgúnEvento: 3 AlgúnEvento: 4 AlgúnEvento: 5 AlgúnTemporizador: 2 AlgúnTemporizador: 3 AlgúnTemporizador: 4 AlgúnTemporizador: 5 AlgúnTemporizador: 1 AlgúnEvento: 1 AlgúnEvento: 2 SomeEvent: 3 SomeEvent: 4 SomeEvent: 5 SomeTimer: 1 SomeTimer: 2 SomeTimer: 3 SomeTimer: 5 SomeTimer: 4 SomeEvent: 1 SomeEvent: 2 SomeEvent: 3 SomeEvent: 4 SomeEvent: 5 Eliminando

Sin el oyente genérico de eventos débiles, la salida continúa sin 1 y luego sin 2 y así sucesivamente.

preguntado el 03 de mayo de 12 a las 16:05

1 Respuestas

EDITAR DESDE OP EDITAR: Entendido. Esto es lo que creo que está sucediendo:

Está declarando una lambda propiedad de SomeClient y que contiene un cierre externo para el ID de variable de instancia. Eso hace que la lambda, que se pasa como un delegado al evento en SomeService a través de SomeEventChangedEventManager, dependa de la existencia continua de esa instancia de ID.

Cuando elimina esa instancia de SomeClient, la variable de ID requerida por esa lambda queda fuera del alcance y se GCed. Sin embargo, no veo ninguna parte de este código que elimine esta lambda de los controladores de SomeEvent de SomeService. Entonces, la lambda permanece en la memoria como una referencia al delegado anónimo, pero otros datos de los que depende ya no están. Esto hace que el tiempo de ejecución lance una excepción, que de alguna manera se traga y no hace que todo el programa explote.

Sin embargo, el evento, que básicamente ejecuta los delegados del controlador en el orden en que se adjuntaron (este es un detalle de implementación que generalmente le dicen que ignore) ha detenido la ejecución porque uno de los controladores se ha descartado. Esto hace que parezca que al eliminar el primer cliente se eliminaron todos, cuando en realidad los controladores de esos clientes simplemente no se ejecutan porque el primer controlador tuvo un error.

La solución es doble:

  • Defina la lambda y guárdela como una variable de instancia de SomeClient. Esto le permite mantener una referencia a él, lo cual es importante porque los delegados no se comparan semánticamente al determinar la igualdad, por lo que el siguiente código no funciona:

    SomeEvent += (a,b,c) => Foo(a,b,c);
    //the following line will not remove the handler added in the previous line,
    //because the two lambdas are compiled into differently-named methods
    //and so this is a different reference to a different method.
    SomeEvent -= (a,b,c) => Foo(a,b,c);
    
  • Implemente IDisposable y/o un finalizador en SomeClient. Al eliminar el cliente de la lista, el eliminador/finalizador, llamado por el GC, debe eliminar la lambda de esta instancia de los oyentes de SomeEvent (probablemente a través de un método RemoveListener() en el Administrador). Debido a que mantuvo una referencia al delegado que apunta exactamente a lo que se agregó, el controlador se eliminará y, por lo tanto, no se ejecutará y no generará errores.

contestado el 03 de mayo de 12 a las 19:05

El genérico fue para mi propia prueba. El setter en realidad nunca se usa y se puede quitar con los mismos resultados. Si creo dos instancias y una queda fuera del alcance, ambas dejan de responder. - Telaviano

Esa es una muy buena y completa respuesta. Sería un problema cuando la acción se saliera del alcance. No pensé en eso. ¡Gracias! - Telaviano

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.