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

mardi 11 janvier 2011

Timestamp UNIX en VB.NET

Dans le cadre d'un projet sur lequel je bosse en entreprise, il m'est arrivé de devoir manipuler les timestamp UNIX en VB.NET. Or les sources à ce sujet sont peu généreuses sur la toile.

On fournit notamment une fonction pour passer d'un timestamp à une date, comme ceci :

Public Function DateFromTimestamp(ByVal timestamp As Long) As DateTime
Dim debut As New DateTime(1970, 1, 1, 0, 0, 0)
Return debut.AddSeconds(timestamp)
End Function


Pour preuve :
P:\>php -r "echo time();"
1294733118


MessageBox.Show(DateFromTimestamp(1294733118).ToString) ' Résultat : "11/01/2011 08:05:18"


Pour récupérer le timestamp d'une date, c'est un poil plus délicat mais pas impossible. On dispose de la propriété Ticks pour une instance DateTime, qui renvoie le nombre de "Ticks" correspondant à une date donnée par 100 nanosecondes. Seulement, on ne sait pas depuis quand. Cependant, on sait qu'un timestamp correspond au nombre de secondes écoulées depuis le premier Janvier 1970 à minuit - le début de l'ère UNIX - et une date lambda. Il suffit donc de récupérer, pour chacune de ces deux dates, le nombre de Ticks, d'en faire la différence et de multiplier le tout par 10-7 pour convertir nos ticks en secondes.

Public Function TimeStamp(Optional ByVal laDate As DateTime = Nothing) As Long
If laDate = Nothing Then
laDate = DateTime.Now
End If
Dim TicksDebut As Long = New DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Local).Ticks
Dim TicksActuels As Long = laDate.Ticks

Return Math.Floor((TicksActuels - TicksDebut) * Math.Pow(10, -7))
End Function


Puisse cette petite astuce servir à quelques développeurs...

Ge0