1. Introduzione
Un tipico principiante potrebbe chiedere: «Dove si usano gli eventi nella pratica? Davvero tutte le interazioni tra classi sono basate su eventi e non su chiamate dirette di metodi?» La risposta è semplice: eventi e delegati non sono una bacchetta magica che risolve tutti i problemi architetturali. Però senza di loro un'app moderna diventa presto "tightly coupled" — quando cambiare un componente richiede di modificare molti altri. Usare eventi e delegati aiuta a creare sistemi flessibili, estendibili e manutenibili.
- UI-programmazione (WinForms, WPF, Xamarin, MAUI): gestione di click, hover, input di testo e altre azioni dell'utente.
- Operazioni asincrone: completamento di download, ricezione dati dalla rete, scattare di timer.
- Architettura a plugin e sistemi estendibili: permette di collegare nuovi moduli senza integrare strettamente il core.
- Sistemi di segnalazione/distribuzione: notificare molti componenti interessati sugli eventi accaduti.
- Osservazione dei cambiamenti (observer): reagire all'arrivo di un nuovo messaggio, alla modifica dei dati, aggiornare l'interfaccia.
Ok, andiamo al dunque!
2. Scheletro architetturale
Immaginiamo di sviluppare una piccola applicazione console — una "Mini-chat" per training interno in azienda (o semplicemente per fare pratica). Abbiamo entità: User, Chat e magari un Bot-assistente. Quando un utente invia un messaggio, la chat deve notificare tutti gli utenti e i bot connessi — così loro mostrano il messaggio a schermo o, nel caso del bot, generano una risposta automatica. Questo è uno scenario classico: il publisher genera un evento (event), gli handler sono iscritti al delegato EventHandler/EventHandler<TEventArgs>, e un metodo-handler come OnMessageReceived reagisce alla notifica.
// Classe User (subscriber)
public class User
{
public string Name { get; }
public User(string name)
{
Name = name;
}
// Metodo che sarà l'handler dell'evento
public void OnMessageReceived(object? sender, MessageEventArgs e)
{
Console.WriteLine($"[{Name}] ha visto un nuovo messaggio: {e.MessageText}");
}
}
// Argomenti dell'evento
public class MessageEventArgs : EventArgs
{
public string MessageText { get; }
public MessageEventArgs(string text) => MessageText = text;
}
// Classe ChatRoom (publisher dell'evento)
public class ChatRoom
{
public event EventHandler<MessageEventArgs>? MessageReceived;
public void SendMessage(string text)
{
// Generiamo l'evento (notifichiamo tutti gli iscritti)
MessageReceived?.Invoke(this, new MessageEventArgs(text));
}
}
Esempio d'uso:
var chat = new ChatRoom();
var user1 = new User("Anton");
var user2 = new User("Maria");
chat.MessageReceived += user1.OnMessageReceived;
chat.MessageReceived += user2.OnMessageReceived;
chat.SendMessage("Ciao a tutti! 😊");
In console vedremo due messaggi — entrambi gli utenti sapranno del nuovo messaggio.
3. Sottoscrizione e dediscrizione dinamica
Nella vita reale un utente può uscire dalla chat e non voler più ricevere messaggi. Insegniamo all'utente a dediscriversi correttamente (operatore -=):
// Nella classe User si può aggiungere il metodo "Uscire dalla chat"
public void Unsubscribe(ChatRoom chat)
{
chat.MessageReceived -= OnMessageReceived;
}
Espandiamo l'esempio:
var chat = new ChatRoom();
var user1 = new User("Anton");
var user2 = new User("Maria");
chat.MessageReceived += user1.OnMessageReceived;
chat.MessageReceived += user2.OnMessageReceived;
chat.SendMessage("Prima notizia");
user2.Unsubscribe(chat); // Maria lascia la chat
chat.SendMessage("Maria non vedrà più questo messaggio");
Sottoscrizioni/dediscrizioni dinamiche succedono ovunque: finestre che si chiudono, tab che vengono rimosse, servizi temporanei che si dissociano da sorgenti globali di eventi. Non dimenticare — un subscriber non dediscritto diventa uno zombie e l'app diventa un memory leak!
4. Gestione dei messaggi e generazione di risposta
Ora aggiungiamo un bot che reagisce a ogni messaggio. Facciamo sì che il bot dica "Ciao!" automaticamente se nel messaggio è presente la parola "bot". Mostriamo anche la sottoscrizione multipla e i delegati multicast.
public class Bot
{
public string Name { get; }
public Bot(string name) => Name = name;
public void OnMessageReceived(object? sender, MessageEventArgs e)
{
// Il bot reagisce alla parola chiave
if (e.MessageText.Contains("bot", StringComparison.OrdinalIgnoreCase))
{
if (sender is ChatRoom chatRoom)
{
Console.WriteLine($"[Bot {Name}]: Ciao! Come posso aiutare?");
// Il bot invia un messaggio in risposta
chatRoom.SendMessage($"Bot {Name} è pronto ad aiutarti.");
}
}
}
}
Usiamo:
var chat = new ChatRoom();
var user = new User("Evgenij");
var bot = new Bot("Assistente");
chat.MessageReceived += user.OnMessageReceived;
chat.MessageReceived += bot.OnMessageReceived;
// Evgenij scrive un messaggio che triggera il bot
chat.SendMessage("Ciao, bot, come va?");
La console mostrerà (dimostriamo l'ordine di chiamata — non è garantito!):
[Evgenij] ha visto un nuovo messaggio: Ciao, bot, come va?
[Bot Assistente]: Ciao! Come posso aiutare?
[Evgenij] ha visto un nuovo messaggio: Bot Assistente è pronto ad aiutarti.
[Bot Assistente]: Ciao! Come posso aiutare?
[Evgenij] ha visto un nuovo messaggio: Bot Assistente è pronto ad aiutarti.
[Bot Assistente]: Ciao! Come posso aiutare?
...
Chi ha notato il potenziale bug? Giusto, un loop infinito: il bot reagisce ai suoi stessi messaggi (vede di nuovo la parola "bot"). Un modo per prevenire questo è aggiungere un semplice controllo:
public void OnMessageReceived(object? sender, MessageEventArgs e)
{
// Il bot non reagisce ai propri messaggi
if (e.MessageText.Contains("bot", StringComparison.OrdinalIgnoreCase) &&
!e.MessageText.Contains(Name))
{
if (sender is ChatRoom chatRoom)
{
Console.WriteLine($"[Bot {Name}]: Ciao! Come posso aiutare?");
chatRoom.SendMessage($"Bot {Name} è pronto ad aiutarti.");
}
}
}
Nella pratica questi scenari sono un ottimo spunto per pensare a gestione di eventi ciclici, espressioni lambda o anche a un meccanismo per cancellare l'esecuzione degli handler successivi (vedi lezione precedente).
5. Operazioni asincrone e callback
Spesso gli eventi sono usati per notificare il completamento di un'operazione asincrona. Per esempio, il download di dati o un calcolo lungo: l'evento Completed segnala la fine, e il metodo RunLongOperationAsync esegue il lavoro pesante.
public class LongRunner
{
// Evento sul completamento del lavoro
public event EventHandler<EventArgs>? Completed;
public async Task RunLongOperationAsync()
{
Console.WriteLine("L'operazione lunga è iniziata...");
await Task.Delay(2000); // Simulazione di lavoro lungo
Console.WriteLine("Operazione completata, notifichiamo gli iscritti.");
Completed?.Invoke(this, EventArgs.Empty);
}
}
Codice client:
var runner = new LongRunner();
// Sottoscrizione all'evento di completamento
runner.Completed += (sender, e) =>
{
Console.WriteLine("Ricevuta notifica: operazione completata!");
};
await runner.RunLongOperationAsync();
Questo è il fondamento dell'interazione asincrona — gli eventi vengono spesso usati nei framework UI per segnalare il completamento di caricamenti, la fine di animazioni, click dell'utente, ecc.
6. Sistemi di segnalazione
Vediamo un altro classico: un sistema di notifiche (per esempio in un e-commerce quando c'è uno sconto su un prodotto). Tutti gli "ascoltatori" interessati vengono informati tramite l'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}] ha ricevuto notifica: {e.Product} ora costa {e.NewPrice.ToString(\"F2\")} u.m.!");
}
}
Usiamo:
var notifier = new SaleNotifier();
var c1 = new Customer("Andrej");
var c2 = new Customer("Olga");
notifier.SaleOccurred += c1.OnSale;
notifier.SaleOccurred += c2.OnSale;
notifier.AnnounceSale("Bollitore", 999.99m);
Se uno dei clienti non è più interessato — si dediscrive:
notifier.SaleOccurred -= c2.OnSale;
notifier.AnnounceSale("Frullatore", 1999.99m);
Questo pattern è usato per distribuire segnali (notification, pub/sub), molto importante nelle architetture moderne.
7. Implementare la cancellazione della catena di eventi
A volte uno degli handler deve "fermare" le notifiche successive. Di solito si usa una sottoclasse di EventArgs con un flag di cancellazione. Nell'esempio sotto itero manualmente su GetInvocationList() e interrompo l'esecuzione quando 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();
// Ciclo classico per iterare sugli iscritti (se serve controllo sull'ordine)
var handlers = SomethingHappened?.GetInvocationList();
if (handlers != null)
{
foreach (var handler in handlers)
{
((EventHandler<CancelEventArgs>)handler)(this, args);
if (args.Cancel)
{
Console.WriteLine("La catena dei messaggi è stata interrotta.");
break;
}
}
}
}
}
Usiamo:
var source = new EventSource();
source.SomethingHappened += (s, e) =>
{
Console.WriteLine("Primo gestore");
};
source.SomethingHappened += (s, e) =>
{
Console.WriteLine("Secondo gestore annulla l'evento.");
e.Cancel = true;
};
source.SomethingHappened += (s, e) =>
{
Console.WriteLine("Questo gestore non sarà chiamato.");
};
source.DoSomething();
Risultato:
Primo gestore
Secondo gestore annulla l'evento.
La catena dei messaggi è stata interrotta.
Questo meccanismo è utile per validazioni, gestione di eventi prima della chiusura di una finestra, controllo dei permessi e altri scenari dove è importante poter dire "Fermo! Non procedere oltre."
8. Thread-safety e gestione degli iscritti
Nelle applicazioni multithread, dove gli iscritti possono essere aggiunti o rimossi mentre l'evento viene sollevato, è importante usare pattern thread-safe (vedi la documentazione ufficiale). Per avere controllo si può implementare l'evento manualmente con gli accessor add e remove e proteggere l'accesso con lock:
public class CustomEvent
{
private EventHandler? _handlers;
public event EventHandler SomethingHappened
{
add
{
lock (this) // Thread-safe
{
_handlers += value;
}
}
remove
{
lock (this)
{
_handlers -= value;
}
}
}
protected void RaiseEvent()
{
// Usiamo una copia
EventHandler? handler;
lock (this)
{
handler = _handlers;
}
handler?.Invoke(this, EventArgs.Empty);
}
}
Questo approccio è raro (di solito il meccanismo standard è sufficiente), ma può servire in sistemi ad alto carico.
9. Eventi in UI — WinForms/WPF/MAUI
WinForms:
private void button1_Click(object sender, EventArgs e)
{
MessageBox.Show("Pulsante premuto!");
}
Dietro questo metodo c'è il meccanismo di sottoscrizione:
button1.Click += button1_Click;
WPF/MAUI: eventi come PropertyChanged (notifiche di modifica delle proprietà del model):
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;
}
I framework usano intensamente gli eventi per costruire interfacce reattive (MVVM).
10. Errori tipici e trappole quando si lavora con gli eventi
Errore №1: creare memory leak.
È il problema più comune. Se un oggetto a vita breve (per esempio una finestra o un servizio temporaneo) si iscrive a un evento di un oggetto a vita lunga (per esempio una cache globale o una classe statica) e non si dediscrive quando viene distrutto, rimarrà in memoria per sempre. Implementa sempre la dediscrizione in Dispose o in un altro punto adatto del ciclo di vita dell'oggetto.
Errore №2: creare loop infiniti di eventi.
Un subscriber può reagire a un evento eseguendo un'azione che genera lo stesso evento di nuovo. Questo porta a ricorsione infinita e al crash con StackOverflowException. Controlla sempre le condizioni in modo che un handler non reagisca agli eventi che lui stesso ha generato.
Errore №3: dipendere dall'ordine di chiamata degli iscritti.
Non fare mai affidamento sul fatto che gli handler vengano invocati nello stesso ordine in cui sono stati registrati. La specifica C# e l'implementazione CLR non lo garantiscono. Se l'ordine è importante, invoca gli iscritti manualmente tramite GetInvocationList() nella sequenza desiderata.
Errore №4: lavoro non sicuro con eventi in ambienti multithread.
Se gli iscritti vengono aggiunti o rimossi da un thread mentre l'evento viene sollevato da un altro, si possono avere race condition. La protezione classica è copiare il delegato in una variabile locale prima di invocarlo:
var handler = MyEvent;
handler?.Invoke(this, EventArgs.Empty);
O usare lock quando si implementano manualmente gli eventi tramite add/remove.
GO TO FULL VERSION