1. Introducción
Un novato típico puede preguntar: «¿Dónde se usan eventos en la práctica? ¿De verdad todas las interacciones entre clases se basan en eventos y no en llamadas directas a métodos?» La respuesta es simple: eventos y delegados no son magia que arregla toda la arquitectura. Pero sin ellos, una aplicación moderna rápidamente se convierte en "estrechamente acoplada" (tightly coupled), cuando cambiar un componente obliga a cambiar muchos otros. Usar eventos y delegados ayuda a crear sistemas flexibles, extensibles y fáciles de mantener.
- Programación UI (WinForms, WPF, Xamarin, MAUI): manejo de clicks, hover, entrada de texto y otras acciones del usuario.
- Operaciones asíncronas: finalización de la descarga de un archivo, obtención de datos de la red, disparo de timers.
- Arquitectura de plugins y sistemas extensibles: permite enchufar nuevos módulos sin integrar rígidamente el código principal.
- Sistemas de señales/distribución: notificar a múltiples componentes interesados sobre eventos ocurridos.
- Observación de cambios de estado («observador»): reaccionar a nuevos mensajes, cambios de datos, actualizar la interfaz.
¡Y ahora — al grano!
2. Esqueleto arquitectónico
Imaginemos que desarrollas una pequeña aplicación de consola — un "Mini-chat" para formación interna en la empresa (o simplemente para practicar). Tenemos entidades: User, Chat y, quizá, un Bot asistente. Cuando un usuario envía un mensaje, el chat debe notificar a todos los usuarios y bots conectados — para que lo muestren en pantalla o, en el caso del bot, generen una respuesta automática. Es el escenario clásico: el publicador genera un evento (event), los manejadores se suscriben al delegado EventHandler/EventHandler<TEventArgs>, y un método manejador como OnMessageReceived reacciona a la notificación.
// Clase User (suscriptor)
public class User
{
public string Name { get; }
public User(string name)
{
Name = name;
}
// Método que será el manejador del evento
public void OnMessageReceived(object? sender, MessageEventArgs e)
{
Console.WriteLine($"[{Name}] vio un nuevo mensaje: {e.MessageText}");
}
}
// Argumentos del evento
public class MessageEventArgs : EventArgs
{
public string MessageText { get; }
public MessageEventArgs(string text) => MessageText = text;
}
// Clase ChatRoom (publicador del evento)
public class ChatRoom
{
public event EventHandler<MessageEventArgs>? MessageReceived;
public void SendMessage(string text)
{
// Disparamos el evento (avisamos a todos los suscriptores)
MessageReceived?.Invoke(this, new MessageEventArgs(text));
}
}
Ejemplo de uso:
var chat = new ChatRoom();
var user1 = new User("Antonio");
var user2 = new User("María");
chat.MessageReceived += user1.OnMessageReceived;
chat.MessageReceived += user2.OnMessageReceived;
chat.SendMessage("¡Hola a todos! 😊");
En la consola veremos dos mensajes — ambos usuarios se enteraron del nuevo mensaje.
3. Suscripción y desuscripción dinámica
En la vida real un usuario puede salir del chat y ya no querer recibir mensajes. Vamos a enseñar al usuario a desuscribirse correctamente (operador -=):
// En la clase User se puede añadir el método "SalirDelChat"
public void Unsubscribe(ChatRoom chat)
{
chat.MessageReceived -= OnMessageReceived;
}
Extendemos el ejemplo:
var chat = new ChatRoom();
var user1 = new User("Antonio");
var user2 = new User("María");
chat.MessageReceived += user1.OnMessageReceived;
chat.MessageReceived += user2.OnMessageReceived;
chat.SendMessage("Primera noticia");
user2.Unsubscribe(chat); // María abandona el chat
chat.SendMessage("María ya no verá este mensaje");
La suscripción/desuscripción dinámica aparece por todas partes: ventanas que se cierran, pestañas que se cierran, servicios temporales que se desuscriben de fuentes globales de eventos. No olvides — un suscriptor que no se desuscribe se convierte en un zombi, ¡y la aplicación en un fuga de memoria!
4. Procesamiento de mensajes y generación de respuesta
Ahora añadamos un bot que reacciona a cada mensaje. Que el bot diga "¡Hola!" automáticamente si en el mensaje aparece la palabra "bot". Además veremos suscripción múltiple y el comportamiento de los delegados multicast.
public class Bot
{
public string Name { get; }
public Bot(string name) => Name = name;
public void OnMessageReceived(object? sender, MessageEventArgs e)
{
// El bot reacciona a la palabra clave
if (e.MessageText.Contains("bot", StringComparison.OrdinalIgnoreCase))
{
if (sender is ChatRoom chatRoom)
{
Console.WriteLine($"[Bot {Name}]: ¡Hola! ¿En qué puedo ayudar?");
// El bot envía un mensaje en respuesta
chatRoom.SendMessage($"Bot {Name} está listo para ayudar.");
}
}
}
}
Uso:
var chat = new ChatRoom();
var user = new User("Eugenio");
var bot = new Bot("Asistente");
chat.MessageReceived += user.OnMessageReceived;
chat.MessageReceived += bot.OnMessageReceived;
// Eugenio escribe un mensaje que dispara al bot
chat.SendMessage("¡Hola, bot, cómo estás?");
La consola mostrará (demostramos el orden de llamadas — ¡no está garantizado!):
[Eugenio] vio un nuevo mensaje: ¡Hola, bot, cómo estás?
[Bot Asistente]: ¡Hola! ¿En qué puedo ayudar?
[Eugenio] vio un nuevo mensaje: Bot Asistente está listo para ayudar.
[Bot Asistente]: ¡Hola! ¿En qué puedo ayudar?
[Eugenio] vio un nuevo mensaje: Bot Asistente está listo para ayudar.
[Bot Asistente]: ¡Hola! ¿En qué puedo ayudar?
...
¿Quién ha visto un bug potencial? Correcto, un bucle infinito: el bot reacciona a sus propios mensajes (sigue viendo la palabra "bot"). Una forma de evitarlo es añadir una comprobación sencilla:
public void OnMessageReceived(object? sender, MessageEventArgs e)
{
// El bot no reacciona a sus propios mensajes
if (e.MessageText.Contains("bot", StringComparison.OrdinalIgnoreCase) &&
!e.MessageText.Contains(Name))
{
if (sender is ChatRoom chatRoom)
{
Console.WriteLine($"[Bot {Name}]: ¡Hola! ¿En qué puedo ayudar?");
chatRoom.SendMessage($"Bot {Name} está listo para ayudar.");
}
}
}
En la práctica, estas situaciones son una buena ocasión para pensar en el manejo de eventos cíclicos, expresiones lambda o incluso un mecanismo para cancelar la ejecución de manejadores posteriores (ver lecturas previas).
5. Operaciones asíncronas y callbacks
Muy a menudo los eventos se usan para notificar la finalización de una operación asíncrona. Por ejemplo, descargar datos de Internet o un cálculo largo: el evento Completed informa del final, y el método RunLongOperationAsync ejecuta la operación prolongada.
public class LongRunner
{
// Evento sobre la finalización del trabajo
public event EventHandler<EventArgs>? Completed;
public async Task RunLongOperationAsync()
{
Console.WriteLine("La operación larga ha empezado...");
await Task.Delay(2000); // Esto simula un trabajo largo
Console.WriteLine("Operación terminada, avisando a los suscriptores.");
Completed?.Invoke(this, EventArgs.Empty);
}
}
Código cliente:
var runner = new LongRunner();
// Suscripción al evento de finalización
runner.Completed += (sender, e) =>
{
Console.WriteLine("Notificación recibida: ¡la operación ha terminado!");
};
await runner.RunLongOperationAsync();
Este es el fundamento de la interacción asíncrona — los eventos se usan mucho en frameworks UI para señalar cargas completadas, fin de animaciones, clicks de usuario, etc.
6. Sistemas de señalización
Veamos otro caso clásico: un sistema de notificaciones (por ejemplo, en una tienda online cuando hay oferta en un producto). Todos los "oyentes" interesados se enteran mediante el evento SaleOccurred:
public class SaleNotifier
{
public event EventHandler<SaleEventArgs>? SaleOccurred;
public void AnnounceSale(string product, decimal newPrice)
{
SaleOccurred?.Invoke(this, new SaleEventArgs(product, newPrice));
}
}
public class SaleEventArgs : EventArgs
{
public string Product { get; }
public decimal NewPrice { get; }
public SaleEventArgs(string product, decimal price)
{
Product = product; NewPrice = price;
}
}
public class Customer
{
public string Name { get; }
public Customer(string name) => Name = name;
public void OnSale(object? sender, SaleEventArgs e)
{
Console.WriteLine($"[{Name}] recibió la notificación: {e.Product} ahora cuesta {e.NewPrice.ToString(\"F2\")} u.e.!");
}
}
Uso:
var notifier = new SaleNotifier();
var c1 = new Customer("Andrés");
var c2 = new Customer("Olga");
notifier.SaleOccurred += c1.OnSale;
notifier.SaleOccurred += c2.OnSale;
notifier.AnnounceSale("Tetera", 999.99m);
Si uno de los clientes ya no está interesado — se desuscribe:
notifier.SaleOccurred -= c2.OnSale;
notifier.AnnounceSale("Batidora", 1999.99m);
Este patrón se usa para distribuir señales (notification, pub/sub), algo muy importante en arquitecturas modernas.
7. Implementación del mecanismo para cancelar la cadena de eventos
A veces uno de los manejadores debe "detener" las notificaciones posteriores. Normalmente se usa un derivado de EventArgs con una bandera de cancelación. En el ejemplo de abajo iteramos manualmente GetInvocationList() y paramos la ejecución cuando Cancel = true.
public class CancelEventArgs : EventArgs
{
public bool Cancel { get; set; }
}
public class EventSource
{
public event EventHandler<CancelEventArgs>? SomethingHappened;
public void DoSomething()
{
var args = new CancelEventArgs();
// Bucle clásico para iterar los suscriptores (si se necesita control del orden)
var handlers = SomethingHappened?.GetInvocationList();
if (handlers != null)
{
foreach (var handler in handlers)
{
((EventHandler<CancelEventArgs>)handler)(this, args);
if (args.Cancel)
{
Console.WriteLine("La cadena de notificaciones ha sido interrumpida.");
break;
}
}
}
}
}
Uso:
var source = new EventSource();
source.SomethingHappened += (s, e) =>
{
Console.WriteLine("Primer manejador");
};
source.SomethingHappened += (s, e) =>
{
Console.WriteLine("Segundo manejador cancela el evento.");
e.Cancel = true;
};
source.SomethingHappened += (s, e) =>
{
Console.WriteLine("Este manejador no será invocado.");
};
source.DoSomething();
Resultado:
Primer manejador
Segundo manejador cancela el evento.
La cadena de notificaciones ha sido interrumpida.
Este mecanismo es útil para validaciones, manejo de eventos antes de cerrar una ventana, comprobaciones de permisos y otras tareas donde es importante poder decir "¡Alto! No procesamos más."
8. Thread-safety y gestión de suscriptores
En aplicaciones multihilo, donde los suscriptores pueden añadirse y removerse al mismo tiempo que se genera el evento, es importante usar patrones thread-safe (ver documentación oficial). Para control puedes implementar el evento manualmente con los accesores add y remove y proteger el acceso con lock:
public class CustomEvent
{
private EventHandler? _handlers;
public event EventHandler SomethingHappened
{
add
{
lock (this) // Seguro para múltiples hilos
{
_handlers += value;
}
}
remove
{
lock (this)
{
_handlers -= value;
}
}
}
protected void RaiseEvent()
{
// Usamos una copia
EventHandler? handler;
lock (this)
{
handler = _handlers;
}
handler?.Invoke(this, EventArgs.Empty);
}
}
Este enfoque se ve rara vez (normalmente el mecanismo estándar es suficiente), pero a veces hace falta en sistemas de alta carga.
9. Eventos en UI — WinForms/WPF/MAUI
WinForms:
private void button1_Click(object sender, EventArgs e)
{
MessageBox.Show("¡Botón pulsado!");
}
Detrás de este método está el mecanismo de suscripción:
button1.Click += button1_Click;
WPF/MAUI: eventos tipo PropertyChanged (notificaciones de cambio de propiedades del modelo):
public class MyViewModel : INotifyPropertyChanged
{
private string _value;
public string Value
{
get => _value;
set
{
if (_value != value)
{
_value = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value));
}
}
}
public event PropertyChangedEventHandler? PropertyChanged;
}
Los frameworks usan intensamente eventos para construir interfaces reactivas (MVVM).
10. Errores típicos y trampas al trabajar con eventos
Error nº1: crear fugas de memoria.
Este es el problema más común. Si un objeto de corta vida (por ejemplo, una ventana o un servicio temporal) se suscribe a un evento de un objeto de larga vida (por ejemplo, un caché global o una clase estática) y no se desuscribe al destruirse, quedará en memoria para siempre. Siempre implementa la desuscripción en el método Dispose o en otro lugar apropiado del ciclo de vida del objeto.
Error nº2: crear bucles infinitos de eventos.
Un suscriptor puede reaccionar a un evento llamando a una acción que vuelve a generar el mismo evento. Esto lleva a recursión infinita y al crash de la aplicación con StackOverflowException. Siempre comprueba condiciones para que un manejador no reaccione a eventos que él mismo provocó.
Error nº3: dependencia del orden de llamada de suscriptores.
Nunca confíes en que los manejadores se llamarán en el mismo orden en que se suscribieron. La especificación de C# y la implementación del CLR no lo garantizan. Si el orden es importante, invoca a los suscriptores manualmente vía GetInvocationList() en la secuencia que necesites.
Error nº4: trabajo inseguro con eventos en entornos multihilo.
Si los suscriptores se añaden o eliminan en un hilo y el evento se lanza en otro, puede aparecer una condición de carrera. La protección clásica es copiar el delegado a una variable local antes de invocar:
var handler = MyEvent;
handler?.Invoke(this, EventArgs.Empty);
O usar lock al implementar manualmente eventos con add/remove.
GO TO FULL VERSION