vendredi 14 janvier 2011

Delegates & opération inter-thread en VB.NET

Il y a quelques temps de cela, Sh0ck m'avait demandé comment faire pour partager les membres au sein d'une classe en .NET. Les exemples sont florissants sur le web mais parfois assez obscurs.

Le problème



Commencez par ouvrir Visual Studio - peu importe la version - et agencez rapidement une interface graphique, par exemple, comme ceci :


Vous pouvez vous passer du label. En fait, seule le champ "TextEdit" et le bouton nous importent.

On va faire en sorte que lorsque nous cliquons sur le bouton, un thread se créé, se lance, et mette à jour le champ TextEdit pour y afficher un "Hello world".

Oui, bon, ok, pas besoin de thread pour ça, mais c'est pour l'exemple.

Créez un champ "BackgroundWorker" en prime. Ensuite, on va dans le code de notre formulaire et on insère la méthode suivante :
Private Sub DoStuff()
' Mise à jour du TextBox
Me.TextBox1.Text = "Hello world!"
End Sub


Cette méthode sera appelée par le BackgroundWorker :

Private Sub BackgroundWorker1_DoWork(ByVal sender As Object, _
ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
Me.DoStuff()
End Sub


Et on "lancera" le BackgroundWorker après clic sur notre bouton :

Private Sub Button1_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Button1.Click
Me.BackgroundWorker1.RunWorkerAsync()
End Sub


On lance la fenêtre, on clique sur notre bouton et, là, c'est le drâme : une exception type InvalidOperationException est lancée. Le descriptif de celle-ci est : Opération inter-threads non valide : le contrôle 'TextBox1' a fait l'objet d'un accès à partir d'un thread autre que celui sur lequel il a été créé..

Et c'est là qu'interviennent les "Delegates". Ils permettent aux différents threads d'accéder aux propriétés communes d'une classe. Pour ceux qui connaissent les mutex - exclusions mutuelles - le principe peut sembler le même, sauf qu'ici, nous n'aurons pas verrouiller ou déverrouiller quoi que ce soit.

Tout d'abord, nous allons créer une méthode publique qui mettra à jour notre TextEdit :

Public Sub MajTextBox1(ByVal chaine As String)
Me.TextBox1.Text = chaine
End Sub


Rien de bien sorcier. Ensuite, nous déclarons dans la liste des membres notre "Delegate" qu'on instanciera et à qui on fournira l'adresse de MajTextBox1. Il s'agit, en quelques sortes, d'une référence de fonction. Le delegate doit donc avoir la même signature que notre méthode MajTextBox1, qu'importe la visibilité de cette dernière. On aura donc à écrire :

Private Delegate Sub AccessMajTextEdit(ByVal chaine As String)


Ensuite, dans notre méthode DoStuff(), appelez la méthode "Invoke", qui va servir à appeler notre Delegate et donc, par la même occasion, la méthode MajTextBox1, les conflits en moins :

Private Sub DoStuff()

' Déclaration et instanciation du Delegate

Dim MyDelegate As New AccessMajTextEdit(AddressOf MajTextBox1)

' La propriété InvokeRequired indique si l'on a
' besoin d'appelé un délégué pour modifier une instance
' d'un autre thread

If Me.InvokeRequired Then
' Appel du delegate
Me.Invoke(MyDelegate, "Hello world !")
End If
End Sub


On relance notre petite application, on clique sur le bouton et, normalement, ...



C'était un petit article sans prétention. Je ne maîtrise véritablement pas le sujet, je ne saurais vous dire ce qu'est un Delegate en détail. On sait juste que ça marche. Vous pouvez très bien utiliser les threads au lieu des BackgroundWorker - ces derniers étant néanmoins plus faciles à utiliser, ce pourquoi je m'en suis servi.

Cette astuce m'est très utile au boulot, c'est pour ça que je l'insère ici pour ne pas l'oublier. En espérant que quelques programmeurs VB.NET puissent profiter.

Le code complet :

Public Class Form1

Private Delegate Sub AccessMajTextEdit(ByVal chaine As String)

Private Sub DoStuff()

' Instanciation du Delegate
Dim MyDelegate As New AccessMajTextEdit(AddressOf MajTextBox1)

' La propriété InvokeRequired indique si l'on a
' besoin d'appelé un délégué pour modifier une instance
' d'un autre thread

If Me.InvokeRequired Then
Me.Invoke(MyDelegate, "Hello world !")
End If
End Sub


Private Sub Button1_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Button1.Click
Me.BackgroundWorker1.RunWorkerAsync()
End Sub

Private Sub
BackgroundWorker1_DoWork(ByVal sender As Object, _
ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
Me.DoStuff()
End Sub

Public Sub
MajTextBox1(ByVal chaine As String)
Me.TextBox1.Text = chaine
End Sub
End Class


Ge0

1 commentaire:

  1. Merci pour ce petit article qui m'a appris à me servir des threads, c'est fou comme ces petits trucs peuvent être utile pour avoir une application fluide et que ne freeze pas à chaque utilisation d'un bouton !

    Tutoriel claire est bien présenté au passage :p.

    Cordialement,
    Xartrick.

    RépondreSupprimer