Definición y manejo de eventos en Visual Basic .NET (VB.NET)

En VB.NET (y .NET en general), un evento es un mecanismo de notificación: un objeto (publicador) avisa a otros objetos (suscriptores) de que "algo ha ocurrido" (clic, tecla, timer, dato cambiado, error, finalización async). Permite programación reactiva y desacoplada.

Conceptos clave

  • Publicador (Publisher): Clase que expone el evento (ej. Button, Timer, tu clase Factura).
  • Suscriptor (Subscriber): Clase que maneja el evento (ej. Formulario, Controlador, Servicio).
  • Delegado (Delegate): Tipo que define la firma del manejador (parámetros y tipo retorno). .NET usa EventHandler (sender, e) o EventHandler(Of TEventArgs).
  • Argumentos del evento (EventArgs): Clase que transporta datos del evento. Hereda de EventArgs. Ej: MouseEventArgs (X, Y, Button), ElapsedEventArgs (SignalTime).
  • Manejador (Handler): Sub/Function con firma compatible al delegado. Ejecuta la lógica de respuesta.
  • Suscripción (Wire-up): Conectar evento con manejador: AddHandler obj.Evento, AddressOf MiManejador (dinámico) o Handles obj.Evento (estático, en tiempo diseño, WithEvents).
  • Desuscripción: RemoveHandler obj.Evento, AddressOf MiManejador. Vital para evitar memory leaks (referencias fuertes impiden GC).

Eventos predefinidos comunes (WinForms / WPF / ASP.NET)

  • UI: Click, DoubleClick, MouseDown/Up/Move/Enter/Leave/Hover/Wheel, KeyDown/Up/Press, TextChanged, Validating/Validated, GotFocus/LostFocus, Load/Shown/Closing/FormClosing, Paint/DrawItem.
  • Componentes: Timer.Tick, SerialPort.DataReceived, FileSystemWatcher.Created/Changed/Deleted/Renamed, BackgroundWorker.DoWork/ProgressChanged/RunWorkerCompleted, HttpClient (async events), Socket.Received.
  • Datos: BindingSource.CurrentChanged, DataTable.RowChanged/RowDeleting/ColumnChanging, EntityFramework SavedChanges.

Declarar evento personalizado (En tu clase)

Public Class Factura
    ' 1. Definir EventArgs personalizado (opcional pero recomendado)
    Public Class FacturaEventArgs
        Inherits EventArgs
        Public Property FacturaId As Integer
        Public Property Total As Decimal
        Public Property Fecha As DateTime
        Public Sub New(id As Integer, total As Decimal, fecha As DateTime)
            Me.FacturaId = id : Me.Total = total : Me.Fecha = fecha
        End Sub
    End Class

    ' 2. Declarar el evento (usa EventHandler(Of T) estándar)
    Public Event FacturaCreada As EventHandler(Of FacturaEventArgs)

    ' 3. Método protegido para lanzar (Raise) el evento (patrón estándar)
    Protected Overridable Sub OnFacturaCreada(e As FacturaEventArgs)
        RaiseEvent FacturaCreada(Me, e) ' Notifica a suscriptores
    End Sub

    ' 4. Lógica de negocio que dispara el evento
    Public Sub CrearNueva(clienteId As Integer, items As List(Of Item))
        ' ... lógica crear factura, calcular total ...
        Dim nuevaFacturaId = 123
        Dim total = 99.99D
        ' 5. LANZAR (RAISE) EL EVENTO
        OnFacturaCreada(New FacturaEventArgs(nuevaFacturaId, total, DateTime.Now))
    End Sub
End Class

Suscribirse al evento (En Form, Servicio, etc.)

Public Class FacturaForm
    Private WithEvents miFactura As New Factura() ' WithEvents permite Handles

    ' Suscripción ESTÁTICA (En tiempo diseño, requiere WithEvents)
    Private Sub miFactura_FacturaCreada(sender As Object, e As Factura.FacturaEventArgs) Handles miFactura.FacturaCreada
        MessageBox.Show($"Factura {e.FacturaId} creada: {e.Total:C} el {e.Fecha:G}")
        ActualizarGrid()
        EnviarEmailCliente(e.FacturaId)
    End Sub

    ' Suscripción DINÁMICA (En tiempo ejecución, SIN WithEvents)
    Private Sub SuscribirOtraFactura(f As Factura)
        AddHandler f.FacturaCreada, AddressOf ManejadorGenericoFactura
    End Sub

    Private Sub ManejadorGenericoFactura(sender As Object, e As Factura.FacturaEventArgs)
        Log.Info($"Factura {e.FacturaId} total {e.Total}")
    End Sub

    ' LIMPIEZA: Desuscribir al cerrar/destruir
    Private Sub FacturaForm_FormClosing(sender As Object, e As FormClosingEventArgs) Handles Me.FormClosing
        RemoveHandler miFactura.FacturaCreada, AddressOf miFactura_FacturaCreada
        ' Si suscripciones dinámicas, guárdalas en lista y RemoveHandler todas aquí.
    End Sub
End Class

Patrón Event-Based Asynchronous (EAP) / Task-based (TAP) moderno

Para operaciones largas, usa Async/Await + Task en lugar de eventos BackgroundWorker (legacy). Los eventos siguen útiles para progreso (IProgress(Of T)), notificaciones de estado, UI, dominio (Domain Events).

Buenas prácticas

  • Nombra eventos en pasado (FacturaCreada, UsuarioIniciadoSesion, ArchivoProcesado).
  • Usa EventHandler(Of TEventArgs) estándar. Evita delegados custom.
  • sender = instancia que lanza (Me). e = datos.
  • Thread Safety: Si evento se lanza desde hilo distinto al UI, usa Invoke / BeginInvoke (WinForms) / Dispatcher.Invoke (WPF) / SynchronizationContext en manejador para tocar UI.
  • Memory Leaks: SIEMPRE RemoveHandler / -= en Dispose, FormClosing, Unloaded. WeakEventManager (WPF) / WeakReference para suscripciones longevas.
  • Documenta EventArgs con XML comments (''').