Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
Gli eventi .NET seguono in genere alcuni schemi noti. La standardizzazione su questi modelli significa che gli sviluppatori possono applicare le conoscenze di questi modelli standard, che possono essere applicati a qualsiasi programma di eventi .NET.
Esaminiamo questi modelli standard in modo da avere tutte le conoscenze necessarie per creare origini eventi standard e sottoscrivere ed elaborare eventi standard nel codice.
Firme dei delegati dell'evento
La firma standard per un delegato di evento .NET è:
void EventRaised(object sender, EventArgs args);
Questa firma standard fornisce informazioni dettagliate sui casi in cui vengono usati gli eventi:
- Il tipo restituito è void. Gli eventi possono avere da zero a molti ascoltatori. La generazione di un evento notifica tutti gli ascoltatori. In generale, i listener non forniscono valori in risposta agli eventi.
-
Eventi indicano il mittente: la firma dell'evento include l'oggetto che ha generato l'evento. Che fornisce a qualsiasi listener un meccanismo per comunicare con il mittente. Il tipo di
senderal momento della compilazione èSystem.Object, anche se probabilmente conosci un tipo più derivato che sarebbe sempre corretto. Per convenzione si usaobject. -
Gli eventi raggruppano più informazioni in una singola struttura: il parametro
argsè un tipo derivato da System.EventArgs che include le ulteriori informazioni necessarie. Nella sezione successiva verrà visualizzato che questa convenzione non è più applicata. Se il tipo di evento non richiede altri argomenti, è comunque necessario specificare entrambi gli argomenti. Esiste un valore speciale, EventArgs.Empty da usare per indicare che l'evento non contiene informazioni aggiuntive.
Creare ora una classe in cui sono elencati i file di una directory o di una delle sue sottodirectory che seguono uno schema. Questo componente genera un evento per ogni file individuato che corrisponde allo schema.
L'uso di un modello di eventi offre alcuni vantaggi di progettazione. È possibile creare più listener di evento che eseguono azioni diverse quando viene trovato uno dei file cercati. Combinando i diversi listener è possibile creare algoritmi più affidabili.
Ecco la dichiarazione di argomento dell'evento iniziale per trovare un file cercato:
public class FileFoundArgs : EventArgs
{
public string FoundFile { get; }
public FileFoundArgs(string fileName) => FoundFile = fileName;
}
Anche se questo sembra un tipo di piccole dimensioni e di soli dati, si deve seguire la convenzione e renderlo un tipo riferimento (class). Ciò significa che l'oggetto argomento viene passato per riferimento e gli eventuali aggiornamenti ai dati vengono visualizzati da tutti gli abbonati. La prima versione è un oggetto non modificabile. È preferibile rendere non modificabili le proprietà nel tipo di argomenti evento. In questo modo, un sottoscrittore non può modificare i valori prima che vengano visualizzati da un altro sottoscrittore. Ci sono eccezioni a questa pratica, come illustrato più avanti.
Ora è necessario creare la dichiarazione di evento nella classe FileSearcher. L'uso del tipo System.EventHandler<TEventArgs> significa che non è necessario creare ancora un'altra definizione di tipo. È sufficiente usare una specializzazione generica.
Si compila la classe FileSearcher per cercare i file che corrispondono a uno schema e si genera l'evento corretto quando viene individuata una corrispondenza.
public class FileSearcher
{
public event EventHandler<FileFoundArgs>? FileFound;
public void Search(string directory, string searchPattern)
{
foreach (var file in Directory.EnumerateFiles(directory, searchPattern))
{
FileFound?.Invoke(this, new FileFoundArgs(file));
}
}
}
Definire e sollevare eventi collegati ai campi
Il modo più semplice per aggiungere un evento alla classe consiste nel dichiarare l'evento come campo pubblico, come illustrato nell'esempio precedente:
public event EventHandler<FileFoundArgs>? FileFound;
Sembra che venga dichiarato un campo pubblico, il che potrebbe apparire come una prassi scorretta nella programmazione orientata a oggetti. Si desidera proteggere l'accesso ai dati tramite le proprietà o i metodi. Anche se questo codice potrebbe essere simile a una procedura non valida, il codice generato dal compilatore crea wrapper in modo che gli oggetti evento possano essere accessibili solo in modi sicuri. Le uniche operazioni disponibili in un evento simile a un campo sono l'aggiunta e la rimozione del gestore:
var fileLister = new FileSearcher();
int filesFound = 0;
EventHandler<FileFoundArgs> onFileFound = (sender, eventArgs) =>
{
Console.WriteLine(eventArgs.FoundFile);
filesFound++;
};
fileLister.FileFound += onFileFound;
fileLister.FileFound -= onFileFound;
Esiste una variabile locale per il gestore. Se utilizzassi il corpo dell'espressione lambda, il gestore remove non funzionerebbe correttamente. Sarebbe un'altra istanza del delegato e tacitamente non farebbe nulla.
Il codice all'esterno della classe non può generare l'evento né può eseguire altre operazioni.
A partire da C# 14, gli eventi possono essere dichiarati come membri parziali. Una dichiarazione di evento parziale deve includere una dichiarazione di definizione e una dichiarazione di implementazione. La dichiarazione di definizione deve usare la sintassi dell'evento simile a un campo. La dichiarazione di implementazione deve dichiarare i gestori add e remove.
Restituzione di valori da sottoscrittori di evento
La versione semplice funziona correttamente. Si aggiungerà un'altra funzionalità: l'annullamento.
Quando si genera l'evento Trovato, i listener devono poter interrompere l'ulteriore elaborazione, se questo file è l'ultimo cercato.
I gestori eventi non restituiscono un valore, quindi è necessario comunicare in un altro modo. Il modello di evento standard usa l'oggetto EventArgs per includere i campi che i sottoscrittori di evento possono usare per comunicare l'annullamento.
Esistono due modelli diversi che potrebbero essere usati, in base alla semantica del contratto di annullamento. In entrambi i casi, si aggiunge un campo booleano agli EventArguments per l'evento di file trovato.
Uno degli schemi consente a qualsiasi sottoscrittore di annullare l'operazione. Per questo schema il nuovo campo viene inizializzato su false. Qualsiasi sottoscrittore può cambiarlo in true. Dopo la generazione dell'evento per tutti i sottoscrittori, il componente FileSearcher esamina il valore booleano e esegue l'azione.
Il secondo modello annullerebbe l'operazione solo se tutti i sottoscrittori volessero annullarla. In questo schema viene inizializzato il nuovo campo per indicare che l'operazione deve essere annullata e qualsiasi sottoscrittore può cambiarlo per indicare che l'operazione deve continuare. Dopo che tutti i sottoscrittori elaborano l'evento generato, il componente FileSearcher esamina il valore booleano e esegue l'azione. In questo modello è presente un passaggio aggiuntivo: il componente deve sapere se i sottoscrittori hanno risposto all'evento. Se non sono presenti sottoscrittori, il campo indicherà erroneamente un annullamento.
Ora si implementerà la prima versione per questo esempio. È necessario aggiungere un campo booleano denominato CancelRequested al tipo FileFoundArgs:
public class FileFoundArgs : EventArgs
{
public string FoundFile { get; }
public bool CancelRequested { get; set; }
public FileFoundArgs(string fileName) => FoundFile = fileName;
}
Questo nuovo campo viene inizializzato automaticamente per false in modo da non annullare accidentalmente. L'unica modifica apportata al componente consiste nel controllare il flag dopo la generazione dell'evento per verificare se uno dei sottoscrittori ha richiesto un annullamento:
private void SearchDirectory(string directory, string searchPattern)
{
foreach (var file in Directory.EnumerateFiles(directory, searchPattern))
{
var args = new FileFoundArgs(file);
FileFound?.Invoke(this, args);
if (args.CancelRequested)
break;
}
}
Un vantaggio di questo schema è che non si tratta di una modifica sostanziale. Nessuno dei sottoscrittori ha richiesto l'annullamento prima e non lo sono ancora. Nessuna parte del codice degli abbonati richiede aggiornamenti a meno che non si voglia supportare il nuovo protocollo di cancellazione.
Aggiorniamo il server di sottoscrizione in modo che richieda un annullamento dopo aver trovato il primo eseguibile:
EventHandler<FileFoundArgs> onFileFound = (sender, eventArgs) =>
{
Console.WriteLine(eventArgs.FoundFile);
eventArgs.CancelRequested = true;
};
Aggiunta di un'altra dichiarazione di evento
Aggiungeremo ora un'altra funzionalità e dimostreremo altri idiomi del linguaggio per gli eventi. Aggiungiamo un overload del metodo Search che attraversa tutte le sottodirectory per cercare i file.
Questo metodo potrebbe diventare un'operazione lunga in una directory con molte sottodirectory. Aggiungiamo un evento che viene generato quando inizia la ricerca in una nuova directory. Questo evento consente ai sottoscrittori di tenere traccia dello stato di avanzamento e aggiornare l'utente in base allo stato di avanzamento. Tutti gli esempi creati finora sono pubblici. Facciamo in modo che questo evento sia un evento interno. Ciò significa che è possibile rendere interni anche i tipi di argomento.
Per iniziare, creare la nuova classe derivata EventArgs per segnalare la nuova directory e lo stato di avanzamento.
internal class SearchDirectoryArgs : EventArgs
{
internal string CurrentSearchDirectory { get; }
internal int TotalDirs { get; }
internal int CompletedDirs { get; }
internal SearchDirectoryArgs(string dir, int totalDirs, int completedDirs)
{
CurrentSearchDirectory = dir;
TotalDirs = totalDirs;
CompletedDirs = completedDirs;
}
}
Anche in questo caso è possibile seguire le indicazioni per creare un tipo di riferimento non modificabile per argomenti evento.
Ora definire l'evento. Questa volta si usa una sintassi diversa. Oltre a usare la sintassi del campo, è possibile creare in modo esplicito la proprietà dell'evento con gestori di aggiunta e rimozione. In questo esempio non è necessario codice aggiuntivo in tali gestori, ma viene illustrato come crearli.
internal event EventHandler<SearchDirectoryArgs> DirectoryChanged
{
add { _directoryChanged += value; }
remove { _directoryChanged -= value; }
}
private EventHandler<SearchDirectoryArgs>? _directoryChanged;
In molti modi, il codice scritto qui rispecchia il codice generato dal compilatore per le definizioni degli eventi di campo illustrate in precedenza. L'evento viene creato usando una sintassi simile a proprietà. Si noti che i gestori hanno nomi diversi: add e remove. Queste funzioni di accesso vengono chiamate per sottoscrivere l'evento o annullare la sottoscrizione all'evento. Si noti che è necessario anche dichiarare un campo di backup privato per archiviare la variabile di evento. Questa variabile viene inizializzata su Null.
Quindi, si aggiunge l'overload del metodo Search che attraversa le sottodirectory e genera entrambi gli eventi. Il modo più semplice consiste nell'usare un argomento predefinito per specificare che si desidera eseguire ricerche in tutte le directory:
public void Search(string directory, string searchPattern, bool searchSubDirs = false)
{
if (searchSubDirs)
{
var allDirectories = Directory.GetDirectories(directory, "*.*", SearchOption.AllDirectories);
var completedDirs = 0;
var totalDirs = allDirectories.Length + 1;
foreach (var dir in allDirectories)
{
_directoryChanged?.Invoke(this, new (dir, totalDirs, completedDirs++));
// Search 'dir' and its subdirectories for files that match the search pattern:
SearchDirectory(dir, searchPattern);
}
// Include the Current Directory:
_directoryChanged?.Invoke(this, new (directory, totalDirs, completedDirs++));
SearchDirectory(directory, searchPattern);
}
else
{
SearchDirectory(directory, searchPattern);
}
}
private void SearchDirectory(string directory, string searchPattern)
{
foreach (var file in Directory.EnumerateFiles(directory, searchPattern))
{
var args = new FileFoundArgs(file);
FileFound?.Invoke(this, args);
if (args.CancelRequested)
break;
}
}
A questo punto, è possibile eseguire l'applicazione utilizzando la funzione di overload per cercare in tutte le sottodirectory. Non sono presenti sottoscrittori per il nuovo evento DirectoryChanged, ma l'uso del linguaggio ?.Invoke() garantisce il corretto funzionamento.
Aggiungiamo un gestore per scrivere una riga che mostra l'avanzamento nella finestra della console.
fileLister.DirectoryChanged += (sender, eventArgs) =>
{
Console.Write($"Entering '{eventArgs.CurrentSearchDirectory}'.");
Console.WriteLine($" {eventArgs.CompletedDirs} of {eventArgs.TotalDirs} completed...");
};
Sono stati illustrati i modelli seguiti in tutto l'ecosistema .NET. Apprendendo questi modelli e convenzioni, si scrive rapidamente C# e .NET idiomatici.
Vedi anche
Verranno quindi visualizzate alcune modifiche in questi modelli nella versione più recente di .NET.