CodeGym /Cours /C# SELF /Exemples de programmation événementielle ( event)

Exemples de programmation événementielle ( event)

C# SELF
Niveau 54, Leçon 4
Disponible

1. Introduction

Un débutant typique peut demander : « Où utilise-t-on les événements en pratique ? Est-ce que toutes les interactions entre classes se font via des événements et non par des appels directs de méthodes ? » La réponse est simple : les événements et les delegates ne sont pas une baguette magique qui règle tous les problèmes d'architecture. Mais sans eux, une application moderne devient vite trop couplée (« tightly coupled »), et modifier un composant oblige souvent à changer beaucoup d'autres choses. L'utilisation des événements et des delegates aide à créer des systèmes flexibles, extensibles et faciles à maintenir.

  • Programmation UI (WinForms, WPF, Xamarin, MAUI) : gestion des clics, hover, saisie de texte et autres actions utilisateur.
  • Opérations asynchrones : fin de téléchargement de fichier, réception de données réseau, déclenchement de timers.
  • Architecture de plugins et systèmes extensibles : permet d'ajouter de nouveaux modules sans les intégrer rigidement dans le code principal.
  • Systèmes de signaux / de diffusion : notifier plusieurs composants intéressés qu'un événement est survenu.
  • Observation de changements d'état (« observateur ») : réagir à l'arrivée d'un nouveau message, à une modification de données, à une mise à jour d'UI.

Allez, au boulot !

2. Squelette architectural

Imaginons que vous développez une petite application console — un "Mini-chat" pour la formation interne d'une boîte (ou juste pour vous entraîner). On a des entités : User, Chat et éventuellement un Bot. Quand un utilisateur envoie un message, le chat doit avertir tous les utilisateurs et les bots connectés — pour qu'ils affichent le message ou, pour le bot, génèrent une réponse automatique. C'est le scénario classique : le publisher émet un événement (event), les handlers s'abonnent via EventHandler/EventHandler<TEventArgs>, et une méthode comme OnMessageReceived réagit à la notification.


// Classe utilisateur (abonné)
public class User
{
    public string Name { get; }

    public User(string name)
    {
        Name = name;
    }

    // Méthode qui sera le handler de l'événement
    public void OnMessageReceived(object? sender, MessageEventArgs e)
    {
        Console.WriteLine($"[{Name}] a vu un nouveau message: {e.MessageText}");
    }
}

// Arguments de l'événement
public class MessageEventArgs : EventArgs
{
    public string MessageText { get; }

    public MessageEventArgs(string text) => MessageText = text;
}

// Classe chat (publisher)
public class ChatRoom
{
    public event EventHandler<MessageEventArgs>? MessageReceived;

    public void SendMessage(string text)
    {
        // On déclenche l'événement (on notifie tous les abonnés)
        MessageReceived?.Invoke(this, new MessageEventArgs(text));
    }
}

Exemple d'utilisation :

var chat = new ChatRoom();
var user1 = new User("Anton");
var user2 = new User("Maria");

chat.MessageReceived += user1.OnMessageReceived;
chat.MessageReceived += user2.OnMessageReceived;

chat.SendMessage("Salut tout le monde ! 😊");

Dans la console, vous verrez deux messages — les deux utilisateurs ont reçu la notification du nouveau message.

3. Abonnement et désabonnement dynamiques

En pratique, un utilisateur peut quitter le chat et ne plus vouloir recevoir de messages. Apprenons à permettre au user de se désabonner proprement (opérateur -=) :

// Dans la classe User on peut ajouter la méthode "Quitter le chat"
public void Unsubscribe(ChatRoom chat)
{
    chat.MessageReceived -= OnMessageReceived;
}

On développe l'exemple :

var chat = new ChatRoom();
var user1 = new User("Anton");
var user2 = new User("Maria");

chat.MessageReceived += user1.OnMessageReceived;
chat.MessageReceived += user2.OnMessageReceived;

chat.SendMessage("Première nouvelle");

user2.Unsubscribe(chat); // Maria quitte le chat

chat.SendMessage("Maria ne verra plus ce message");

L'abonnement/désabonnement dynamique est partout : fenêtres qui se ferment, onglets qui disparaissent, services temporaires qui se désabonnent de sources globales d'événements. N'oubliez pas — un abonné non désabonné devient un zombie et l'application peut fuiter de la mémoire !

4. Traitement des messages et génération de réponses

Ajoutons maintenant un bot qui réagit à chaque message. Imaginons que le bot dise "Salut !" automatiquement si le message contient le mot "bot". On verra aussi la notion d'abonnement multiple et le comportement des delegates multi-cibles.

public class Bot
{
    public string Name { get; }

    public Bot(string name) => Name = name;

