Duda sobre ejemplo de MSDN para exponer evento

Answered Duda sobre ejemplo de MSDN para exponer evento

  • Sunday, August 07, 2011 5:07 AM
     
      Has Code

    Hola. Hace uno dias, Pedro Hurtado, en este foro me recomendo este articulo: http://msdn.microsoft.com/es-es/library/w369ty8x(v=vs.80).aspx

    Alli se explica detalladamente con un ejemplo como exponer eventos. Aqui dejo una parte del ejemplo

     protected virtual void OnRaiseCustomEvent(CustomEventArgs e)
        {
          // Make a temporary copy of the event to avoid possibility of
          // a race condition if the last subscriber unsubscribes
          // immediately after the null check and before the event is raised.
          EventHandler<CustomEventArgs> handler = RaiseCustomEvent;
    
          // Event will be null if there are no subscribers
          if (handler != null)
          {
            // Format the string to send inside the CustomEventArgs parameter
            e.Message += String.Format(" at {0}", DateTime.Now.ToString());
    
            // Use the () operator to raise the event.
            handler(this, e);
          }
        }

    Me llamo la atención el primer comentario (aunque no estoy seguro de haber entendido bien). Alli dice que hay que hace una copia temporal de "RaiseCustomEvent" en la variable "handler" por si el último subscripo al evento se dió de baja justo en el instante que transcurre entre el "if" y la llamada al evento con handler(this, e). Pero lo cierto es que no hace una copia, sino que crea la variable "handler" y hace que ésta apunte al mismo objeto que "RaiseCustomEvent", con lo cual si ocurriera lo que pretende prevenir, el error se produciría igual. ¿Que opinan?

    Saludos

