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

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 Donner une note à l'article (5)

Article lu   fois.

L'auteur

Profil ProSite personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

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

I. 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.

II. 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.

II.1. 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.

Image non disponible
Figure 1 : Onglet Components de la Toolbox

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

Image non disponible
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, quand à elle, d'autoriser ou non l'annulation de la tâche en cours du BackgroundWorker.

Image non disponible
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é.

II.2. Code

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

 
Sélectionnez

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.

 
Sélectionnez

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.

III. 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).

III.1. Description

Image non disponible
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.

III.2. 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 !

 
Sélectionnez

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.

 
Sélectionnez

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.

Image non disponible
Figure 5 : Apperç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.
 
Sélectionnez

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

III.3. 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.

 
Sélectionnez

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.

III.4. 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és WorkerSupportsCancellation du BackgroundWorker est à false une exception de type InvalidOperationException est levée.

 
Sélectionnez

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

III.5. Afficher le résultat

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

  • 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.

 
Sélectionnez

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 a réaliser simultanément et dont les résultats de chacunes 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)

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

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.