1. Scénarios pratiques d'utilisation des événements
Les événements ne sont pas seulement une jolie théorie de manuel. En pratique, ils sont nécessaires dans presque une application sur deux, et si vous travaillez avec UI ou des services réseau — dans la plupart des cas.
Voyons quelques scénarios typiques issus du développement réel. En même temps, regardons comment les standards et les bonnes pratiques aident à éviter les erreurs courantes.
Réagir aux actions de l'utilisateur (comme dans l'UI)
Probablement le scénario le plus fréquent — quand l'utilisateur fait quelque chose (clique sur un bouton, sélectionne un élément dans une liste) et l'application doit réagir. C'est exactement ainsi que fonctionnent Windows Forms, WPF, UWP, Avalonia, MAUI et autres frameworks UI.
Mini-exemple (sans UI — on simule un bouton) :
public class Button
{
public event EventHandler? Click; // standard delegate
public void SimulateClick()
{
Click?.Invoke(this, EventArgs.Empty); // le "bouton" a été "cliqué"
}
}
class Program
{
static void Main()
{
var button = new Button();
button.Click += (sender, e) => Console.WriteLine("Bouton cliqué !");
button.SimulateClick(); // > Bouton cliqué !
}
}
Dans un vrai framework UI, l'événement Click se déclenche quand l'utilisateur clique la souris ou tape l'écran. Tout ce qui s'est abonné à cet événement recevra la notification — c'est ainsi que fonctionne toute la logique des applications.
Signaler la progression, l'achèvement d'opérations et les erreurs
Imaginez : téléchargement de fichier, traitement de données, calcul long. Il faut informer de la progression ou des erreurs. On organise souvent un événement pour chaque « pas de progression » et un événement séparé pour « terminé » ou « erreur ».
Exemple de téléchargeur de fichiers :
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); // imitation de délai
ProgressChanged?.Invoke(this, i);
}
DownloadCompleted?.Invoke(this, EventArgs.Empty);
}
}
var downloader = new FileDownloader();
downloader.ProgressChanged += (s, progress) => Console.WriteLine($"Téléchargement : {progress}%");
downloader.DownloadCompleted += (s, e) => Console.WriteLine("Téléchargement terminé.");
downloader.Download();
Ici plusieurs événements coexistent : on peut s'abonner à tout (ou seulement à ce qui est nécessaire). C'est semblable au comportement de nombreux classes .NET standard pour les threads, le téléchargement de fichiers, le travail avec HTTP, etc.
Réaction au cycle de vie des objets (par exemple, « sauvegardé », « supprimé »)
Supposons que vous ayez un modèle de domaine. Vous voulez que lorsque l'utilisateur sauvegarde ou supprime un objet, certains processus réagissent : mettre à jour le cache, le log, envoyer un message, etc.
public class UserRepository
{
public event EventHandler<UserEventArgs>? UserSaved;
public event EventHandler<UserEventArgs>? UserDeleted;
public void Save(User user)
{
//... on sauvegarde l'utilisateur
UserSaved?.Invoke(this, new UserEventArgs(user));
}
public void Delete(User user)
{
//... on supprime l'utilisateur
UserDeleted?.Invoke(this, new UserEventArgs(user));
}
}
public class UserEventArgs : EventArgs
{
public User User { get; }
public UserEventArgs(User user) => User = user;
}
Maintenant, différents modules peuvent s'abonner et — sans liens directs ! — recevoir des notifications sur toutes les modifications des utilisateurs. De plus, le repository lui‑même ne sait pas qui est « connecté ».
Événements asynchrones et multithreading
Parfois, les événements sont générés en dehors du thread principal (UI) — par exemple depuis des tâches en arrière‑plan, des timers ou des opérations asynchrones. Dans ces cas, il est important de se rappeler que le code des handlers s'exécutera dans un thread « étranger ». Si un handler tente de mettre à jour l'interface directement depuis un autre thread — cela provoquera des erreurs.
Qu'est-ce que le marshalling ? Le marshalling est le transfert de l'exécution du code d'un thread à un autre, généralement du thread en arrière‑plan vers le thread UI, afin de mettre à jour l'interface en toute sécurité. Dans les applications UI (WinForms, WPF), on utilise des mécanismes comme SynchronizationContext ou Dispatcher qui permettent de « rediriger » l'appel du handler vers le thread voulu.
Ne faites pas ceci :
// L'événement est appelé depuis un thread en arrière‑plan, et le handler met à jour l'UI directement — vous obtiendrez une exception !
Recommandé : vérifier le thread d'exécution et si nécessaire faire le marshalling vers le thread UI (via SynchronizationContext ou Dispatcher). Dans les applications console et serveur, ces contraintes n'existent généralement pas.
Event Aggregator / Messaging
Dans les grandes applications, on utilise souvent un agrégateur d'événements centralisé (Event Aggregator). Il réduit le couplage : les abonnés et les éditeurs ne se connaissent pas et échangent des messages via un hub.
public class EventAggregator
{
public event EventHandler<SomeEventArgs>? SomeEvent;
public void Publish(SomeEventArgs args)
{
SomeEvent?.Invoke(this, args);
}
}
2. Meilleurs patterns et pratiques
Utilisez l'attribut [CallerMemberName] pour les erreurs
Si vous créez du code d'infrastructure (par exemple des loggers, des traceurs), logguez le nom de la méthode‑source de l'événement en utilisant l'attribut CallerMemberName — cela simplifie le diagnostic.
Essayez de rendre les événements thread-safe, si nécessaire
Les événements sont multicast, et les accès depuis différents threads demandent de la prudence : avant d'appeler, copiez le délégué dans une variable locale.
var handler = MyEvent;
if (handler != null) handler(this, args);
En C# 6+ utilisez l'appel sûr : MyEvent?.Invoke(this, args).
Faites vos EventArgs personnalisés immuables
Définissez vos types EventArgs avec des propriétés en lecture seule — cela évite la modification accidentelle de l'état de l'événement par les abonnés.
public class WorkCompletedEventArgs : EventArgs
{
public string Message { get; }
public WorkCompletedEventArgs(string message) => Message = message;
}
public ou private event
Déclarez les événements public event, et encapsulez l'appel dans une méthode protected virtual OnEventName. Cela assure l'extensibilité (un héritier peut override le comportement), et les consommateurs externes ne pourront pas déclencher l'événement directement.
Ne rompez pas la séquence du cycle de vie
Si vous lancez une longue chaîne de handlers, rappelez‑vous : quelqu'un peut s'abonner et tenter de modifier la logique interne. Documentez où et quand les événements sont appelés, et si un événement peut se déclencher plusieurs fois.
Événements multiples et composition
Quand un objet écoute plusieurs sources, regroupez l'abonnement et la désinscription au même endroit (par exemple les méthodes Subscribe/Unsubscribe). Si nécessaire, conservez des champs délégués privés pour faciliter la désinscription.
3. Comment NE PAS utiliser les événements : erreurs typiques
Erreur №1 : ignorer le pattern d'événements standard.
Si chaque classe déclare ses propres délégués pour chaque événement, cela devient vite le chaos. Essayez d'utiliser les délégués standard EventHandler ou EventHandler<T>, où T hérite de EventArgs. Cette approche rend le code plus clair et familière aux développeurs .NET.
Erreur №2 : appel incorrect d'un événement depuis du code externe.
N'appelez jamais un événement directement en dehors de la classe éditeur. L'événement ne doit être initié qu'à l'intérieur de la classe, généralement via une méthode protégée OnEventName. Les abonnés ont seulement le droit de s'abonner (+=) et de se désabonner (-=).
Mauvais :
foo.MyEvent(); // erreur ! On ne peut pas appeler un événement depuis l'extérieur directement
Bien :
// Seulement à l'intérieur de foo :
// protected virtual void OnMyEvent()
// {
// MyEvent?.Invoke(this, EventArgs.Empty);
// }
Erreur №3 : appeler un événement sans vérifier la présence d'abonnés (null).
Si vous tentez d'appeler un événement qui n'a pas d'abonnés, vous obtiendrez un NullReferenceException. En C# 6.0+ utilisez ?.Invoke(...) — l'événement ne sera appelé que s'il y a des abonnés.
Erreur №4 : oublier de se désabonner d'un événement (fuite de mémoire).
Si un objet s'est abonné à un événement mais ne se désabonne pas avant d'être détruit, l'éditeur garde la référence vers l'abonné. Le garbage collector ne pourra pas libérer la mémoire, ce qui est critique pour des éditeurs longue durée et des abonnés de courte durée (par ex. ViewModel, forms). La meilleure solution est la désinscription explicite ou l'utilisation de références faibles (WeakReference) quand c'est approprié.
Erreur №5 : appeler l'événement en dehors de la méthode protégée virtuelle OnEvent.
En appelant l'événement directement, vous empêchez les héritiers d'override correctement le comportement. Le pattern correct est de déclarer une méthode protected virtual qui appelle l'événement.
Exemple standard :
protected virtual void OnSomething(EventArgs e)
{
Something?.Invoke(this, e);
}
GO TO FULL VERSION