Skip to content

8. Eventos de usuario

En el capítulo anterior abordamos el concepto de eventos vinculados a los componentes de un formulario. Ahora veremos cómo crear eventos en nuestras propias clases.

8.1. Objetos delegados predefinidos

El concepto de objeto delegate ya se mencionó en el capítulo anterior, pero en aquel momento solo se trató de forma superficial. Cuando analizamos cómo se declaraban los controladores de eventos de los componentes de un formulario, nos encontramos con un código similar al siguiente:


            this.buttonAfficher.Click += new System.EventHandler(this.buttonAfficher_Click);

donde buttonAfficher era un componente de tipo [Button]. Esta clase tiene un campo Click definido de la siguiente manera:

  • [1]: la clase [Button]
  • [2]: sus eventos
  • [3,4]: el evento Click
  • [5]: la declaración del evento [Control.Click] [4].
    • EventHandler es un prototipo (un modelo) de método denominado delegado.
    • event es una palabra clave que restringe las funcionalidades de delegate y EventHandler: un objeto delegate tiene más funcionalidades que un objeto event.

El delegate EventHandler se define de la siguiente manera:

 

El delegate EventHandler designa un modelo de método:

  • cuyo primer parámetro es un tipo Object
  • cuyo segundo parámetro es de tipo EventArgs
  • que no devuelve ningún resultado

Un método que se ajuste al modelo definido por EventHandler podría ser el siguiente:


        private void buttonAfficher_Click(object sender, EventArgs e);

Para crear un objeto de tipo EventHandler, se procede de la siguiente manera:

EventHandler evtHandler=new EventHandler(méthode correspondant au prototype  défini par le type EventHandler);

De este modo, se podrá escribir:

EventHandler evtHandler=new EventHandler(buttonAfficher_Click);

Una variable de tipo delegate es, en realidad, una lista de referencias a métodos que se corresponden con el modelo delegate. Para añadir un nuevo método M a la variable evtHandler anterior, se utiliza la sintaxis:

evtHandler+=new EvtHandler(M);

La notación += se puede utilizar aunque evtHandler sea una lista vacía.

La instrucción:


            this.buttonAfficher.Click += new System.EventHandler(this.buttonAfficher_Click);

Añade un método de tipo EventHandler a la lista de métodos del evento buttonAfficher.Click. Cuando se produce el evento Click en el componente buttonAfficher, VB ejecuta la instrucción:

            buttonAfficher.Click(source, evt);

donde:

  • source es el componente de tipo object que origina el evento
  • evt, de tipo EventArgs, y no contiene información

Todos los métodos de firma void M(object,EventArgs) que se han asociado al evento Click mediante:


            this.buttonAfficher.Click += new System.EventHandler(M);

se invocarán con los parámetros (source, evt) transmitidos por VB.

8.2. Definir objetos delegados

La instrucción


    public delegate int Opération(int n1, int n2);

define un tipo denominado Opération que representa un prototipo de función que acepta dos enteros y devuelve un entero. Es la palabra clave delegate la que convierte a Opération en una definición de prototipo de función.

Una variable op de tipo Opération tendrá la función de almacenar una lista de funciones que se correspondan con el prototipo Opération:

int f1(int,int)
int f2(int,int)
...
int fn(int,int)

El registro de un método fi en la variable op se realiza mediante op=new Operación(fi) o, más sencillamente, mediante op=fi.. Para añadir un método fj a la lista de funciones ya registradas, se escribe op+= fj. Para eliminar un método fk ya registrado, se escribe op-=fk. Si en nuestro ejemplo escribimos n=op(n1,n2), se ejecutarán todos los métodos registrados en la variable op con los parámetros n1 y n2. El resultado n obtenido será el del último método ejecutado. No es posible obtener los resultados generados por todos los métodos. Por este motivo, si se almacena una lista de métodos en una función delegada, estos suelen devolver un resultado de tipo void.

Consideremos el siguiente ejemplo:


using System;
namespace Chap6 {
    class Class1 {
        // definición de un prototipo de función
        // acepta dos enteros como parámetros y devuelve un entero
        public delegate int Opération(int n1, int n2);

        // dos métodos de instancia correspondientes al prototipo
        public int Ajouter(int n1, int n2) {
            Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
            return n1 + n2;
        }//añadir

        public int Soustraire(int n1, int n2) {
            Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")");
            return n1 - n2;
        }//restar