All Replies

  • Sunday, August 07, 2011 5:41 AM
    Moderator
     
      Has Code

    hola

    ten en cuenta que esta declarando el evento por medio del uso del EventHandler<> por eso aplica esa tecnica

    si usas eventos de forma clasica deberias haber declarado un delegate

    la forma en como lo harias normalmente sin el EventHandler<> seria

    Events Tutorial

    veras alli que se define
    public delegate void ChangedEventHandler(object sender, EventArgs e);

    y tambien el evento
    public event ChangedEventHandler Changed;

    pero para lanzarlo solo se usa
    protected virtual void OnChanged(EventArgs e) 
    {
     if (Changed != null)
      Changed(this, e);
    }
    esta creo es la forma a la cual apuntas, solo se valida que el evento no sea null, ya que si es null es porque nadie se subscribio al evento
    declarando el delegate y el evento no necesitas hacer una copia como menciona alli
    en cambio usando el  EventHandler<> si se requiere
    aqui hay mas info de la forma clasica de declarar evento

    saludos


    Leandro Tuttini

    Blog
    Buenos Aires
    Argentina

  • Sunday, August 07, 2011 6:20 AM
     
      Has Code
    declarando el delegate y el evento no necesitas hacer una copia como menciona alli
    en cambio usando el  EventHandler<> si se requiere

    Este es el comentario al que me refiero:

        // Make a temporary copy of the event to avoid possibility of
       // a race condition if the last subscriber unsubscribes
       // immediately after the null check and before the event is raised.

    Creo que no dice que sea necesaria la copia, es sólo para prevenir una remota posibilidad de que pase "if(Changed !=null)" y se de de baja justo antes de "Changed(this,e)". O quiza este traduciendo mal?

    Eso por un lado, pero además no hace realmente una copia, si mirás bien el codigo:

     EventHandler<CustomEventArgs> handler = RaiseCustomEvent;

    Aquí las dos variables (handler y RaiseCustomEvent) apuntan al mismo objeto, por lo tanto al darse de baja, las dos variables quedarian en null y se produciria un error. ¿No debería ser algo así?

    EventHandler<CustomEventArgs> handler = new EventHandler<CustomEventArgs>(); y lugo no se como seguiría

    saludos


  • Sunday, August 07, 2011 10:18 AM
    Moderator
     
     

    Hola,

     

    EventHandler<TEventArgs> es en un delegado, por eso te dice que no hace falta definir un delegado de hecho la definición de este es la siguiente:

    public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e) where TEventArgs: EventArgs;

    Te explico, esto es un delegado donde uno de los parametros es un Genericos c#,  pero si te das cuenta al final de la definición te encuentras  con lo siguiente where TEventArgs :EventArgs Lo que le esta diciendo es que cualquier valor que se le pase debe de heredar de EventArgs y así es como se recomienda crear todas las clases que se definen para pasar datos en un evento.

     

    De hecho si no estuviese definido de esta forma en el ejemplo, te obliga a definir lo siguiente, para poder definir un evento:

    public delegate void CustomEventHandler<objet sender,CustomEventArgs);

    public event CustomEventHandler Custom;

    Esta es la forma en la que se definían los eventos antes del framework 2.0, a partir de este se puede hacer lo siguiente:

    public event EventHandler<CustomEventArgs> Custom. 

    Tanto esta linea como las dos anteriores son lo mismo.

     

    Basicamente quiero que te quede claro que para definir un evento necesitas cuatro cosas:

    1. Una clase que herede de EventArgs y que en la mayoría de los casos se definen sus propiedades como public tipo propiedad{private set; get;}. Para que el consumidor del evento no nos pueda alterar los datos. Hay veces que esa propiedad se define como public set y es porque queremos que el consumidor del evento nos informe de algo. Si te fijas en las siguientes clases:

     FormClosingEventArgs  y FormClosedEventArgs tienes en la primera de ellas dos propiedades Cancel y CloseReason fijate que Cancel cuando tu lo consumes viene con valor false, si tu lo pones a true el formulario no se cierra, por contra FormClosedEventArgs solamente nos informa de CloseReason.

    Como buena practica a esta clase en el contructor se le definen todos parametros que después tu almacenas en las propiedadess, fijate en la firma del contructor de FormClosingEventArgs

    public FormClosingEventArgs(CloseReason closeReason, bool cancel) : base(cancel)

    2. Un delegado (recomendado con la siguiente firma) delegado(object sender,eventargs e);

    3. Un evento del tipo del delegado que es el que se consume.

    4. En la clase que lanza el evento  un metodo que empieza por OnRaise segido del nombre del evento OnRaiseCustomEvent y que como parametro recibe la clase que defines en punto 1.

    Como buena practica lo lógico que ese método lo definas con el siguiente nivel de acceso protected virtual, para que solamente los herederos sean capaces de verlo y reemplazarlo.

     

    A partir de la versión de framework 2.0 y la aparación de los genericos, te puedes ahorrar definir el delegado, puesto EventHandler<TEventArgs> es un delegado que permite pasar cualquier clase que herede de EventArgs.

     

    Alli dice que hay que hace una copia temporal de "RaiseCustomEvent" en la variable "handler" por si el último subscripo al evento se dió de baja justo en el instante que transcurre entre el "if" y la llamada al evento con handler(this, e). Pero lo cierto es que no hace una copia, sino que crea la variable "handler" y hace que ésta apunte al mismo objeto que "RaiseCustomEvent"

     

    Si tienes toda la razón es decir es una variable que en ejemplo se llama handler pero que se podría llamar "Hola";

     

    con lo cual si ocurriera lo que pretende prevenir, el error se produciría igual.

     

    Error no se va a producir ninguno puesto que pregunta if (handler!=null). Esto se hace porque yo he programado en mi clase la llamada a al metodo "OnRaise" y tu puede ser que no hayas consumido el evento. si yo no preveo eso y escribo lo siguiente:

    handler(this,e)  sin el control de no es nulo eso produce un error. Imagina consumir todos los eventos de los controles y formularios para solucionar este problema.

    Independientemente de que tu por el Cuadro de propiedades te vayas a la pestaña de eventos y pulses doble click, lo que hace internamente es lo siguiente,  en el InicializeComponent del Formulario crea la siguiente linea.

     

    this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.Form1_FormClosing);

    Eso es lo que ese famoso handler o Hola, según te guste, aunque te recomiendo handler:) no sea null. Es decir alguien ha dicho que quiere consumir nuestro evento.

     

    Si lo que quieres es que eso sea null; tienes que escribir lo siguiente:

    this.FormClosing -= new System.Windows.Forms.FormClosingEventHandler(this.Form1_FormClosing);

     

    Saludos,


     


    phurtado
  • Sunday, August 07, 2011 10:33 AM
    Moderator
     
     Answered Has Code

    @AntiWork

    Esto es para evitar problemas con multithread...

    A ver si te lo puedo explicar :)

    Imagina que tienes el código que no debe usarse aunque aparece en el 99% de ejemplos:

    if (Changed!=null)
    {
       Changed(this, new ChantedEventArgs());
    }
    

    Imagina la siguiente situación:

    1. Tienes un thread (TA) que tiene un objeto que está suscrito al evento Changed.
    2. Tienes otro thead (TB) que desuscribe el mismo objeto del evento

    Bien, imagina que tienes dos objetos suscritos a dicho evento en dos threads distintos. Imagina que se pone en marcha el thread 1:

    • Se está ejecutando TA y debe lanzarse el evento Changed...
    • El TA ejecuta el Changed!=null y devuelve true. Por lo que entra en el if.
    • En ese momento el CLR decide cambiar de thread y entra el  TB
    • El TB desuscribe el objeto del evento changed. En este momento Changed ha pasado a ser null
    • De nuevo el CLR cambia de thread y ahora entra TA
    • El código de TA ya había evaluado el if, por lo que lanzará el Changed(this, ...). Pero... Changed es null! -> NullReferenceException

    Bien, veamos ahora que pasa si usas el código que debe usarse:

    var handler = this.Changed;
    if (handler !=null)
    {
      handler(this, new ChangedEventArgs());
    }
    

    • Se está ejecutando TA y debe lanzarse el evento Changed...
    • TA ejecuta el handler=this.Changed. En este punto TA tiene una variable local que es una copia del delegate. Eso es porque los Multicast delegate son imutables. Cuando se añade o se modifica un elemento de un Multicast delegate lo que se hace realmente es crear otro Multicast delegate
    • TA ejecuta el handler!=null y devuelve true, por lo que entra en el if.
    • En ese momento el CLR decide cambiar de thread y entra el  TB
    • El TB desuscribe el objeto del evento changed. En este momento se crea otro multicast delegate que sea asigna a null y se guarda en Changed.
    • De nuevo el CLR cambia de thread y ahora entra TA
    • El código de TA ya había evaluado el if. En este momento Changed es null pero handler NO, pues handler contiene el multicast delegate antiguo (el que había antes de que se desuscribiese el último suscriptor). Así pues lanzamos el evento. Consecuencia: un objeto desuscrito recibe el evento. Mejor esto que un NullReferenceException.

    Es lioso de explicar, pero te pongo un ejemplo, que con la ayuda del debugger lo entenderás. Crea un proyecto de consola con este código

      class Program
      {
        private static A a = new A();
    
        static void Main(string[] args)
        {
          a.foo();
        }
    
        public static void Add()
        {
          a.evt1 +=new EventHandler(a_evt1);
        }
    
        public static void Remove()
        {
          a.evt1 -= a_evt1;
        }
    
        static void a_evt1(object sender, EventArgs e)
        {
        }
      }
    
    
      class A
      {
        public event EventHandler evt1;
    
        public A()
        {
          
        }
    
        public void foo()
        {
          var handler = this.evt1;
          int i = 0;
        }
      }
    
    


    Pon un breakpoint dentro de foo(), justo en la primera linea.

    Una vez se pare la ejecución añade dos watches: Changed y handler.

    En la ventana de watch, añade Program.Add() y ejecuta el watch. En este momento verás que Changed tiene valor y handler vale null.

    Ejecuta la línea handler=this.evt1. En este momento Changed y handler tienen el mismo valor.

    Añade el watch Program.Remove(); En este momento verás que Changed pasa a ser null pero handler sigue teniendo valor:

     

    Un saludo! :D

    PD: Más info en el post de Eric Lippert: http://blogs.msdn.com/b/ericlippert/archive/2009/04/29/events-and-races.aspx


    Eduard Tomàs Blog: http://geeks.ms/blogs/etomas -- Twitter: eiximenis
  • Sunday, August 07, 2011 11:17 AM
    Moderator
     
     

    Hola Eduard:

    La verdad es que eres como un libro abierto, da gusto aprender a tu lado, ni se me había pasado por la cabeza, y yo que pensé que había dado una buena respuesta :(

    Como sugerencia si no te importa explica como crear los watches, paso a paso y te queda de 20 no de 10 :)

     

    Saludos,


    phurtado
  • Sunday, August 07, 2011 11:32 AM
    Moderator
     
     

    Hola a todos,

     

    Rectifico está parte de mi respuesta:

     

    Alli dice que hay que hace una copia temporal de "RaiseCustomEvent" en la variable "handler" por si el último subscripo al evento se dió de baja justo en el instante que transcurre entre el "if" y la llamada al evento con handler(this, e). Pero lo cierto es que no hace una copia, sino que crea la variable "handler" y hace que ésta apunte al mismo objeto que "RaiseCustomEvent"

     

    Si tienes toda la razón es decir es una variable que en el  ejemplo se llama handler pero que se podría llamar "Hola";

     

    Lo correcto es lo que dice Eduard.

    Saludos,


    phurtado
  • Sunday, August 07, 2011 4:13 PM
     
     

    Hola. La explicación de Eduard Tomás (el paso a paso de los thread TA y TB) es perfecta para explicar por qué podría volverse null la variable Changed al pasar de una linea de codigo a otra: de "if(this.changed);" a "Changed(this, new ChangedEvenArgument);".

    Pero no estoy seguro de haber entendido bien el por qué esa copia que se hace en la variable handler soluciona el problema. En general, cuando la instancia de un objeto cambia, el cambio se refleja en todas la varible que lo referencian. Desde este punto de vista, al hacer this.Changed = null; la variable handler tambien debería ser null porque ambas apuntan a la misma instancia.

    Creo haber entendido, de la explicacion de Eduard, que el secreto esta en las características especiales que tienen los objetos "Multicast delegate" que cuando cambian, se crea una nueva instancia. De esta manera todas las otra variables que referencien al objeto conservarán su valor original. ¿Es así?

    Muchas gracias.

  • Sunday, August 07, 2011 4:36 PM
    Moderator
     
     

    Pero no estoy seguro de haber entendido bien el por qué esa copia que se hace en la variable handler soluciona el problema

    te ha dado la respuesta cuando comento

    Eso es porque los Multicast delegate son imutables. Cuando se añade o se modifica un elemento de un Multicast delegate lo que se hace realmente es crear otro Multicast delegate

    o sea por inmutable quiere decir que devuelve un clone del objecto, y no trabja sobre eel original, por eso aunque el original se cambie el que tu usas para alznar el evento queda sin cambio

     

    saludos


    Leandro Tuttini

    Blog
    Buenos Aires
    Argentina
  • Sunday, August 07, 2011 5:14 PM
     
     

    Ok. Entonces entendí bien.

    Muchas gracias.

    Saludos