martes, 22 de abril de 2014

Eventos - events




Los eventos son un tipo definido dentro del CLR que nos permite notificar a otros objetos algo que está sucediendo en el interior de nuestra clase. Para que estos objetos puedan ser informados, antes tienen que suscribirse al evento.


Desde la aparición de Visual Basic 6, normalmente achacamos los eventos a controles tales como Buttons, Combobox, DataGrids, etc., pero su aplicación y su enfoque va mucho más allá, y su uso en clases que no forman parte de la GUI, es tan normal, tan común y tan útil como en éstas.

El concepto de evento está completamente ligado al de delegado, ya que se nutre de estos para guardar las acciones que se suscriben al mismo, y así añadir su restricción de firma para estas acciones, de manera que solo se puedan suscribir a estos eventos los métodos que cumplan con la firma de su delegado.


Recuerda que aquí tienes el indice de todos los posts del Curso de LinQ.



Pongamos que tenemos una clase Persona, y que en ella exponemos la funcionalidad de informar (a quien se quiera suscribir) que nuestros objetos instanciados han superado la mayoría de edad. Esto lo realizaríamos con un evento.














Nuestra clase ofrecería un evento público que podría llamarse MayorDeEdad, y que informaría de esto a todos los objetos que se hubieran suscrito.

Con el uso de eventos, hacemos que nuestras clases sean mucho más reutilizables, ya que dejemos en manos de nuestros consumidores la elección de la acción que quieren realizar cuando pasa un determinado suceso. En el ejemplo anterior nosotros podríamos haber forzado a que nuestra clase ejecutara un método en vez de un evento cuando su edad superara ‘X’ años, una acción como escribir un texto en consola, en un MessageBox o en un mail, pero esto sería encajonar mucho la funcionalidad, y con el evento dejamos un abanico de posibilidades abierto.


Definición en código de un Evento

Podemos definir un evento dentro del CLR de la siguiente forma:


public delegate void MiDelegado(DateTime MomentoInternoSuceso);
 
 
public event MiDelegado MayorDeEdad;

Nuestro evento, informará que el objeto Persona ha cumplido la mayoría de edad y además dentro de esa información nos indicará el momento exacto en el que lo ha hecho, que puede ser que no sea exactamente el mismo en el que se ha producido la comunicación.


La definición consta de 4 partes:

  1. public .- Ámbito de la declaración
  2. event .- Palabra reservada para declarar eventos.
  3. MiDelegado .- Delegado de referencia al que tendrán que ceñirse los métodos que se suscriban a nuestro evento.
  4. MayorDeEdad .- Nombre de nuestro evento.


Esta declaración de evento, sería completamente válida y correcta para la ejecución de nuestro código, pero se saldría de las reglas y recomendaciones que nos da Microsoft para el uso de eventos. Estas recomendaciones se basan en la firma del delegado que tiene que acompañar a un evento, y que tiene que cumplir las siguientes reglas:

  • No tiene que devolver ningún tipo de dato, por lo que tiene que ser un método void obligatoriamente.
  • Tiene que recibir 2 parámetros, el primero de tipo object, que indicará el objeto que ha lanzado el evento, nombrándose con el nombre de sender, y el segundo tiene que ser de una clase que derive de EventArgs, y en ella deben de incluirse todo la información adicional que quiere dar nuestro evento, este se nombrará como e.

El Framework ya contiene un delegado base para esto, que se llama EventHandler y que tiene la firma siguiente:

/// <summary>
/// Represents the method that will handle an event that has no event data.
/// </summary>
/// <param name="sender">The source of the event. </param><param name="e">An object that contains no event data. </param><filterpriority>1</filterpriority>
[ComVisible(true)]
[__DynamicallyInvokable]
[Serializable]
public delegate void EventHandler(object sender, EventArgs e);


Con poco que hayamos consumido algún evento dentro del Framework, esta firma ya nos resultará familiar.


La clase EventArgs, es prácticamente una clase vacía, pero es la clase base de la que tendremos que heredar cuando queramos ofrecer información dentro de nuestros eventos.