        //: un método estático correspondiente al prototipo
        public static int Augmenter(int n1, int n2) {
            Console.WriteLine("Augmenter(" + n1 + "," + n2 + ")");
            return n1 + 2 * n2;
        }//aumentar

        static void Main(string[] args) {

            // se define un objeto de tipo «operación» para registrar funciones en él
            // se registra la función estática «aumentar»
            Opération op = Augmenter;
            // se ejecuta el delegado
            int n = op(4, 7);
            Console.WriteLine("n=" + n);

            // creación de un objeto c1 de tipo class1
            Class1 c1 = new Class1();
            // se registra en el delegado el método «añadir» de c1
            op = c1.Ajouter;
            // ejecución del objeto delegado
            n = op(2, 3);
            Console.WriteLine("n=" + n);
            // se registra en el delegado el método «restar» de c1
            op = c1.Soustraire;
            n = op(2, 3);
            Console.WriteLine("n=" + n);
            //: registro de dos funciones en el delegado
            op = c1.Ajouter;
            op += c1.Soustraire;
            // ejecución del objeto delegado
            op(0, 0);
            // se elimina una función del delegado
            op -= c1.Soustraire;
            // se ejecuta el delegado
            op(1, 1);
        }
    }
}
  • línea 3: define una clase Class1.
  • línea 6: definición de delegate Opération: un prototipo de métodos que aceptan dos parámetros de tipo int y devuelven un resultado de tipo int
  • líneas 9-12: el método de instancia Ajouter tiene la firma del delegado Opération.
  • líneas 14-17: el método de instancia Soustraire tiene la firma del delegado Opération.
  • líneas 20-23: el método de clase Augmenter tiene la firma del delegado Opération.
  • línea 25: se ejecuta el método Main
  • línea 20: la variable op es de tipo delegado Operación. Contendrá una lista de métodos con la firma del tipo delegado Operación. Se le asigna una primera referencia de método, la del método estático Class1.Augmenter.
  • línea 31: se ejecuta delegate op: se ejecutarán todos los métodos a los que hace referencia op. Se ejecutarán con los parámetros pasados a delegate y op.. En este caso, solo se ejecutará el método estático Class1.Augmenter.
  • línea 35: se crea una instancia c1 de la clase Class1.
  • línea 37: el método de instancia c1.Ajouter se asigna al delegado op. Augmenter era un método estático, Ajouter es un método de instancia. Se ha querido demostrar que esto no tiene importancia.
  • Línea 39: se ejecuta delegate op: el método Ajouter se ejecutará con los parámetros pasados a delegate op.
  • línea 42: se repite el mismo proceso con el método de instancia Soustraire.
  • Líneas 46-47: se colocan los métodos Ajouter y Soustraire en delegate y op.
  • Línea 49: se ejecuta delegate op: los dos métodos Ajouter y Soustraire se ejecutarán con los parámetros pasados a delegate y op.
  • línea 51: el método Soustraire se elimina de delegate y op.
  • línea 53: se ejecuta el delegate op: se va a ejecutar el método restante Ajouter.

Los resultados de la ejecución son los siguientes:

1
2
3
4
5
6
7
8
9
Augmenter(4,7)
n=18
Ajouter(2,3)
n=5
Soustraire(2,3)
n=-1
Ajouter(0,0)
Soustraire(0,0)
Ajouter(1,1)

8.3. ¿Delegados o interfaces?

Los conceptos de delegates e interfaces pueden parecer bastante similares y cabe preguntarse cuáles son exactamente las diferencias entre ambos. Tomemos el siguiente ejemplo, similar a uno ya estudiado:


using System;
namespace Chap6 {
    class Program1 {
        // definición de un prototipo de función
        // acepta dos enteros como parámetros y devuelve un entero
        public delegate int Opération(int n1, int n2);

        // dos métodos de instancia que se corresponden con el prototipo
        public static int Ajouter(int n1, int n2) {
            Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
            return n1 + n2;
        }//añadir

        public static int Soustraire(int n1, int n2) {
            Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")");
            return n1 - n2;
        }//restar

        // Ejecución de un delegado
        public static int Execute(Opération op, int n1, int n2){
            return op(n1, n2);
        }

