Framework 2.0 : Présentation du composant BackgroundWorker (C#)

LARDE Geoffrey

Dans cet article, je présente le composant BackgroundWorker. Je détaille ensuite sa création ainsi que son utilisation au travers d'un exemple concret.

Cet article est aussi disponible en VB.Net.

N'hésitez pas à commenter cet article ! Commentez ♪

- Titre : Framework 2.0 : Présentation du composant BackgroundWorker (C#)

- Auteur : LARDE Geoffrey

- Parution : 28 septembre 2005

- Mise à jour : 5 octobre 2005

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2005 Geoffrey LARDE. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- Introduction

- Présentation

- Le composant BackgroundWorker

- Création

- Designer

- Code

- Utilisation

- Description

- Débuter le traitement

- Visualiser l'état d'avancement

- Annuler le traitement

- Afficher le résultat

- Conclusion

- Ressources

- Remerciements

Introduction

Qui ne s'est jamais heurté, lors d'opérations longues telles que les téléchargements, les accès à une base de données ou encore à un gros calcul, à un freeze de l'interface utilisateur ? Vous trouvez le multithreading délicat et laissez donc vos utilisateurs bouche bée devant une interface utilisateur figée et qui ne répond plus ? Pas de problème, le framework 2.0 possède un composant qui correspond à vos besoins : le BackgroundWorker.

Présentation

Le composant BackgroundWorker

Le BackgroundWorker est un composant qui permet de réaliser un traitement lourd et consommateur de temps dans un thread séparé et dédié et ainsi éviter un freeze de l'interface utilisateur.

Création

Le composant BackgroundWorker peut, comme tous les composants .Net se créer soit à l'aide du designer soit directement au travers du code. Voici une présentation des deux méthodes.

Designer

Pour créer un BackgroundWorker à l'aide du designer de Visual Studio, rien de plus simple : il nous suffit de faire glisser depuis l'onglet Components de la Toolbox le composant BackgroundWorker sur notre formulaire.

Figure 1 : Onglet Components de la Toolbox

Nous pouvons ensuite visualiser ses propriétés et les modifier.

Figure 2 : Propriétés

J'ai ainsi renommé mon BackgroundWorker bgwDesign et passé ses propriétés WorkerReportsProgress et WorkerSupportsCancellation à true.

La propriété WorkerReportsProgress donne à notre BackgroundWorker la possibilité de nous informer ou non de son état d'avancement.

La propriété WorkerSupportsCancellation nous permet, quant à elle, d'autoriser ou non l'annulation de la tâche en cours du BackgroundWorker.

Figure 3 : Événements

Nous abonnons ensuite notre BackgroundWorker aux événements qui nous intéressent :

- DoWork : C'est cet événement qui se déclenche lorsque nous faisons appel au BackgroundWorker ;

- ProgressChanged : Cet événement, si la propriété WorkerReportsProgress est activée, se déclenche lorsque nous voulons indiquer que l'état d'avancement du BackgroundWorker change ;

- RunWorkerCompleted : Une fois le traitement du BackgroundWorker terminé cet événement est déclenché.

Code

Pour créer notre BackgroundWorker directement dans le code, il faut dans un premier temps le déclarer. Je l'ai appelé bgwCode.

private System.ComponentModel.BackgroundWorker bgwCode;

Nous devons ensuite l'instancier, initialiser ses propriétés (RunWorkerCompleted et ProgressChanged) et l'abonner aux différents événements (DoWork, RunWorkerCompleted et ProgressChanged) dont je vous ai parlé précédemment.

bgwCode = new BackgroundWorker();

bgwCode.WorkerReportsProgress = true;
bgwCode.WorkerSupportsCancellation = true;

bgwCode.DoWork += new DoWorkEventHandler(bgwCode_DoWork);
bgwCode.RunWorkerCompleted += new RunWorkerCompletedEventHandler(bgwCode_RunWorkerCompleted);
bgwCode.ProgressChanged += new ProgressChangedEventHandler(bgwCode_ProgressChanged);

Nous pouvons donc pu constater que la création de notre BackgroundWorker s'est passé très simplement dans les deux cas. Voyons maintenant comment l'utiliser.

Utilisation

Dans cette partie, je m'appuie sur un exemple d'application winform faisant appel à une méthode réalisant un traitement lourd et qui, dans un environnement monothreadé, entraîne un gel de l'interface utilisateur (UI).

Description

Figure 4 : Interface de l'application

Nous avons donc :

- un NumericUpDown nudNbLoop permettant de choisir le nombre de tours de boucle ;

- un Button btnStartCancel permettant de démarrer ou d'annuler le traitement ;

- un Label lblResult permettant d'afficher le résultat ;

- une ProgressBar pgbState permettant de visualiser l'état d'avancement du traitement.

Débuter le traitement

Pour lancer le traitement, il nous suffit de cliquer sur le bouton btnStartCancel. C'est dans l'événement clic de ce bouton que nous appelons la méthode RunWorkerAsync du BackgroundWorker. Celle-ci prépare le nouveau thread. Une fois le nouveau thread démarré, l'événement DoWork est déclenché ce qui permet au traitement en arrière-plan de s'exécuter de manière asynchrone. Nous passons en paramètre à la méthode RunWorkerAsync le nombre de tours de boucle choisi par l'utilisateur au travers de nudNbLoop.

Si le traitement est déjà en cours (Propriété IsBusy du BackgroundWorker est à true), appeler la méthode RunWorkerAsync une seconde fois lève une exception de type InvalidOperationException.

Vous remarquerez sans doute que la méthode RunWorkerAsync ne prend au maximum qu'un seul paramètre. En effet, mais ce paramètre étant de type objet, rien ne nous empêche de lui passer un tableau d'objets (string, int, etc.) ou même une structure !

private void btnStartCancel_Click(object sender, EventArgs e)
{
if (btnStartCancel.Text.Equals("Démarrer"))
{
lblResult.Text = "Traitement en cours...";
btnStartCancel.Text = "Annuler";
nudNbLoop.Enabled = false;
bgwDesign.RunWorkerAsync((int)nudNbLoop.Value);
}
else
{
...
}
}

C'est dans l'événement DoWork que nous appelons la méthode Treatment qui, comme son nom l'indique réalise le traitement. Pour cela nous récupérons tout d'abord l'objet BackgroundWorker qui a déclenché l'événement et qui nous est fourni par l'objet sender. Nous passons ensuite le nombre de tours de boucle choisi par l'utilisateur grâce à la propriété Argument des DoWorkEventArgs.

private void bgwDesign_DoWork(object sender, DoWorkEventArgs e)
{
BackgroundWorker worker = sender as BackgroundWorker;

e.Result = Treatment((int)e.Argument, (int)e.Argument, worker, e);
}

Que ce soit dans l'envent handler du DoWork ou dans la méthode Treatment, il est totalement interdit de manipuler les contrôles de l'interface utilisateur. En effet, ces deux méthodes ne s'exécutent pas dans le thread de l'interface et par conséquent les valeurs de ces contrôles pourraient changer ou l'avoir été ! Par contre les event handler des événements ProgressChanged et RunWorkerCompleted sont là pour ça, car ils s'exécutent dans le même thread que l'interface. De plus, n'oubliez pas de bien passer tous vos paramètres à la méthode RunWorkerAsync, car c'est le seul moyen de récupérer des paramètres dans le traitement.

Figure 5 : Aperçu de l'exception levé lors de l'accès au contrôle depuis l'event handler du DoWork.

Pour le traitement je me suis basé sur une méthode récursive. Néanmoins, une méthode avec une simple boucle for n'aurait rien changé au principe. Les points importants de cette méthode sont les suivants :

- vérifier grâce à la propriété CancellationPending du BackgroundWorker si aucune demande d'annulation n'a été faite. Si c'est le cas alors positionner la propriété Cancel du DoWorkEventArgs à true ce qui aura pour résultat de stopper l'exécution du BackgroundWorker ;

- faire le traitement : ici je fais un Sleep de 100 millisecondes et je retourne le nombre de boucles effectuées ;

- appeler la méthode ReportProgress du BackgroundWorker et lui passer en paramètre le pourcentage d'avancement. Cette méthode déclenche alors l'événement ProgressChanged. Il est à noter que la logique de calcul du pourcentage d'avancement doit être entièrement gérée par le développeur en fonction de l'architecture du traitement.

private long Treatment(int nb, int max, BackgroundWorker worker, DoWorkEventArgs e)
{
long result = 0;

if (worker.CancellationPending)
{
e.Cancel = true;
}
else
{
int pourcent = (int)(((double)max - (double)nb) / (double)max * 100);
worker.ReportProgress(pourcent);

if (nb <= 1)
{
result = 1;
}
else
{
System.Threading.Thread.Sleep(100);
result = Treatment(nb - 1, max, worker, e) + 1;
}
}

return result;
}

Visualiser l'état d'avancement

L'événement ProgressChanged s'exécutant dans le même thread que l'UI, cela nous permet donc d'accéder à ses contrôles. Dans le cas présent, j'utilise une ProgressBar pour visualiser l'évolution du traitement. Nous pouvons donc utiliser la propriété ProgressPercentage du ProgressChangedEventArgs pour modifier la valeur de la ProgressBar.

private void bgwDesign_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
this.progressBar1.Value = e.ProgressPercentage;
}

Si la propriété WorkerReportsProgress du BackgroundWorker est à false une exception de type InvalidOperationException est levée.

Outre le fait de faire remonter l'état d'avancement à l'utilisateur, celui-ci peut choisir d'annuler le traitement.

Annuler le traitement

Pour annuler le traitement, il nous suffit de cliquer sur le bouton btnStartCancel. C'est dans l'événement clic de ce bouton que nous appelons la méthode CancelAsync du BackgroundWorker. Celle-ci demande l'arrêt du traitement en cours et positionne la propriété CancellationPending du BackgroundWorker à true. C'est ensuite à la logique interne du traitement de vérifier régulièrement l'état de cette propriété.

Si la propriété WorkerSupportsCancellation du BackgroundWorker est à false une exception de type InvalidOperationException est levée.

private void btnStartCancel_Click(object sender, EventArgs e)
{
if (btnStartCancel.Text.Equals("Démarrer"))
{
...
}
else
{
bgwDesign.CancelAsync();
btnStartCancel.Text = "Démarrer";
pgbState.Value = 0;
nudNbLoop.Enabled = true;
}
}

Afficher le résultat

Pour finir, il ne nous reste plus qu'à afficher le résultat.
Il peut y avoir plusieurs types de résultats :

- il y a eu une erreur pendant le traitement. Dans ce cas la propriété Error du RunWorkerCompletedEventArgs est différente de null ;

- le traitement a été annulé. Dans ce cas la propriété Canceled du RunWorkerCompletedEventArgs est à true ;

- le traitement s'est déroulé normalement. Nous pouvons afficher le résultat qui se trouve dans la propriété Result du RunWorkerCompletedEventArgs.

Il peut arriver que le code dans le event handler du DoWork se finisse pendant que la demande d'annulation est en train d'être faite. Le passage à true de la propriété CancellationPending peut donc être manqué. Dans ce cas la propriété Canceled du RunWorkerCompletedEventArgs présent dans le event handler du RunWorkerCompleted ne passera pas à true et ce malgré la demande d'annulation.

private void bgwDesign_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Error != null)
{
lblResult.Text = "Une erreur est survenue ! Détail : " + e.Error.Message;
}
else if (e.Cancelled)
{
lblResult.Text = "Opération annulée !";
}
else
{
lblResult.Text = "Opération terminée ! Résultat : " + e.Result.ToString();
}

btnStartCancel.Text = "Démarrer";
nudNbLoop.Enabled = true;
pgbState.Value = 0;
}

Conclusion

Dans cet article nous avons donc pu voir comment utiliser le composant BackgroundWorker. Celui-ci est donc un bon outil dans le cas de gros traitements ou d'attente de réponse. Il nous permet donc de vulgariser le multithreading pour des opérations simples. Néanmoins, dans le cas où nous aurions plusieurs opérations asynchrones à réaliser simultanément et dont les résultats de chacune dépendraient de ceux des autres alors je pense que l'utilisation du BackgroundWorker ne serait pas adaptée. L'utilisation de la classe Thread du Namespace System.Threading serait préférable.

Ressources

- Les sources de cet article (Miroir HTTP)

Remerciements

Merci à Katyucha pour sa relecture. (Sa page perso)