Framework 2.0 : Présentation du composant BackgroundWorker (VB.Net)

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 C#.

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 WithEvents bgwCode As System.ComponentModel.BackgroundWorker

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 System.ComponentModel.BackgroundWorker

bgwCode.WorkerReportsProgress = True
bgwCode.WorkerSupportsCancellation = True

AddHandler bgwCode.DoWork, AddressOf bgwCode_DoWork
AddHandler bgwCode.RunWorkerCompleted, AddressOf bgwCode_RunWorkerCompleted
AddHandler bgwCode.ProgressChanged, AddressOf 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 Sub btnStartCancel_Click( _
	ByVal sender As System.Object, _
	ByVal e As System.EventArgs) _
	Handles btnStartCancel.Click
	
	If btnStartCancel.Text.Equals("Démarrer") Then
		lblResult.Text = "Traitement en cours..."
		btnStartCancel.Text = "Annuler"
		nudNbLoop.Enabled = False
		bgwDesign.RunWorkerAsync(CInt(nudNbLoop.Value))
	Else
		...
	End If
End Sub

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 Sub bgwDesign_DoWork( _
	ByVal sender As System.Object, _
	ByVal e As System.ComponentModel.DoWorkEventArgs) _
	Handles bgwDesign.DoWork

	Dim worker As BackgroundWorker = CType(sender, BackgroundWorker)
	e.Result = Treatment(CInt(e.Argument), CInt(e.Argument), worker, e)
End Sub

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 Function Treatment( _
	ByVal nb As Integer, _ 
	ByVal max As Integer, _
	ByVal worker As BackgroundWorker, _
	ByVal e As DoWorkEventArgs) As Long
	
	Dim result As Long = 0

	If worker.CancellationPending Then
		e.Cancel = True
	Else
		Dim pourcent As Integer = CInt(((CDec(max) - CDec(nb)) / CDec(max) * 100))
		worker.ReportProgress(pourcent)
		If nb <= 1 Then
			result = 1
		Else
			System.Threading.Thread.Sleep(100)
			result = Treatment(nb - 1, max, worker, e) + 1
		End If
	End If
	
	Return result
End Function

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 Sub bgwDesign_ProgressChanged( _
	ByVal sender As Object, _
	ByVal e As ProgressChangedEventArgs) _
	Handles bgwDesign.ProgressChanged
	
	pgbState.Value = e.ProgressPercentage
End Sub

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 Sub btnStartCancel_Click( _
	ByVal sender As Object, _
	ByVal e As EventArgs) _
	Handles btnStartCancel.Click
	
	If btnStartCancel.Text.Equals("Démarrer") Then
		...
	Else
		bgwDesign.CancelAsync()
		btnStartCancel.Text = "Démarrer"
		pgbState.Value = 0
		nudNbLoop.Enabled = True
	End If
End Sub

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 Sub bgwDesign_RunWorkerCompleted( _
	ByVal sender As Object, _
	ByVal e As RunWorkerCompletedEventArgs) _
	Handles bgwDesign.RunWorkerCompleted

	If Not (e.Error Is Nothing) Then
		lblResult.Text = "Une erreur est survenue ! Détail : " + e.Error.Message
	ElseIf e.Cancelled Then
		lblResult.Text = "Opération annulée !"
	Else
		lblResult.Text = "Opération terminée ! Résultat : " + e.Result.ToString
	End If

	btnStartCancel.Text = "Démarrer"
	nudNbLoop.Enabled = True
	pgbState.Value = 0
End Sub

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.