1. Escenarios prácticos de uso de eventos
Los eventos no son solo teoría bonita de los libros. En la práctica son necesarios casi en cada segunda aplicación, y si trabajas con UI o servicios de red — en la mayoría de los casos.
Vamos a ver algunos escenarios típicos de desarrollo real. Al mismo tiempo miraremos cómo los estándares y buenas prácticas ayudan a evitar errores comunes.
Reaccionar a acciones del usuario (como en la UI)
Probablemente el escenario más frecuente — cuando el usuario hace algo (pulsa un botón, selecciona un elemento en una lista) y la aplicación debe reaccionar. Así funcionan Windows Forms, WPF, UWP, Avalonia, MAUI y otros frameworks de UI.
Mini-ejemplo (sin UI — simulamos un botón):
public class Button
{
public event EventHandler? Click; // estándar delegate
public void SimulateClick()
{
Click?.Invoke(this, EventArgs.Empty); // el "Botón" fue "pulsado"
}
}
class Program
{
static void Main()
{
var button = new Button();
button.Click += (sender, e) => Console.WriteLine("¡Botón pulsado!");
button.SimulateClick(); // > ¡Botón pulsado!
}
}
En un framework de UI real el evento Click se dispara cuando el usuario hace clic con el ratón o toca la pantalla. Todo lo que se haya suscrito a ese evento recibirá la notificación — así funciona la lógica de las aplicaciones.
Señalizar progreso, finalización de operaciones y errores
Imagínate: descarga de un archivo, procesamiento de datos, un cálculo largo. Hay que informar sobre el progreso o los errores. A menudo se organiza un evento para cada "paso de progreso" y otro evento para "completado" o "error".
Ejemplo de un descargador de archivos:
public class FileDownloader
{
public event EventHandler<int>? ProgressChanged;
public event EventHandler? DownloadCompleted;
public event EventHandler<string>? DownloadFailed;
public void Download()
{
for (int i = 1; i <= 100; i += 10)
{
Thread.Sleep(50); // simulación de retardo
ProgressChanged?.Invoke(this, i);
}
DownloadCompleted?.Invoke(this, EventArgs.Empty);
}
}
var downloader = new FileDownloader();
downloader.ProgressChanged += (s, progress) => Console.WriteLine($"Descarga: {progress}%");
downloader.DownloadCompleted += (s, e) => Console.WriteLine("Descarga completada.");
downloader.Download();
Aquí hay varios eventos: puedes suscribirte a todo (o solo a lo que necesites). Esto es parecido a cómo funcionan muchas clases estándar de .NET para hilos, descargas de archivos, trabajo con HTTP, etc.
Reacción al ciclo de vida de objetos (por ejemplo, "guardado", "eliminado")
Supongamos que tienes un modelo de dominio. Quieres que, cuando el usuario guarde o elimine un objeto, algunos procesos reaccionen: actualicen caché, hagan logging, envíen un mensaje, etc.
public class UserRepository
{
public event EventHandler<UserEventArgs>? UserSaved;
public event EventHandler<UserEventArgs>? UserDeleted;
public void Save(User user)
{
//... guardamos el usuario
UserSaved?.Invoke(this, new UserEventArgs(user));
}
public void Delete(User user)
{
//... eliminamos el usuario
UserDeleted?.Invoke(this, new UserEventArgs(user));
}
}
public class UserEventArgs : EventArgs
{
public User User { get; }
public UserEventArgs(User user) => User = user;
}
Ahora diferentes módulos pueden suscribirse y —sin acoplamientos directos— recibir notificaciones sobre cambios en los usuarios. Además, el repositorio no sabe quién "se conectó".
Eventos asíncronos y multihilo
A veces los eventos se generan fuera del hilo principal (UI) — por ejemplo, desde tareas en background, timers o operaciones asíncronas. En esos casos es importante recordar que el código de los manejadores se ejecutará en un hilo "ajeno". Si un manejador intenta actualizar elementos de la interfaz directamente desde otro hilo — provocará errores.
¿Qué es el marshaling? El marshaling es transferir la ejecución de código de un hilo a otro, normalmente de un hilo en background de vuelta al hilo de UI, para actualizar la interfaz de forma segura. En aplicaciones UI (WinForms, WPF) se usan mecanismos como SynchronizationContext o Dispatcher, que permiten "redirigir" la llamada del manejador al hilo correcto.
No hagas esto:
// El evento se invoca desde un hilo en background, y el manejador actualiza la UI directamente — obtendrás una excepción!
Se recomienda: comprobar el hilo de ejecución y, si es necesario, hacer marshaling al hilo de UI (vía SynchronizationContext o Dispatcher). En aplicaciones de consola y servidor normalmente no hay tales restricciones.
Event Aggregator / Messaging
En aplicaciones grandes a menudo se usa un agregador centralizado de eventos (Event Aggregator). Reduce el acoplamiento: suscriptores y publicadores no se conocen y se comunican a través de un hub.
public class EventAggregator
{
public event EventHandler<SomeEventArgs>? SomeEvent;
public void Publish(SomeEventArgs args)
{
SomeEvent?.Invoke(this, args);
}
}
2. Mejores patrones y prácticas
Usa el atributo [CallerMemberName] para errores
Si estás escribiendo código de infraestructura (por ejemplo, loggers, tracers), registra el nombre del método origen del evento usando el atributo CallerMemberName — eso facilita el diagnóstico.
Procura que los eventos sean thread-safe, si hace falta
Los eventos son multicast, y las llamadas desde distintos hilos requieren cuidado: antes de invocar copia el delegado a una variable local.
var handler = MyEvent;
if (handler != null) handler(this, args);
En C# 6+ usa la llamada segura: MyEvent?.Invoke(this, args).
Haz los EventArgs personalizados inmutables
Define tus tipos de EventArgs con propiedades solo de lectura — así evitas modificaciones accidentales del estado del evento por parte de los suscriptores.
public class WorkCompletedEventArgs : EventArgs
{
public string Message { get; }
public WorkCompletedEventArgs(string message) => Message = message;
}
public o private event
Haz los eventos public event, y encapsula la invocación en un método protected virtual OnEventName. Esto da extensibilidad (una clase derivada puede sobreescribir) y los consumidores externos no podrán invocar el evento directamente.
No rompas la secuencia del ciclo de vida
Si inicias una cadena larga de manejadores, recuerda: alguien puede suscribirse y tratar de cambiar la lógica interna. Documenta dónde y cuándo se invocan eventos, y si un evento puede dispararse varias veces.
Eventos múltiples y composición
Cuando un objeto escucha varias fuentes, agrupa la suscripción y la desuscripción en un mismo lugar (por ejemplo, métodos Subscribe/Unsubscribe). Si hace falta, guarda campos privados-delegados para facilitar la desuscripción.
3. Cómo NO usar eventos: errores típicos
Error nº1: ignorar el patrón estándar de eventos.
Si cada clase declara sus propios delegates para cada evento, eso se vuelve un caos rápido. Procura usar los delegados estándar EventHandler o EventHandler<T>, donde T hereda de EventArgs. Ese enfoque hace el código más claro y familiar para desarrolladores .NET.
Error nº2: invocar el evento incorrectamente desde código externo.
Nunca invoques un evento directamente fuera de la clase-publicadora. El evento debe iniciarse solo dentro de la clase, normalmente a través de un método protegido OnEventName. Los suscriptores solo tienen derecho a suscribirse (+=) y desuscribirse (-=).
Mal:
foo.MyEvent(); // ¡error! No puedes invocar el evento desde fuera directamente
Bien:
// Solo dentro de foo:
// protected virtual void OnMyEvent()
// {
// MyEvent?.Invoke(this, EventArgs.Empty);
// }
Error nº3: invocar el evento sin comprobar si hay suscriptores (null).
Si intentas invocar un evento que no tiene suscriptores, obtendrás un NullReferenceException. En C# 6.0+ usa ?.Invoke(...) — el evento solo se llamará si hay suscriptores.
Error nº4: olvidar desuscribirse del evento (fuga de memoria).
Si un objeto se ha suscrito a un evento pero no se desuscribe antes de ser destruido, el publicador mantiene la referencia al suscriptor. El garbage collector no podrá liberar la memoria, lo cual es crítico cuando los publicadores viven mucho y los suscriptores son de corta vida (por ejemplo, ViewModel, formularios). La mejor solución es la desuscripción explícita o usar referencias débiles (WeakReference) donde tenga sentido.
Error nº5: invocar el evento fuera de un método protegido y virtual OnEvent.
Al invocar el evento directamente privas a las clases derivadas de la posibilidad de sobreescribir el comportamiento correctamente. El patrón correcto es declarar un método protected virtual que invoque el evento.
Ejemplo estándar:
protected virtual void OnSomething(EventArgs e)
{
Something?.Invoke(this, e);
}
GO TO FULL VERSION