Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

Chapitre 716 minIntermédiaire

async et await en WinForms : une interface qui ne se fige pas

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • expliquer pourquoi une fenêtre affiche « Ne répond pas » ;
  • écrire un gestionnaire d'événement async qui attend une opération longue ;
  • distinguer une attente d'entrée-sortie d'un calcul à déplacer avec Task.Run ;
  • afficher une progression et proposer une annulation ;
  • éviter le blocage avec .Result et l'erreur d'accès inter-threads.

4 exercices corrigés1 schéma

async et await sont les mots-clés de C# qui permettent d'attendre la fin d'une opération longue (une requête à la base, un appel à une API, la lecture d'un gros fichier) sans bloquer le programme pendant l'attente. Dans une application WinForms, ils évitent que la fenêtre se fige et que Windows affiche « (Ne répond pas) » dans sa barre de titre. Toutes les bibliothèques modernes d'accès aux données en dépendent : ce chapitre vient donc avant la base de données.

Pourquoi la fenêtre se fige

Souviens-toi de la boucle de messages : un seul message est traité à la fois, sur un seul fil d'exécution, le thread de l'interface (UI thread). Si ton gestionnaire de clic attend trois secondes la réponse du serveur MySQL, la boucle est bloquée trois secondes : la fenêtre ne se redessine plus, les clics s'accumulent, et au bout de quelques secondes Windows grise la fenêtre.

csharp
// À NE PAS FAIRE : la fenêtre est figée pendant 3 secondes
private void btnCharger_Click(object? sender, EventArgs e)
{
    Thread.Sleep(3000);   // simule une requête lente
    lblStatut.Text = "Chargé";
}

La solution : rendre la main pendant l'attente

Avec await, la méthode se met en pause à l'endroit de l'attente et rend la main à la boucle de messages. La fenêtre continue de vivre. Quand l'opération est terminée, la suite de la méthode reprend, sur le thread de l'interface : tu peux modifier les contrôles sans précaution.

csharp
private async void btnCharger_Click(object? sender, EventArgs e)
{
    lblStatut.Text = "Chargement…";
    await Task.Delay(3000);   // simule une requête lente, sans bloquer
    lblStatut.Text = "Chargé";
}
Sans await : appel synchrone (Thread.Sleep, Open, ExecuteReader)thread UIclicle gestionnaire bloque le threadclics et redessins en retardfenêtre figée : « Ne répond pas »Avec awaitthread UIcliclanceredessinclicredessinsuite : remplit la grilleréseaurequête MySQL en attente0 s3 stemps
Sans await, le thread de l'interface est bloqué ; avec await, il reste libre pendant l'attente

Les trois règles d'écriture :

  1. Une méthode qui contient await est marquée async.
  2. Elle renvoie Task (rien à renvoyer) ou Task<T> (un résultat de type T), et son nom se termine par Async par convention : ChargerAsync, ListerAsync.
  3. Un appelant attend son résultat avec await : List<Patient> liste = await _service.ListerAsync();.
csharp
private async Task ChargerAsync()
{
    btnActualiser.Enabled = false;
    lblStatut.Text = "Chargement…";
    try
    {
        _tous = await _service.ListerAsync();   // la fenêtre reste réactive pendant la requête
        Filtrer();
    }
    catch (Exception ex)
    {
        lblStatut.Text = "Erreur de chargement";
        MessageBox.Show($"Impossible de charger les patients.\n{ex.Message}", "Erreur",
            MessageBoxButtons.OK, MessageBoxIcon.Error);
    }
    finally
    {
        btnActualiser.Enabled = true;   // exécuté dans tous les cas
    }
}

private async void FrmPatients_Load(object? sender, EventArgs e) => await ChargerAsync();
private async void btnActualiser_Click(object? sender, EventArgs e) => await ChargerAsync();

Désactiver le bouton pendant le chargement empêche l'utilisateur impatient de lancer dix requêtes à la suite. Le bloc finally le réactive, même en cas d'erreur.

async void : seulement pour les gestionnaires d'événements

Les gestionnaires d'événements doivent garder leur signature (void), d'où async void. C'est le seul cas où l'on écrit async void. Partout ailleurs, une méthode asynchrone renvoie Task, pour que son appelant puisse l'attendre et récupérer ses erreurs.

async Taskasync void
L'appelant peut l'attendreOui, avec awaitNon
Une exception non interceptéeremonte à l'appelant, au awaitremonte jusqu'à la boucle de messages : WinForms affiche la boîte « Exception non gérée »
Usagetoutes tes méthodesuniquement les gestionnaires d'événements

Le piège mortel : .Result et .Wait()

Il est tentant d'écrire var liste = _service.ListerAsync().Result; pour obtenir le résultat sans transformer la méthode en async. Dans une application WinForms, c'est un interblocage (deadlock) presque assuré : .Result bloque le thread de l'interface en attendant la fin de la tâche ; la tâche, une fois la réponse reçue, veut reprendre sa suite sur le thread de l'interface… qui est bloqué. Chacun attend l'autre, la fenêtre reste figée pour toujours.

La règle : async jusqu'en haut. Si une méthode appelle une méthode asynchrone, elle devient asynchrone à son tour, jusqu'au gestionnaire d'événement.

Lancer deux opérations en même temps : Task.WhenAll

À l'ouverture de la fenêtre d'ordonnance, il faut la liste des médecins et celle des patients. Les attendre l'une après l'autre additionne les deux durées. On peut lancer les deux requêtes, puis attendre qu'elles soient toutes les deux terminées :

csharp
private async void FrmOrdonnance_Load(object? sender, EventArgs e)
{
    Task<List<Medecin>> tacheMedecins = _medecinService.ListerAsync();   // lancée
    Task<List<Patient>> tachePatients = _patientService.ListerAsync();   // lancée aussi
    await Task.WhenAll(tacheMedecins, tachePatients);                    // attend les deux

    cboMedecin.DataSource = await tacheMedecins;   // déjà terminée : ne fait que lire le résultat
    cboPatient.DataSource = await tachePatients;
}

Appeler une méthode …Async sans await lance l'opération et renvoie une Task en cours ; c'est le await qui attend. Ici, c'est possible parce que chaque méthode du DAO ouvre sa propre connexion : deux requêtes ne peuvent pas partager en même temps une même connexion MySQL, ni un même DbContext d'Entity Framework.

Attendre ou calculer : Task.Run

await libère le thread pendant une attente : réseau, base de données, disque. Ces opérations ne consomment pas de processeur pendant qu'elles attendent, et les bibliothèques fournissent leurs versions …Async (OpenAsync, ExecuteReaderAsync, GetFromJsonAsync, File.ReadAllTextAsync).

Un calcul long (parcourir 100 000 lignes, générer un PDF, hacher des mots de passe), lui, occupe le processeur. Marquer la méthode async ne suffit pas : il faut l'exécuter sur un autre thread avec Task.Run, et attendre son résultat.

csharp
private async void btnStatistiques_Click(object? sender, EventArgs e)
{
    btnStatistiques.Enabled = false;
    StatistiquesOrdonnances stats = await Task.Run(() => CalculerStatistiques(_ordonnances));
    lblStatut.Text = $"{stats.Nombre} ordonnances, {stats.MoyenneLignes:N1} médicaments en moyenne";
    btnStatistiques.Enabled = true;
}

Le code passé à Task.Run s'exécute sur un thread du pool : il ne doit pas toucher aux contrôles. Il calcule et renvoie un résultat ; c'est la suite, après le await, de retour sur le thread de l'interface, qui l'affiche.

Afficher une progression

IProgress<T> transmet des nouvelles d'une tâche de fond vers l'interface, en toute sécurité. L'objet Progress<T>, créé sur le thread de l'interface, y exécute son action à chaque Report :

csharp
private async void btnAnalyser_Click(object? sender, EventArgs e)
{
    btnAnalyser.Enabled = false;
    var progression = new Progress<int>(pourcent => barProgression.Value = pourcent);
    int total = await Task.Run(() => AnalyserFichier(progression));
    lblStatut.Text = $"{total} lignes analysées";
    btnAnalyser.Enabled = true;
}

private static int AnalyserFichier(IProgress<int> progression)
{
    int total = 0;
    for (int i = 1; i <= 100; i++)
    {
        total += TraiterBloc(i);   // un centième du travail
        progression.Report(i);     // 1 %, 2 %… jusqu'à 100 %
    }
    return total;
}

Proposer une annulation

Une recherche qui traîne doit pouvoir être annulée. Le mécanisme standard est le jeton d'annulation : un CancellationTokenSource crée un CancellationToken, que l'on passe aux méthodes …Async. Appeler Cancel() les interrompt avec une OperationCanceledException.

csharp
private CancellationTokenSource? _annulation;

private async void btnRechercher_Click(object? sender, EventArgs e)
{
    _annulation = new CancellationTokenSource();
    btnAnnuler.Enabled = true;
    try
    {
        dgvMedicaments.DataSource = await _api.RechercherAsync(txtNom.Text, _annulation.Token);
    }
    catch (OperationCanceledException)
    {
        lblStatut.Text = "Recherche annulée.";
    }
    finally
    {
        btnAnnuler.Enabled = false;
    }
}

private void btnAnnuler_Click(object? sender, EventArgs e) => _annulation?.Cancel();

Le chapitre 11 reprend cet exemple complet, avec le délai d'attente de HttpClient.

L'erreur « opération inter-threads non valide »

Si du code qui s'exécute sur un autre thread (dans un Task.Run, dans un System.Threading.Timer) modifie un contrôle, WinForms lève une InvalidOperationException : « Opération inter-threads non valide : le contrôle 'lblStatut' a fait l'objet d'un accès à partir d'un thread autre que celui sur lequel il a été créé ». Les contrôles ne doivent être touchés que par le thread de l'interface.

Trois solutions, de la meilleure à la moins bonne :

  1. Renvoyer un résultat depuis Task.Run et l'afficher après le await (le cas ordinaire, ci-dessus) ;
  2. IProgress<T> pour les nouvelles intermédiaires ;
  3. Invoke en dernier recours, pour exécuter un bout de code sur le thread de l'interface : lblStatut.Invoke(() => lblStatut.Text = "Terminé");.

Exercices

Exercice 1 sur 4 Prévoir l'affichage Réussi À revoir

Le label lblStatut est vide au départ. L'utilisateur clique une fois sur le bouton. Qu'affiche le label juste après le clic, puis au bout de deux secondes ? La fenêtre peut-elle être déplacée pendant ces deux secondes ?

csharp
private async void btnTest_Click(object? sender, EventArgs e)
{
    lblStatut.Text = "A";
    await Task.Delay(2000);
    lblStatut.Text += "B";
}
Voir la réponseMasquer la réponse de l'exercice 1

Juste après le clic, « A » : la méthode s'exécute jusqu'au await, puis rend la main, et la boucle de messages redessine le label. Au bout de deux secondes, la suite reprend sur le thread de l'interface : « AB ». Pendant l'attente, la fenêtre reste réactive : on peut la déplacer, et même recliquer sur le bouton, ce qui relancerait la méthode (d'où l'intérêt de désactiver le bouton).

Tu avais trouvé ?

Exercice 2 sur 4 La fenêtre figée Réussi À revoir

Ce code fige l'application au premier clic, définitivement. Explique pourquoi et corrige-le.

csharp
private void btnCharger_Click(object? sender, EventArgs e)
{
    List<Patient> patients = _service.ListerAsync().Result;
    dgvPatients.DataSource = patients;
}
Voir la réponseMasquer la réponse de l'exercice 2

.Result bloque le thread de l'interface jusqu'à la fin de ListerAsync. Mais la suite de ListerAsync, après son propre await, doit reprendre sur ce même thread, qui est bloqué : interblocage. La correction est d'attendre avec await, ce qui rend la main pendant la requête :

csharp
private async void btnCharger_Click(object? sender, EventArgs e)
{
    try
    {
        dgvPatients.DataSource = await _service.ListerAsync();
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message, "Erreur", MessageBoxButtons.OK, MessageBoxIcon.Error);
    }
}

Tu avais trouvé ?

Exercice 3 sur 4 Await ou Task.Run ? Réussi À revoir

Pour chaque opération, dis si tu l'attends directement avec await (attente d'entrée-sortie) ou si tu la places dans un Task.Run (calcul).

  1. await conn.OpenAsync(), l'ouverture d'une connexion MySQL.
  2. Le hachage BCrypt d'un mot de passe, qui prend environ 250 ms de calcul.
  3. Le téléchargement de la liste des médicaments depuis l'API.
  4. Le calcul de la durée moyenne de traitement sur 200 000 lignes déjà chargées en mémoire.
  5. La lecture d'un fichier CSV de 5 Mo avec File.ReadAllLinesAsync.
Voir la réponseMasquer la réponse de l'exercice 3
  1. await : attente réseau, la méthode est déjà asynchrone.
  2. Task.Run : c'est du calcul pur, il bloquerait le thread de l'interface un quart de seconde.
  3. await : attente réseau (GetFromJsonAsync).
  4. Task.Run : calcul sur des données en mémoire.
  5. await : lecture disque, la méthode …Async existe. Le découpage des lignes qui suit, s'il est lourd, peut aller dans un Task.Run.

Tu avais trouvé ?

Exercice 4 sur 4 Trouve le bug Réussi À revoir

L'application plante avec « Opération inter-threads non valide ». Corrige ce code sans utiliser Invoke.

csharp
private async void btnCompter_Click(object? sender, EventArgs e)
{
    await Task.Run(() =>
    {
        int n = _ordonnances.Count(o => o.DateEmission.Year == 2026);
        lblStatut.Text = $"{n} ordonnances en 2026";
    });
}
Voir la réponseMasquer la réponse de l'exercice 4

Le label est modifié dans le code passé à Task.Run, donc sur un thread du pool. La tâche doit seulement calculer et renvoyer le résultat ; l'affichage se fait après le await, de retour sur le thread de l'interface :

csharp
private async void btnCompter_Click(object? sender, EventArgs e)
{
    int n = await Task.Run(() => _ordonnances.Count(o => o.DateEmission.Year == 2026));
    lblStatut.Text = $"{n} ordonnances en 2026";
}

Tu avais trouvé ?

Chapitre terminé !

Fais les 4 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Architecture en couches : interface, métier, accès aux donnéesRevoir le sommaire du cours