        static void Main(string[] args) {
            // ejecución del delegado «Añadir»
            Console.WriteLine(Execute(Ajouter, 2, 3));
            // ejecución del delegado Restar
            Console.WriteLine(Execute(Soustraire, 2, 3));
            // ejecución de un delegado multicast
            Opération op = Ajouter;
            op += Soustraire;
            Console.WriteLine(Execute(op, 2, 3));
            // Se elimina una función del delegado
            op -= Soustraire;
            // ejecución del delegado
            Console.WriteLine(Execute(op, 2, 3));
        }
    }
}

En la línea 20, el método Execute espera una referencia a un objeto del tipo delegado «Opération», definido en la línea 6. Esto permite pasar al método Execute diferentes métodos (líneas 26, 28, 32 y 36). Esta propiedad de polimorfismo también se puede conseguir con una interfaz:


using System;

namespace Chap6 {

    // interfaz IOperation
    public interface IOperation {
        int operation(int n1, int n2);
    }

    // clase Añadir
    public class Ajouter : IOperation {
        public int operation(int n1, int n2) {
            Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
            return n1 + n2;
        }
    }

    // clase Restar
    public class Soustraire : IOperation {
        public int operation(int n1, int n2) {
            Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")");
            return n1 - n2;
        }
    }

    // clase de prueba
    public static class Program2 {
        // Ejecución del único método de la interfaz IOperation
        public static int Execute(IOperation op, int n1, int n2) {
            return op.operation(n1, n2);
        }

        public static void Main() {
            // ejecución del delegado «Sumar»
            Console.WriteLine(Execute(new Ajouter(), 2, 3));
            // Ejecución del delegado «Restar»
            Console.WriteLine(Execute(new Soustraire(), 2, 3));
        }
    }
}
  • líneas 6-8: la interfaz [IOperation] define un método operation.
  • líneas 11-16 y 19-24: las clases [Ajouter] y [Soustraire] implementan la interfaz [IOperation].
  • líneas 29-31: el método Execute, cuyo primer parámetro es del tipo de la interfaz IOperation. El método Execute recibirá sucesivamente como primer parámetro una instancia de la clase Ajouter y, a continuación, una instancia de la clase Soustraire.

Aquí se aprecia claramente el carácter polimórfico que tenía el parámetro de tipo delegate del ejemplo anterior. Ambos ejemplos muestran, al mismo tiempo, las diferencias entre estos dos conceptos.

Los tipos delegate y interface son intercambiables

  • si la interfaz solo tiene un método. De hecho, el tipo delegate es una envoltura para un único método, mientras que la interfaz puede definir varios métodos.
  • si no se utiliza el aspecto de multidifusión de delegate. De hecho, este concepto de multidifusión no existe en la interfaz.

Si se cumplen estas dos condiciones, se puede elegir entre las dos firmas siguientes para el método Execute:


int Execute(IOperation op, int n1, int n2)
int Execute(Opération op, int n1, int n2)

La segunda, que utiliza el delegate, puede resultar más flexible de usar. De hecho, en la primera firma, el primer parámetro del método debe implementar la interfaz IOperation. Esto obliga a crear una clase para definir en ella el método que se va a pasar como primer parámetro al método Execute. En la segunda firma, cualquier método existente que tenga la firma correcta sirve. No es necesario realizar ninguna construcción adicional.

8.4. Gestión de eventos

Los objetos delegate pueden utilizarse para definir eventos. Una clase C1 puede definir un evento evt de la siguiente manera:

  • se define un tipo delegate dentro o fuera de la clase C1:
delegate TResult Evt(T1 param1, T2 param2, ...);
  • la clase C1 define un campo de tipo delegate Evt:
public Evt Evt1;
  • cuando una instancia c1 de la clase C1 quiera notificar un evento, ejecutará su delegado Evt1 pasándole los parámetros definidos por el delegado Evt. Todos los métodos registrados en delegate y Evt1 se ejecutarán entonces con estos parámetros. Se puede decir que han sido notificados del evento Evt1.
  • Si un objeto c2 que utiliza un objeto c1 desea ser notificado de la ocurrencia del evento Evt1 en el objeto c1, registrará uno de sus métodos, c2.M, en el objeto delegado c1.Evt1 del objeto c1.. De este modo, su método c2.M se ejecutará cada vez que elevento Evt1 se produzca en el objeto c1. También podrá darse de baja cuando ya no desee recibir notificaciones sobre el evento.
  • Dado que el objeto delegado c1.Evt1 puede registrar varios métodos, diferentes objetos ci podrán registrarse en el delegado c1.Evt1 para recibir notificaciones del evento Evt1 en c1.