Por todo esto tendríamos que realizar una serie de cambios en nuestro código para hacerlo compatible, los cambios serían los siguientes:


1.- Crearemos una clase que herede de EventArgs y que incluirá un campo para almacenar el DateTime con el momento exacto del cumplimento de la mayoría de edad.

public class MomentoEventArgs : EventArgs
{
    public DateTime InstanteDelSuceso { get; set; }  
}

Por nomenclatura, estas clases finalizan con la definición de EventArgs, para que sean más compresibles a la lectura, en nuestro ejemplo MomentoEventArgs.

2.- En segundo lugar, modificaremos la definición de nuestro delegado con los nuevos cambios:

public delegate void MiDelegado(object sender, MomentoEventArgs MomentoInternoSuceso);


Llegados a este punto, y pensando en un desarrollo amplio, podríamos llevarnos las manos a la cabeza con la cantidad de delegados que tendríamos que definirnos para poder cubrir los posibles casos de información dentro de nuestras clases EventArgs. Esto supondría una pérdida de tiempo y un aumento de nuestro código, pero Microsoft sacó una respuesta inmediata añadiendo el delegado genérico EventHandler<TEvenArgs>, que tiene la siguiente firma:


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



Nota: Me resulta bastante curioso que dejaran un delegado genérico tan abierto, y no lo limitaran a una clase que derivara de EventArgs, como marca la firma, pero supongo que sus razones tendrían, pero la firma ideal hubiera sido la siguiente:

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

Si tenéis alguna duda sobre restricciones de Generics os remito a mis entradas anteriores sobre el tema aquí: link

Con lo que nos podríamos librar de la definición del delegado, y nos bastaría solo con la del evento:

public event EventHandler<MomentoEventArgs> MayorDeEdad;



Implementación interna de un Evento

En esta parte vamos a tratar la manera en que se realizan las llamadas a un campo de tipo evento dentro de la misma clase, y como debemos comprobar si tiene o no tiene algún suscriptor vinculado.


Para ello vamos a comenzar a implementar la clase Persona:

public class Persona
{
    public Guid   ID     { get; set; }
    public string Nombre { get; set; }
    private int  _edad;
 
    public int Edad
    {
        get { return _edad; }
 
        private set
        {
            _edad = value;
            /// Llamada al evento
            if(this.Edad >= 18)
                this.MayorDeEdad(this, new MomentoEventArgs{ InstanteDelSuceso = DateTime.Now });
        }
    }
 
 
    public event EventHandler<MomentoEventArgs> MayorDeEdad;
 
    public Persona(string nombre)
    {
        this.ID     = new Guid();
        this.Nombre = nombre;
        this.Edad   = 0;
    }
 
 
    public void CumplirAños()
    {
        this.Edad++;
    }
}


Peculiaridades de la implementación:

  • Tiene una propiedad de solo lectura (de forma externa) llamada edad. Esto es así para que nadie pueda manipular la edad desde el exterior a la clase, y para que solo se pueda hacer esto desde el interior y desde su método CumplirAños.
  • Tiene un constructor que siempre inicializa la edad a 0 años, por lo que nuestras personas nacen cuando se crean sus instancias.
  • Tiene un método CumplirAños, que aumenta en un año la edad actual de la persona.


La parte más importante sin en cambio es la llamada al evento:


private set
{
    _edad = value;
    /// Llamada al evento
    if(this.Edad >= 18)
        this.MayorDeEdad(this, new MomentoEventArgs{ InstanteDelSuceso = DateTime.Now });
}


Como se puede apreciar, cada vez que cambia la edad, comprobamos que supere los 18 años, y en ese caso lanzamos el evento. Este código tiene un error muy normal que se suele cometer cuando se empieza en el uso con eventos, y es que no está comprobando que haya alguien suscrito al evento (this.MayorEdad ¡= null), y si nadie se hubiera suscrito este código lanzaría un error por llamar a un evento nulo. Para arreglar esto, realizaríamos el siguiente cambio:

