Una de las cosas que más me ha llamado la atención en el cambio de VB a C# ha sido la diferencia de cómo tratan ambos lenguajes el tema de los eventos. Me imagino que estaba demasiado acostumbrado a crear eventos en VB y, sencillamente, no contemplaba ya otras opciones. De hecho, antes del .Net, en VB 6.0, ya creábamos los eventos en aquellas “maravillosas clases”:
VB. 6.0
Ejemplo de clase de VB 6.0 con un evento definido:
' -------------------------------------------------------------------------- ' PruebasContador ' Clase de pruebas de eventos ' ' alskare, Jul/09 ' -------------------------------------------------------------------------- Option Explicit ' Declaración del evento público de pruebas Public Event MiEvento(ByVal NumeroActual As Integer) Private contador As Integer ' Rutina que realiza una cuenta y dispara el evento Public Sub Inicio() contador = 0 Dim i As Integer For i = 1 To 1000 If i Mod 10 = 0 Then RaiseEvent MiEvento(i) End If Next i End Sub
Con algo tan simple como esto (la verdad es ahora lo veo simple, pero reconozco que me costó lo suyo entenderlo en su día), era frecuente añadir la rutina que capturaba el evento en algún que otro formulario, más o menos como el ejemplo que incluyo a continuación:
' -------------------------------------------------------------------------- ' Form: EjecutaPruebas ' Formulario de ejecución de las pruebas ' ' alskare, Jul/09 ' -------------------------------------------------------------------------- Option Explicit ' Declaración de la clase Private WithEvents c As PruebasContador Private Sub Form_Load() ' Instancia de la clase Set c = New PruebasContador ' Ejecución del módulo de la clase que inicia el contador y dispara el ' evento c.Inicio End Sub ' Captura del evento. En el ejemplo sólo muestra el resultado en la ventana ' de inmediato. Private Sub c_MiEvento(ByVal NumeroActual As Integer) Debug.Print NumeroActual End Sub
Bueno, el caso es que, acostumbrado a esto, no noté una gran diferencia en VB.Net y enseguida me acostumbré a hacer uso de las facilidades que me suministraba el .Net Framework como el usar clases como parámetros en los eventos.
VB.NET
Así, una hipotética adaptación de VB6 a VB.Net del ejemplo del contador anterior podía haber sido algo como lo siguiente:
' ----------------------------------------------------------------------------- ' Pruebas de eventos ' ' alskare, Jul/09 ' ----------------------------------------------------------------------------- Option Explicit On Option Strict On Namespace jnSoftware.Pruebas.Eventos ''' <summary> ''' ContadorEvents. Clase que se usará como parámetro en el disparador. ''' La clase contiene la propiedad Contador que incluirá el valor a "traspasar" entre ''' la clase disparadora y la clase receptora ''' </summary> Public Class ContadorEvents Inherits EventArgs Private _contador As Integer Public Property Contador() As Integer Get Return _contador End Get Set(ByVal value As Integer) _contador = value End Set End Property End Class ''' <summary> ''' PruebasContador. Al igual que en el ejemplo de VB6, sólo contiene un método que ''' dispara el evento ''' </summary> Public Class PruebasContador ' Declaración del evento público de pruebas Public Event MiEvento(ByVal sender As Object, ByVal e As ContadorEvents) Private _contador As Integer = 0 Private _campoContadorEvents As ContadorEvents = New ContadorEvents() Public Sub Inicio() For i As Integer = 1 To 1000 If i Mod 10 = 0 Then _campoContadorEvents.Contador = i RaiseEvent MiEvento(Me, _campoContadorEvents) End If Next End Sub End Class ''' <summary> ''' Clase que ejecuta las pruebas. ''' </summary> ''' <remarks></remarks> Public Class PruebasEventosVB Private Shared WithEvents c As New PruebasContador Public Shared Sub Main() c.Inicio() Console.ReadKey() End Sub ' Cada vez que se dispara el evento en la clase PruebasContador, se ejecuta este método pudiendo leer el ' valor del contador gracias al parámetro del contador e.Contador Private Shared Sub MuestraProgreso(ByVal sender As Object, ByVal e As ContadorEvents) Handles c.MiEvento Console.WriteLine("Número {0}", e.Contador) End Sub End Class End Namespace
He intentado seguir la misma nomenclatura que la usada en el ejemplo de VB6 para poder realizar las comparaciones. Es de destacar el método MuestraProgreso, que queda “vinculado” al disparador gracias a la cláusula Handles que se incluye después de la firma estándar. La cláusula Handles es de lo que me llamó más la atención puesto que podía vincularse un mismo método a los eventos de varios objetos, siempre que incluyesen la misma firma (ideal para capturar eventos de controles de formularios).
La vinculación del evento al método podríamos haberla creado de modo dinámico sin incluir la cláusula Handles. Esto me ha sido muy práctico cuando se trata de crear controles dinámicamente o cuando he tenido algún que otro problema de rendimiento al dispararse demasiadas veces algún evento. Así, una modificación que podríamos realizar en la clase PruebasEventos para que se vincule el evento de un modo dinámico sería algo así como:
Public Class PruebasEventosVB Private Shared WithEvents c As New PruebasContador Public Shared Sub Main() ' Vinculación del evento al módulo MuestraProgreso AddHandler c.MiEvento, AddressOf MuestraProgreso c.Inicio() ' Casi del mismo modo se podría desvincular RemoveHandler c.MiEvento, AddressOf MuestraProgreso Console.ReadKey() End Sub Private Shared Sub MuestraProgreso(ByVal sender As Object, ByVal e As ContadorEvents) Console.WriteLine("Número {0}", e.Contador) End Sub End Class
Como podemos apreciar, entre VB 6 y VB.Net no existen grandes diferencias. Tan sólo es cuestión de ir habituándose a las nuevas características que nos ofreció .Net. No obstante, el día que me empeciné en empezar a crear las aplicaciones en C#, la cosa no ha sido tan fácil.
C#
Para empezar, resulta que no existe la cláusula Handles de VB ni, que yo sepa, nada que se le parezca, así que habrá que hacer uso de una vinculación dinámica al igual que se ha creado en el último listado. De hecho, en este último listado, al hacer uso de AddHandler, sin saberlo ya hemos estado empleando algo nuevo para aquellos que nos hemos movido siempre en VB: los delegados. Aunque cueste un poco el entendimiento de este concepto, yo he creído entender que un delegado es una función anónima que, sencillamente, tiene la dirección de memoria de la función. Pues eso, que ya en VB hemos usado los delegados pero, como dice el maestro Guille, VB es muy protector con nosotros y nos protege de ciertas cosas que no necesitamos saber, o que no es obligatorio que sepamos.
El caso es que para poder crear una vinculación dinámica necesitamos que sea una función la que se encargue de “recepcionar” los datos que queremos traspasar al disparar el evento, así que, además del evento, deberemos crear un delegado para que pueda crear tal vinculación (jo, qué rollo, ¿no?). Mejor lo vemos con un ejemplo:
using System; namespace jnSoftware.Pruebas.Eventos { /// <summary> /// ContadorEvents. Clase que se usará como parámetro en el disparador /// </summary> public class ContadorEvents: EventArgs { public int Contador { get; set; } } public class PruebasContador { // Definición del delegado public delegate void MiEventoDelegate(object sender, ContadorEvents e); // Definición del evento public event MiEventoDelegate MiEvento; private int contador = 0; private ContadorEvents campoContador = new ContadorEvents(); public void Inicio() { for (int i = 0; i <= 1000; i++) { if (i % 10 == 0) { campoContador.Contador = i; if (MiEvento != null) MiEvento(this, campoContador); } } } } /// <summary> /// Clase que ejecuta las pruebas /// </summary> public class PruebasEventosC { private static PruebasContador c = new PruebasContador(); static void Main() { // Vinculación entre el evento y la función MuestraProgreso. Nos fijamos que // se hace uso del delegado definido en la clase PruebasContador c.MiEvento += new PruebasContador.MiEventoDelegate(MuestraProgreso); c.Inicio(); // Al igual que se ha hecho con anterioridad, se desvincula el evento c.MiEvento -= new PruebasContador.MiEventoDelegate(MuestraProgreso); Console.ReadKey(); } static void MuestraProgreso(object sender, ContadorEvents e) { Console.WriteLine("Número {0}", e.Contador); } } }
Como se puede apreciar, las máximas diferencias podemos encontrarlas en la clase PruebasContador, sobre todo porque, frente a VB, define el Delegado antes mencionado y, antes de lanzar el evento, realiza una comprobación para ver si existen suscripciones a MiEvento.
Bueno, ahí queda. Por supuesto, creando esta entrada no he intentado escribir nada nuevo del tema, puesto que existe, ya en internet miles de artículos que destripan mucho mejor que yo todo lo referente a los eventos, pero sí creo haber puesto mi granito de arena para todos aquellos que, como me ha pasado a mí, se han encontrado con la aparición de los delegados a la hora de pasar a programar en C#.