En este escenario, tenemos:

  • una clase que notifica un evento
  • unas clases a las que se notifica dicho evento. Se dice que se suscriben al evento.
  • un tipo delegate que define la firma de los métodos a los que se notificará el evento

El marco .NET define:

  • una firma estándar del delegate de un evento
public delegate void MyEventHandler(object source, EventArgs evtInfo);
  • source: el objeto que ha notificado el evento
  • evtInfo: un objeto de tipo EventArgs o derivado que aporta información sobre el evento
  • el nombre del delegate debe terminar en EventHandler
  • una forma estándar de declarar un evento de tipo MyEventHandler en una clase:
1
2
3
4
public Class C1{
    public event MyEventHandler Evt1;
...
}

El campo Evt1 es de tipo delegate. La palabra clave event sirve para restringir las operaciones que se pueden realizar sobre él:

  • desde fuera de la clase C1, solo son posibles las operaciones += y -=. Esto impide la eliminación (por ejemplo, por error del desarrollador) de los métodos suscritos al evento. Solo se puede suscribirse (+=) o darse de baja (-=) del evento.
  • Solo una instancia de tipo C1 puede ejecutar la llamada Evt1(source,evtInfo), que desencadena la ejecución de los métodos suscritos al evento Evt1.

El marco .NET proporciona un método genérico que cumple con la firma de delegate de un evento:

public delegate void EventHandler<TEventArgs>(object source, TEventArgs evtInfo) where TEventArgs : EventArgs
  • el delegate EventHandler utiliza el tipo genérico TEventArgs, que es el tipo de su segundo parámetro
  • el tipo TEventArgs debe derivarse del tipo EventsArgs (donde TEventArgs: EventArgs)

Con este tipo genérico delegate, la declaración de un evento X en la clase C seguirá el siguiente esquema recomendado:

  • definir un tipo XEventArgs derivado de EventArgs para encapsular la información sobre el evento X
  • definir en la clase C un evento de tipo EventHandler<XEventArgs>.
  • definir en la clase C un método protegido
protected void OnXHandler(XEventArgs e);

destinado a «publicar» el evento X a los suscriptores.

Consideremos el siguiente ejemplo:

  • una clase Emetteur encapsula una temperatura. Se monitoriza dicha temperatura. Cuando esta temperatura supera un umbral determinado, debe lanzarse un evento. Llamaremos a este evento TemperatureTropHaute. La información sobre este evento se encapsulará en un tipo TemperatureTropHauteEventArgs.
  • Una clase Souscripteur se suscribe al evento anterior. Cuando recibe la notificación del evento, muestra un mensaje en la consola.
  • Un programa de consola crea un emisor y dos suscriptores. Introduce las temperaturas mediante el teclado y las registra en una instancia de tipo Emetteur. Si esta es demasiado alta, la instancia de tipo Emetteur publica el evento TemperatureTropHaute.

Para cumplir con el método recomendado de gestión de eventos, definimos en primer lugar el tipo TemperatureTropHauteEventArgs para encapsular la información sobre el evento:


using System;

namespace Chap6 {
    public class TemperatureTropHauteEventArgs:EventArgs {
        // Temperatura en el momento del evento
        public decimal Temperature { get; set; }
        // fabricantes
        public TemperatureTropHauteEventArgs() {
        }
        public TemperatureTropHauteEventArgs(decimal temperature) {
            Temperature = temperature;
        }
    }
}
  • línea 6: la información encapsulada por la clase TemperatureTropHauteEventArgs es la temperatura que provocó el evento TemperatureTropHaute.

La clase Emetteur es la siguiente:


using System;

namespace Chap6 {
    public class Emetteur {
        static decimal SEUIL = 19;

        // temperatura observada
        private decimal temperature;
        // nombre de la fuente
        public string Nom { get; set; }
        // evento notificado
        public event EventHandler<TemperatureTropHauteEventArgs> TemperatureTropHaute;

        // lectura/escritura de temperatura
        public decimal Temperature {
            get {
                return temperature;
            }
            set {
                temperature = value;
                if (temperature > SEUIL) {
                    // se notifica el evento a los abonados
                    OnTemperatureTropHaute(new TemperatureTropHauteEventArgs(temperature));
                }
            }
        }

        // notificación de un evento
        protected virtual void OnTemperatureTropHaute(TemperatureTropHauteEventArgs evt) {
            // Envío del evento TemperatureTropHaute a los suscriptores
            TemperatureTropHaute(this, evt);
        }
    }
}
  • línea 5: el umbral de temperatura a partir del cual se publicará el evento TemperatureTropHaute.
  • línea 10: el emisor tiene un nombre para su identificación
  • línea 12: el evento TemperatureTropHaute.
  • Líneas 15-26: el método get, que devuelve la temperatura, y el método set, que la registra. Es el método set el que publica el evento TemperatureTropHaute si la temperatura que se va a registrar supera el umbral de la línea 5. Este método hace que se publique el evento mediante el método OnTemperatureTropHauteHandler de la línea 29, pasándole como parámetro un objeto TemperatureTropHauteEventArgs en el que se ha registrado la temperatura que ha superado el umbral.
  • líneas 29-32: se publica el evento TemperatureTropHaute con el propio emisor como primer parámetro y el objeto TemperatureTropHauteEventArgs recibido como parámetro como segundo parámetro.

La clase Souscripteur, que se suscribirá al evento TemperatureTropHaute, es la siguiente:


using System;

namespace Chap6 {
    public class Souscripteur {
        // nombre
        public string Nom { get; set; }

        // administrador del evento TemperatureTropHaute
        public void EvtTemperatureTropHaute(object source, TemperatureTropHauteEventArgs e) {
            // visualización en la consola del operador
            Console.WriteLine("Souscripteur [{0}] : la source [{1}] a signalé une température trop haute : [{2}]", Nom, ((Emetteur)source).Nom, e.Temperature);
        }
    }
}
  • línea 6: cada suscriptor se identifica mediante un nombre.
  • líneas 9-12: el método que se asociará al evento TemperatureTropHaute. Tiene la firma de tipo delegate EventHandler<TEventArgs> que debe tener un gestor de eventos. El método muestra en la consola: el nombre del suscriptor que muestra el mensaje, el nombre del emisor que ha notificado el evento y la temperatura que lo ha desencadenado.
  • La suscripción al evento TemperatureTropHaute de un objeto Emetteur no se realiza en la clase Souscripteur. La llevará a cabo una clase externa.

El programa [Program.cs] vincula todos estos elementos entre sí:


using System;
namespace Chap6 {
    class Program {
        static void Main(string[] args) {
            // creación de un emisor de eventos
            Emetteur e1 = new Emetteur() { Nom = "e" };
            // creación de una tabla con 2 suscriptores
            Souscripteur[] souscripteurs = new Souscripteur[2];
            for (int i = 0; i < souscripteurs.Length; i++) {
                // creación de un suscriptor
                souscripteurs[i] = new Souscripteur() { Nom = "s" + i };
                // se le suscribe al evento TemperatureTropHaute de e1
                e1.TemperatureTropHaute += souscripteurs[i].EvtTemperatureTropHaute;
            }
            // se leen las temperaturas desde el teclado
            decimal temperature;
            Console.Write("Température (rien pour arrêter) : ");
            string saisie = Console.ReadLine().Trim();
            // siempre que la línea introducida no esté vacía
            while (saisie != "") {
                // ¿Es la entrada un número decimal?
                if (decimal.TryParse(saisie, out temperature)) {
                    // temperatura correcta: se guarda
                    e1.Temperature = temperature;
                } else {
                    // se indica el error
                    Console.WriteLine("Température incorrecte");
                }
                // nueva entrada
                Console.Write("Température (rien pour arrêter) : ");
                saisie = Console.ReadLine().Trim();
            }//while
        }
    }
}
  • línea 6: creación del emisor
  • líneas 8-14: creación de dos suscriptores que se suscriben al evento TemperatureTropHaute del emisor.
  • líneas 20-32: bucle de introducción de temperaturas mediante el teclado
  • línea 24: si la temperatura introducida es correcta, se transmite al objeto Emetteur e1, que activará el evento TemperatureTropHaute si la temperatura es superior a 19 °C.

Los resultados de la ejecución son los siguientes:

1
2
3
4
Température (rien pour arrêter) : 17
Température (rien pour arrêter) : 21
Souscripteur [s0] : la source [e] a signalé une température trop haute : [21]
Souscripteur [s1] : la source [e] a signalé une température trop haute : [21]