private set
{
    _edad = value;
    /// Llamada al evento
    if(this.Edad >= 18)
        if(this.MayorDeEdad != null)
            this.MayorDeEdad(this, new MomentoEventArgs{ InstanteDelSuceso = DateTime.Now });
}


Añadiríamos la comprobación antes de la llamada de manera que solo lanzaríamos el evento en caso de que tuviera algún suscriptor. Esto sigue siendo completamente correcto, pero en nuestro ejemplo solo tenemos una llamada al evento, si tuviéramos 100 o 1000, ¿Tendríamos que hacer la comprobación en todas?, y si tuviéramos que modificar algo de la comprobación, ¿tendríamos que ir a cada una de ellas a realizar el cambio?, para esto y para otras cosas más haremos uso del Patrón On.

Nos crearemos un método Protected Virtual void, con el nombre de nuestro evento, precedido por la palabra On y recibiendo por parámetro la clase EventArgs. Esto es así, para que las clases que hereden de la nuestra, tenga la capacidad de poder sobreescribir este método y puedan cambiar la funcionalidad y la condición de lanzamiento del mismo. A la vez encapsularemos dentro de este evento la comprobación de suscripción.

Así quedaría en nuestro ejemplo:


protected virtual void OnMayorDeEdad(MomentoEventArgs e)
{
    if(this.MayorDeEdad != null)
        this.MayorDeEdad(this, e);
}

Y así la llamada:

private set
{
    _edad = value;
    /// Llamada al evento
    if(this.Edad >= 18)
        this.OnMayorDeEdad(new MomentoEventArgs{ InstanteDelSuceso = DateTime.Now });
}


Suscribiéndose a eventos


La forma de suscribirnos a un evento, es prácticamente igual que la de instanciar un delegado, cuando nos suscribimos desde aplicaciones de GUI (Windows Forms, WPF, ASP, etc) nos parece transparente ya que es el propio Visual Studio es el que lo realiza por nosotros en las cases o métodos (.Designer, InitializeComponents, etc), pero vamos a ver unos ejemplos de cómo podemos hacerlo:


Code:
static void Main(string[] args)
{
    Persona p = new Persona ( "Pedro" );
 
    /// Desde llamada a método común
    p.MayorDeEdad += p_MayorDeEdad;
 
    /// Desde delegado anónimo que NO HACE USO DE PARÁMETROS
    p.MayorDeEdad += delegate
    {
        /// Hacer algo 
    };
 
    /// Desde delegado anónimo que HACE USO DE PARÁMETROS
    p.MayorDeEdad += delegate(object sender, MomentoEventArgs e)
    {
        DateTime d = e.InstanteDelSuceso;
        /// Hacer algo 
    };
 
    /// Desde Expression Lambda
    p.MayorDeEdad += (sender, e) =>
    {
        DateTime d = e.InstanteDelSuceso;
        /// Hacer algo
    };
}
 
static void p_MayorDeEdad(object sender, ProgramMomentoEventArgs e)
{
  /// Hacer algo
}


Aparte de todas estas, habría otra que podríamos usarla por herencia, dentro las clases que heredaran de Persona, por medio de la redefinición de métodos, así:

public class Empleado : Persona
{
    protected override void OnMayorDeEdad(MomentoEventArgs e)
    {
        /// hacer cosas
            
        base.OnMayorDeEdad(e);
    }
}


¿Por qué eventos existiendo delegados?


Hay gente que se pregunta el por qué de la existencia de eventos, cuando un delegado podría realizar su función con las mismas garantías que un evento. La respuesta la tenemos en uno de los pilares de la POO, y es la encapsulación. Si sustituyéramos nuestros eventos por delegados, la ejecución de los mismos podría lanzarse al gusto por cualquier objeto que consumiera la clase. Con los eventos esto no pasa y solo se lanzan cuando la implementación interna de la clase lo requiere, y vienen a ser un delegado público sin permisos de ejecución.





Pues ha quedado una entrada un poco más larga de lo normal, pero así lo tenemos todo en un mismo texto.

Continuamos con LinQ.