CodeGym /Corsi /C# SELF /Scenari pratici e best practice

Scenari pratici e best practice

C# SELF
Livello 53 , Lezione 4
Disponibile

1. Scenari pratici di utilizzo degli eventi

Gli eventi non sono solo una bella teoria dai libri. In pratica servono in quasi ogni seconda applicazione, e se lavori con UI o servizi di rete — nella maggior parte dei casi.

Vediamo alcuni scenari tipici dello sviluppo reale. Allo stesso tempo guarderemo come gli standard e le best practice aiutino a evitare errori comuni.

Reagire alle azioni dell'utente (come nella UI)

Probabilmente lo scenario più comune — quando l'utente fa qualcosa (clicca un bottone, seleziona un elemento nella lista) e l'app deve reagire. È così che funzionano Windows Forms, WPF, UWP, Avalonia, MAUI e altri framework UI.

Mini-esempio (senza UI — simuliamo un bottone):


public class Button
{
    public event EventHandler? Click; // standard delegate

    public void SimulateClick()
    {
        Click?.Invoke(this, EventArgs.Empty); // "Bottone" è stato "premuto"
    }
}

class Program
{
    static void Main()
    {
        var button = new Button();
        button.Click += (sender, e) => Console.WriteLine("Bottone premuto!");
        button.SimulateClick(); // > Bottone premuto!
    }
}

In un vero framework UI l'evento Click scatta quando l'utente clicca il bottone col mouse o tocca lo schermo. Tutto ciò che si è iscritto a questo evento riceverà la notifica — è così che funziona tutta la logica delle applicazioni.

Segnalare progresso, completamento operazioni ed errori

Immagina: download di un file, elaborazione dati, calcolo lungo. Devi informare sul progresso o sugli errori. Spesso si organizza un evento per ogni "step di progresso" e un evento separato per "completato" o "errore".

Esempio di downloader di file:


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); // simulazione di delay
            ProgressChanged?.Invoke(this, i);
        }
        DownloadCompleted?.Invoke(this, EventArgs.Empty);
    }
}

var downloader = new FileDownloader();
downloader.ProgressChanged += (s, progress) => Console.WriteLine($"Download: {progress}%");
downloader.DownloadCompleted += (s, e) => Console.WriteLine("Download completato.");
downloader.Download();

Qui ci sono diversi eventi: puoi iscriversi a tutto (o solo a quello che ti serve). Somiglia a come funzionano molte classi standard .NET per thread, download, HTTP ecc.

Reazione al ciclo di vita degli oggetti (es. "salvato", "eliminato")

Supponiamo di avere un modello di dominio. Vuoi che quando un utente salva o elimina un oggetto, alcuni processi reagiscano: aggiornino la cache, il log, inviino messaggi ecc.


public class UserRepository
{
    public event EventHandler<UserEventArgs>? UserSaved;
    public event EventHandler<UserEventArgs>? UserDeleted;

    public void Save(User user)
    {
        //... salviamo l'utente
        UserSaved?.Invoke(this, new UserEventArgs(user));
    }

    public void Delete(User user)
    {
        //... eliminiamo l'utente
        UserDeleted?.Invoke(this, new UserEventArgs(user));
    }
}

public class UserEventArgs : EventArgs
{
    public User User { get; }
    public UserEventArgs(User user) => User = user;
}

Ora vari moduli possono iscriversi e — senza legami diretti! — ricevere notifiche su tutte le modifiche agli utenti. Il repository stesso non sa chi "si è collegato".

Eventi asincroni e multithreading

A volte gli eventi sono generati non dal thread principale (UI) — per esempio da task in background, timer o operazioni asincrone. In questi casi è importante ricordare che il codice degli handler verrà eseguito in un "thread estraneo". Se l'handler tenta di aggiornare componenti UI direttamente da un altro thread — otterrai eccezioni.

Cos'è il marshalling? Il marshalling è il trasferimento di esecuzione del codice da un thread a un altro, solitamente da un thread background al thread UI, per aggiornare la UI in modo sicuro. Nelle app UI (WinForms, WPF) si usano meccanismi come SynchronizationContext o Dispatcher, che permettono di "ridirigere" la chiamata dell'handler sul thread corretto.

Non fare così:


// L'evento è invocato da un thread in background, ma l'handler aggiorna la UI direttamente — otterrai un'eccezione!

Si raccomanda: controllare il thread di esecuzione e, se necessario, fare il marshalling sul thread UI (tramite SynchronizationContext o Dispatcher). Nelle applicazioni console o server queste restrizioni di solito non ci sono.

Event Aggregator / Messaging

Nelle applicazioni grandi spesso si usa un aggregatore centrale di eventi (Event Aggregator). Riduce l'accoppiamento: subscriber e publisher non si conoscono e scambiano messaggi tramite un hub.


public class EventAggregator
{
    public event EventHandler<SomeEventArgs>? SomeEvent;

    public void Publish(SomeEventArgs args)
    {
        SomeEvent?.Invoke(this, args);
    }
}

2. Pattern e best practice

Usa l'attributo [CallerMemberName] per gli errori

Se scrivi codice infrastrutturale (es. logger, tracer), registra il nome del metodo sorgente dell'evento usando l'attributo CallerMemberName — aiuta nella diagnostica.

Cerca di rendere gli eventi thread-safe, se necessario

Gli eventi sono multicast, e gli accessi da thread diversi richiedono attenzione: prima di invocare copia il delegato in una variabile locale.


var handler = MyEvent;
if (handler != null) handler(this, args);

In C# 6+ usa la chiamata sicura: MyEvent?.Invoke(this, args).

Definisci gli EventArgs personalizzati come immutable

Definisci i tuoi tipi EventArgs con proprietà readonly — questo evita modifiche accidentali dello stato dell'evento da parte dei subscriber.


public class WorkCompletedEventArgs : EventArgs
{
    public string Message { get; }
    public WorkCompletedEventArgs(string message) => Message = message;
}

public o private event

Dichiara gli eventi public event, e incapsula l'invocazione in un metodo protected virtual OnEventName. Questo permette l'estendibilità (il sottotipo può override), e impedisce ai consumatori esterni di invocare l'evento direttamente.

Non violare la sequenza del ciclo di vita

Se inizi una lunga catena di handler, ricorda: qualcuno può iscriversi e provare a cambiare la logica interna. Documenta dove e quando gli eventi vengono chiamati, e se un evento può scattare più volte.

Più eventi e composizione

Quando un oggetto ascolta più sorgenti, raggruppa subscribe e unsubscribe in un unico posto (per esempio metodi Subscribe/Unsubscribe). Se necessario conserva campi delegati privati per facilitare l'unsubscribe.

3. Come NON usare gli eventi: errori tipici

Errore №1: ignorare il pattern standard degli eventi.
Se ogni classe dichiara i propri delegati per ogni evento, diventa rapidamente caos. Cerca di usare i delegati standard EventHandler o EventHandler<T>, dove T eredita da EventArgs. Questo rende il codice più comprensibile e familiare agli sviluppatori .NET.

Errore №2: chiamare l'evento in modo non corretto dal codice esterno.
Non chiamare mai un evento direttamente al di fuori della classe publisher. L'evento è iniziato solo dentro la classe, di solito tramite un metodo protetto OnEventName. I subscriber possono solo iscriversi (+=) e cancellare l'iscrizione (-=).

Male:


foo.MyEvent(); // errore! Non puoi chiamare l'evento dall'esterno direttamente

Bene:


// Solo dentro foo:
// protected virtual void OnMyEvent()
// {
//     MyEvent?.Invoke(this, EventArgs.Empty);
// }

Errore №3: chiamare l'evento senza verificare la presenza di subscriber (null).
Se provi a chiamare un evento senza subscriber, otterrai un NullReferenceException. In C# 6.0+ usa ?.Invoke(...) — l'evento verrà chiamato solo se ci sono subscriber.

Errore №4: dimenticare di fare unsubscribe dall'evento (memory leak).
Se un oggetto si è iscritto a un evento ma non si è cancellato prima di essere distrutto, il publisher mantiene il riferimento al subscriber. Il garbage collector non potrà liberare la memoria, cosa critica per publisher long-lived e subscriber short-lived (per esempio ViewModel, form). La soluzione migliore è unsubscribe esplicito o usare riferimenti deboli (WeakReference) dove appropriato.

Errore №5: chiamare l'evento al di fuori del metodo protetto virtuale OnEvent.
Chiamando l'evento direttamente privi i sottotipi della possibilità di override corretto del comportamento. Il pattern corretto è dichiarare un metodo protected virtual che invoca l'evento.

Esempio standard:


protected virtual void OnSomething(EventArgs e)
{
    Something?.Invoke(this, e);
}
1
Sondaggio/quiz
Ciclo di vita degli eventi, livello 53, lezione 4
Non disponibile
Ciclo di vita degli eventi
Analisi dettagliata della sottoscrizione agli eventi
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION