CodeGym /Corsi /C# SELF /Esempi di programmazione basata su eventi (

Esempi di programmazione basata su eventi ( event)

C# SELF
Livello 54 , Lezione 4
Disponibile

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.

1
Sondaggio/quiz
Errori tipici con i delegati, livello 54, lezione 4
Non disponibile
Errori tipici con i delegati
Best practice nella programmazione ad eventi
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION