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.
Nous pouvons ensuite visualiser ses propriétés et les modifier.
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.
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▲
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.
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▲
Remerciements▲
Merci à Katyucha pour sa relecture. (Sa page perso)