    public void OnMessageReceived(object? sender, MessageEventArgs e)
    {
        // Le bot réagit au mot-clé
        if (e.MessageText.Contains("bot", StringComparison.OrdinalIgnoreCase))
        {
            if (sender is ChatRoom chatRoom)
            {
                Console.WriteLine($"[Bot {Name}]: Bonjour ! Comment puis-je aider ?");
                // Le bot envoie un message en réponse
                chatRoom.SendMessage($"Le bot {Name} est prêt à vous aider.");
            }
        }
    }
}

Utilisation :

var chat = new ChatRoom();
var user = new User("Evgeniy");
var bot = new Bot("Assistant");

chat.MessageReceived += user.OnMessageReceived;
chat.MessageReceived += bot.OnMessageReceived;

// Evgeniy envoie un message qui déclenche le bot
chat.SendMessage("Salut, bot, comment ça va ?");

La console affichera (on montre l'ordre d'appel — qui n'est pas garanti !) :

[Evgeniy] a vu un nouveau message: Salut, bot, comment ça va ?
[Bot Assistant]: Bonjour ! Comment puis-je aider ?
[Evgeniy] a vu un nouveau message: Le bot Assistant est prêt à vous aider.
[Bot Assistant]: Bonjour ! Comment puis-je aider ?
[Evgeniy] a vu un nouveau message: Le bot Assistant est prêt à vous aider.
[Bot Assistant]: Bonjour ! Comment puis-je aider ?
...

Vous voyez le bug potentiel ? Exact, une boucle infinie : le bot réagit à ses propres messages (il voit encore le mot "bot"). Une manière d'éviter ça est d'ajouter une simple vérification :

public void OnMessageReceived(object? sender, MessageEventArgs e)
{
    // Le bot ne réagit pas à ses propres messages
    if (e.MessageText.Contains("bot", StringComparison.OrdinalIgnoreCase) &&
        !e.MessageText.Contains(Name))
    {
        if (sender is ChatRoom chatRoom)
        {
            Console.WriteLine($"[Bot {Name}]: Bonjour ! Comment puis-je aider ?");
            chatRoom.SendMessage($"Le bot {Name} est prêt à vous aider.");
        }
    }
}

Dans la pratique, ces situations sont une excellente occasion de réfléchir à la gestion des événements cycliques, aux expressions lambda ou même à un mécanisme d'annulation des handlers suivants (voir les sections ci-dessous).

5. Opérations asynchrones et callbacks

Très souvent, les événements servent à annoncer la fin d'une opération asynchrone. Par exemple, le téléchargement de données depuis Internet ou un calcul long : l'événement Completed signale la fin, et la méthode RunLongOperationAsync exécute l'opération longue.

public class LongRunner
{
    // Événement de fin d'opération
    public event EventHandler<EventArgs>? Completed;

    public async Task RunLongOperationAsync()
    {
        Console.WriteLine("Opération longue commencée...");
        await Task.Delay(2000); // Simulation de travail long
        Console.WriteLine("Opération terminée, on prévient les abonnés.");
        Completed?.Invoke(this, EventArgs.Empty);
    }
}

Code client :

var runner = new LongRunner();

// Abonnement à l'événement de fin
runner.Completed += (sender, e) =>
{
    Console.WriteLine("Notification reçue : l'opération est terminée !");
};

await runner.RunLongOperationAsync();

C'est la base des interactions asynchrones — les événements sont souvent utilisés dans les frameworks UI pour signaler la fin d'un chargement, la fin d'une animation, un clic, etc.

6. Systèmes de signaux

Regardons une autre tâche classique : un système de notifications (par exemple dans un e-shop quand un produit est en promo). Tous les "listeners" intéressés apprennent la nouvelle via l'événement 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}] a reçu la notification : {e.Product} coûte maintenant {e.NewPrice.ToString(\"F2\")} u.e. !");
    }
}

Utilisation :

var notifier = new SaleNotifier();
var c1 = new Customer("Andrei");
var c2 = new Customer("Olga");

notifier.SaleOccurred += c1.OnSale;
notifier.SaleOccurred += c2.OnSale;

notifier.AnnounceSale("Bouilloire", 999.99m);

Si un client n'est plus intéressé — on le désabonne :

notifier.SaleOccurred -= c2.OnSale;
notifier.AnnounceSale("Mixeur", 1999.99m);

Ce pattern est utilisé pour la diffusion de signaux (notification, pub/sub), très important dans les architectures modernes.

7. Implémentation d’un mécanisme d’annulation d’une chaîne d’événements

Parfois, l'un des handlers doit "arrêter" la notification aux handlers suivants. On utilise habituellement un descendant de EventArgs avec un flag d'annulation. Dans l'exemple ci-dessous on itère manuellement sur GetInvocationList() et on stoppe l'exécution quand 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();
        // Boucle classique pour parcourir les abonnés (si on a besoin de contrôler l'ordre)
        var handlers = SomethingHappened?.GetInvocationList();
        if (handlers != null)
        {
            foreach (var handler in handlers)
            {
                ((EventHandler<CancelEventArgs>)handler)(this, args);
                if (args.Cancel)
                {
                    Console.WriteLine("La chaîne de notifications a été interrompue.");
                    break;
                }
            }
        }
    }
}

Utilisation :

var source = new EventSource();
source.SomethingHappened += (s, e) =>
{
    Console.WriteLine("Premier gestionnaire");
};
source.SomethingHappened += (s, e) =>
{
    Console.WriteLine("Le deuxième gestionnaire annule l'événement.");
    e.Cancel = true;
};
source.SomethingHappened += (s, e) =>
{
    Console.WriteLine("Ce gestionnaire ne sera pas appelé.");
};

source.DoSomething();

Résultat :

Premier gestionnaire
Le deuxième gestionnaire annule l'événement.
La chaîne de notifications a été interrompue.

Ce mécanisme est utile pour la validation, le traitement d'événements avant la fermeture d'une fenêtre, la vérification des permissions et d'autres tâches où il faut pouvoir dire "Stop ! On n'enchaîne pas."

8. Sécurité des threads et gestion des abonnés

Dans les applications multithread où des abonnés peuvent être ajoutés ou retirés pendant que l'événement est déclenché, il est important d'utiliser des patterns thread-safe (voir la documentation officielle). Pour contrôler l'accès on peut implémenter l'événement manuellement avec les accessors add et remove et protéger l'accès avec lock :

public class CustomEvent
{
    private EventHandler? _handlers;

    public event EventHandler SomethingHappened
    {
        add
        {
            lock (this) // Sécurisé pour les threads
            {
                _handlers += value;
            }
        }
        remove
        {
            lock (this)
            {
                _handlers -= value;
            }
        }
    }

    protected void RaiseEvent()
    {
        // On utilise une copie
        EventHandler? handler;
        lock (this)
        {
            handler = _handlers;
        }
        handler?.Invoke(this, EventArgs.Empty);
    }
}

Cette approche est rare (le mécanisme standard suffit la plupart du temps), mais parfois nécessaire pour des systèmes à haute charge.

9. Événements dans l’interface utilisateur — de WinForms/WPF à MAUI

WinForms :

private void button1_Click(object sender, EventArgs e)
{
    MessageBox.Show("Bouton cliqué !");
}

Derrière cette méthode se cache le mécanisme d'abonnement :

button1.Click += button1_Click;

WPF/MAUI : événements de type PropertyChanged (notifications de changement de propriétés du modèle) :

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;
}

Les frameworks utilisent beaucoup les événements pour construire des interfaces réactives (MVVM).

10. Erreurs fréquentes et pièges courants lors de l’utilisation des événements

Erreur n°1 : créer des fuites mémoire.
C'est le problème le plus fréquent. Si un objet de courte durée de vie (par ex. une fenêtre ou un service temporaire) s'abonne à un objet de longue durée (par ex. un cache global ou une classe statique) et ne se désabonne pas à sa destruction, il restera en mémoire indéfiniment. Implémentez toujours le désabonnement dans Dispose ou dans un autre point approprié du cycle de vie de l'objet.

Erreur n°2 : créer des boucles infinies d'événements.
Un abonné peut réagir à un événement en effectuant une action qui réémet le même événement. Cela peut provoquer une récursion infinie et faire planter l'app avec un StackOverflowException. Vérifiez toujours les conditions pour que le handler ne réagisse pas aux événements qu'il a lui-même provoqués.

Erreur n°3 : dépendre de l'ordre d'appel des abonnés.
Ne comptez jamais sur le fait que les handlers seront appelés dans l'ordre d'abonnement. La spécification C# et l'implémentation CLR ne le garantissent pas. Si l'ordre est important, appelez les abonnés manuellement via GetInvocationList() dans la séquence voulue.

Erreur n°4 : unsafe handling des événements en multithread.
Si des abonnements sont ajoutés ou retirés depuis un thread et qu'un événement est invoqué depuis un autre thread, des races peuvent apparaître. La protection classique : copier le delegate dans une variable locale avant l'appel :

var handler = MyEvent;
handler?.Invoke(this, EventArgs.Empty);

Ou utiliser lock quand on implémente manuellement add/remove.

2
Mission
C# SELF, niveau 54, leçon 4
Bloqué
Système de notifications de soldes
Système de notifications de soldes
1
Étude/Quiz
Erreurs typiques avec les delegates, niveau 54, leçon 4
Indisponible
Erreurs typiques avec les delegates
Meilleures pratiques de la programmation événementielle
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION