Note
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier les répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de changer de répertoire.
Les événements .NET respectent en général quelques modèles connus. La normalisation sur ces modèles signifie que les développeurs peuvent appliquer les connaissances de ces modèles standard, qui peuvent être appliqués à n’importe quel programme d’événements .NET.
Examinons ces modèles standard pour que vous disposiez de toutes les connaissances nécessaires pour créer des sources d’événements standard, et vous abonner à et traiter des événements standard dans votre code.
Signatures de délégués d’événements
La signature standard d’un délégué d’événement .NET est la suivante :
void EventRaised(object sender, EventArgs args);
Cette signature standard fournit des informations sur à quel moment les événements sont utilisés :
- Le type de retour est void. Les événements peuvent avoir de zéro à de nombreux écouteurs. Le déclenchement d’un événement avertit tous les auditeurs. En général, les écouteurs ne fournissent pas de valeurs en réponse aux événements.
-
Événements indiquent l’expéditeur: la signature de l’événement inclut l’objet qui a déclenché l’événement. Cela fournit à n’importe quel écouteur un mécanisme de communication avec l’expéditeur. Le type au moment de compilation de
senderestSystem.Object, même si vous connaissez probablement un type plus dérivé qui serait toujours correct. Par convention, utilisezobject. -
Les événements regroupent davantage d'informations dans une seule structure : le
argsparamètre est un type dérivé de System.EventArgs qui inclut toute information supplémentaire nécessaire. (Vous verrez dans la section suivante que cette convention n’est plus appliquée.) Si votre type d’événement n’a pas besoin d’autres arguments, vous devez toujours fournir les deux arguments. Il existe une valeur spéciale, EventArgs.Empty que vous devez utiliser pour indiquer que votre événement ne contient aucune information supplémentaire.
Commençons par créer une classe qui répertorie les fichiers contenus dans un répertoire ou dans l’un de ses sous-répertoires qui suivent un modèle. Ce composant déclenche un événement pour chaque fichier détecté qui correspond au modèle.
L’utilisation d’un modèle d’événement offre certains avantages en matière de conception. Vous pouvez créer plusieurs détecteurs d’événements qui effectuent des actions différentes quand un fichier recherché est trouvé. La combinaison des différents détecteurs permet de créer des algorithmes plus robustes.
Voici la déclaration d’argument d’événement initiale pour trouver un fichier recherché :
public class FileFoundArgs : EventArgs
{
public string FoundFile { get; }
public FileFoundArgs(string fileName) => FoundFile = fileName;
}
Bien que ce type ressemble à un petit type « données uniquement », vous devez suivre la convention et faire de lui un type référence (class). Cela signifie que l’objet d’argument est passé par référence et que toutes les mises à jour des données sont affichées par tous les abonnés. La première version est un objet immuable. Vous devriez préférer rendre immuables les propriétés de votre type d'argument d'événement. De cette façon, un abonné ne peut pas modifier les valeurs avant qu’un autre abonné ne les voit. (Il existe des exceptions à cette pratique, comme vous le voyez plus loin.)
Ensuite, nous devons créer la déclaration d’événement dans la classe FileSearcher. L’utilisation du type System.EventHandler<TEventArgs> signifie que vous n’avez pas besoin de créer une autre définition de type. Vous utilisez simplement une spécialisation générique.
Nous allons remplir la classe FileSearcher pour rechercher les fichiers qui correspondent à un modèle et déclencher l’événement approprié quand une correspondance est détectée.
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));
}
}
}
Définir et déclencher des événements de type champ
Pour ajouter un événement à votre classe, le plus simple consiste à déclarer cet événement en tant que champ public, comme dans l’exemple précédent :
public event EventHandler<FileFoundArgs>? FileFound;
Ce code semble déclarer un champ public, ce qui semble être une mauvaise pratique orientée objet. Vous devez protéger l’accès aux données par l’intermédiaire des propriétés ou méthodes. Bien que ce code ressemble à une mauvaise pratique, le code généré par le compilateur crée des wrappers afin que les objets d’événement ne soient accessibles que de manière sécurisée. Les seules opérations disponibles sur un événement de type champ sont ajouter et supprimer le gestionnaire.
var fileLister = new FileSearcher();
int filesFound = 0;
EventHandler<FileFoundArgs> onFileFound = (sender, eventArgs) =>
{
Console.WriteLine(eventArgs.FoundFile);
filesFound++;
};
fileLister.FileFound += onFileFound;
fileLister.FileFound -= onFileFound;
Il existe une variable locale pour le gestionnaire. Si vous avez utilisé le corps de l’expression lambda, le gestionnaire remove ne pouvait pas fonctionner pas correctement. Il s’agirait d’une autre instance du délégué, et l’opération ne ferait rien en mode silencieux.
Le code en dehors de la classe ne peut pas déclencher l’événement, ni effectuer d’autres opérations.
À compter de C# 14, les événements peuvent être déclarés en tant que membres partiels. Une déclaration d’événement partiel doit inclure une déclaration de définition et une déclaration d’implémentation. La déclaration de définition doit utiliser la syntaxe d’événement de type champ. La déclaration d’implémentation doit déclarer les gestionnaires add et remove.
Valeurs renvoyées provenant des abonnés d'événements
Notre version simple fonctionne correctement. Ajoutons maintenant une autre fonctionnalité : l’annulation.
Quand vous déclenchez l’événement Found, les détecteurs doivent pouvoir arrêter le traitement si ce fichier est le dernier recherché.
Les gestionnaires d’événements ne retournent pas de valeur. Vous devez donc communiquer cela d’une autre manière. Le modèle d’événement standard utilise l’objet EventArgs pour inclure des champs que les abonnés aux événements peuvent utiliser pour signaler une annulation.
Deux modèles différents peuvent être utilisés, en fonction de la sémantique du contrat d’annulation. Dans les deux cas, vous ajoutez un champ booléen à EventArguments pour l’événement de fichier trouvé.
L’un des modèles autorise n’importe quel abonné à annuler l’opération. Pour ce modèle, le nouveau champ est initialisé avec la valeur false. Tout abonné peut le changer et lui affecter la valeur true. Après le déclenchement de l’événement pour tous les abonnés, le composant FileSearcher examine la valeur booléenne et prend des mesures.
Le deuxième modèle annule l’opération uniquement si tous les abonnés souhaitent l’annuler. Dans ce modèle, le nouveau champ est initialisé pour indiquer que l’opération doit être annulée, et n’importe quel abonné peut le changer pour indiquer que l’opération doit continuer. Une fois que tous les abonnés traitent l’événement déclenché, le composant FileSearcher examine la valeur booléenne et prend des mesures. Il existe une étape supplémentaire dans ce modèle : le composant doit savoir si des abonnés ont répondu à l’événement. S’il n’y a pas d’abonnés, le champ indique incorrectement une annulation.
Implémentons la première version pour cet exemple. Vous devez ajouter un champ booléen nommé CancelRequested au type FileFoundArgs :
public class FileFoundArgs : EventArgs
{
public string FoundFile { get; }
public bool CancelRequested { get; set; }
public FileFoundArgs(string fileName) => FoundFile = fileName;
}
Ce nouveau champ est automatiquement initialisé pour false afin de ne pas annuler accidentellement. La seule autre modification apportée au composant consiste à vérifier l’indicateur après avoir levé l’événement pour voir si l’un des abonnés a demandé une annulation :
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;
}
}
L’un des avantages de ce modèle est qu’il ne s’agit pas d’un changement radical. Aucun des abonnés n’a demandé l’annulation avant, et ils ne le sont toujours pas. Aucun du code de l’abonné n’a besoin de mises à jour, sauf s’ils souhaitent prendre en charge le nouveau protocole d’annulation.
Nous allons maintenant mettre à jour l’abonné pour qu’il demande une annulation dès qu’il trouve le premier exécutable :
EventHandler<FileFoundArgs> onFileFound = (sender, eventArgs) =>
{
Console.WriteLine(eventArgs.FoundFile);
eventArgs.CancelRequested = true;
};
Ajout d’une autre déclaration d’événement
Nous allons ajouter une autre fonctionnalité et illustrer d’autres idiomes de langage pour les événements. Ajoutons une surcharge de la méthode Search qui parcourt tous les sous-répertoires à la recherche de fichiers.
Cette méthode peut être une opération longue dans un répertoire avec de nombreux sous-répertoires. Ajoutons un événement déclenché au début de chaque nouvelle recherche dans un répertoire. Cet événement permet aux abonnés de suivre la progression et de mettre à jour l'utilisateur concernant l'avancement. Tous les exemples que vous avez créés jusqu’à présent sont publics. Nous allons rendre cet événement un événement interne. Cela signifie que nous pouvons aussi rendre internes les types d’argument.
Vous commencez par créer la classe dérivée EventArgs pour signaler le nouveau répertoire et la progression.
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;
}
}
Là encore, nous pouvons suivre les recommandations pour créer un type référence immuable pour les arguments d’événements.
Maintenant, définissons l’événement. Cette fois, vous utilisez une syntaxe différente. Outre l’utilisation de la syntaxe de champ, vous pouvez créer explicitement la propriété d’événement avec des gestionnaires d’ajout et de suppression. Dans cet exemple, vous n’avez pas besoin de code supplémentaire dans ces gestionnaires, mais cela montre comment les créer.
internal event EventHandler<SearchDirectoryArgs> DirectoryChanged
{
add { _directoryChanged += value; }
remove { _directoryChanged -= value; }
}
private EventHandler<SearchDirectoryArgs>? _directoryChanged;
De nombreuses façons, le code que vous écrivez ici reflète le code généré par le compilateur pour les définitions d’événements de champ que vous avez vues précédemment. Vous créez l’événement à l’aide d’une syntaxe similaire à propriétés. Notez que les gestionnaires ont des noms différents : add et remove. Ces accesseurs sont appelés à s’abonner à l’événement ou à se désabonner de l’événement. Notez que vous devez également déclarer un champ de stockage privé pour stocker la variable d’événement. Cette variable est initialisée sur null.
Ensuite, nous allons ajouter la surcharge de la méthode Search qui parcourt les sous-répertoires et déclenche les deux événements. Le moyen le plus simple consiste à utiliser un argument par défaut pour spécifier que vous souhaitez rechercher dans tous les répertoires :
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;
}
}
À ce stade, nous pouvons exécuter l’application qui appelle la surcharge pour rechercher dans tous les sous-répertoires. Il n’existe aucun inscrit sur le nouvel événement DirectoryChanged, mais l’utilisation de l’idiome ?.Invoke() garantit que cela fonctionne correctement.
Ajoutons un gestionnaire pour écrire une ligne qui affiche la progression dans la fenêtre de la console.
fileLister.DirectoryChanged += (sender, eventArgs) =>
{
Console.Write($"Entering '{eventArgs.CurrentSearchDirectory}'.");
Console.WriteLine($" {eventArgs.CompletedDirs} of {eventArgs.TotalDirs} completed...");
};
Vous avez vu des modèles suivis dans l’écosystème .NET. En apprenant ces modèles et conventions, vous écrivez rapidement des modèles et des conventions idiomatiques C# et .NET.
Voir aussi
Ensuite, vous voyez quelques modifications dans ces modèles dans la version la plus récente de .NET.