La gestion des évènements en Visual Basic  2005

Par Olivier Delmotte, MVP Visual Basic.

Tous les développeurs .NET utilisent les évènements, aussi bien en Winform qu’en Webform. Les développeurs VB aussi. Voyons maintenant comment tout cela fonctionne.

Qu’est-ce qu’un évènement ?

Pour illustrer au mieux mon propos, prenez une newsletter.

Quand vous voulez vous tenir informé des actualités d’un site web, par exemple, vous pouvez vous abonner à sa newsletter. C’est alors le site web qui vous enverra un mail pour vous tenir au courant.

Un évènement c’est pareil.

Mettons que vous ayez deux types A et B. B expose un évènement auquel est abonnée A. Chaque fois que nécessaire, le type B lance l’évènement (la newsletter). A possède ce que l’on appelle un Handler. Ce Handler joue le rôle de la boîte mail.

En réalité, c’est un tout petit peu plus compliqué que cette description pour le moins simpliste.

Comment s’abonner à un évènement ?

Pour répondre à cette question, prenons un exemple tout simple. Une Form et un bouton sur celle-ci. Pour illustrer mon propos, on va afficher la date et l’heure à chaque clic sur le bouton. Mais nous n’allons pas passer par le designer Visual Studio afin de bien voir comment procéder.

Imports System.DrawingImports System.Windows.FormsPublic Class FormSansDesigner    Inherits Form    Private b As Button    Public SubNew()        b = New Button       b.Dock = DockStyle.Fill        b.Text = "Afficher l'heure"        Me .Controls.Add(b)        Me.Size = New Size(300, 150)        Me.StartPosition = FormStartPosition.CenterScreen    End SubEnd Class

Ce qui devrait vous donner cette superbe fenêtre :

Ce qui devrait vous donner cette superbe fenêtre

Le bouton possède bien un évènement Click. Il y a deux méthodes pour s’abonner à cet évènement. Mais avant, il faut un Handler pour cet évènement (votre boîte mail). C’est la méthode AfficherHeure suivante :

   Private Sub AfficherHeure(ByVal sender AsObject, ByVal e As EventArgs)        Me .Text = DateTime.Now.ToString()    End Sub

Comme vous le constatez, cette méthode à une signature particulière.

Les évènements, dans le Framework .Net, utilisent tous ou presque, ce type de signature :

  • Sender représente l’instance qui a lancé cet évènement.
  • e, de type EventArgs ou héritant d’EventArgs, sert à transmettre des informations supplémentaires. Nous y reviendrons plus en détails plus tard.

Voyons maintenant comment nous abonner.

AddHandler

La première méthode, et celle qui est fondamentale avec VB, est la méthode AddHandler.

Vous verrez qu’elle est incontournable en VB.Net pour ce qui est de la gestion des évènements.

Voici donc le code pour rajouter un abonnement à l’évènement Click de notre bouton, à rajouter dans le constructeur de notre Form :

       AddHandler b.Click, AddressOf AfficherHeure

Explication.

AddHandler, comme son nom l’indique, sert à ajouter un Handler sur un évènement. Il est donc logique de lui préciser sur quel évènement, ici l’évènement Click de notre bouton b, et le Handler (la boîte mail) qui sera chargé de traiter cet évènement, ici la méthode AfficherHeure, ou plutôt un delegate de la méthode AfficherHeure.

Pour les habitués de C++, on peut apparenter un Delegate (en très très gros) à un pointeur de fonction.

Si vous testez l’application, lors d’un clic sur le bouton, AfficherHeure est bien appelée et l’heure s’affiche dans la barre de titre de la fenêtre.

Mission accomplie.

Clause Handles / WithEvents

La deuxième méthode est celle adoptée par le designer de Visual Studio :

Imports System.DrawingImports System.Windows.FormsPublic Class FormSansDesigner    Inherits Form    Private WithEvents b As Button    Public SubNew()        b = New Button       b.Dock = DockStyle.Fill        b.Text = "Afficher l'heure"        Me .Controls.Add(b)        Me.Size = New Size(300, 150)        Me.StartPosition = FormStartPosition.CenterScreen    End Sub    Private Sub AfficherHeure(ByVal sender As Object, ByVal e As EventArgs) Handles b.Click        Me.Text = DateTime.Now.ToString()    End SubEnd Class

Tout d’abord, le bouton est déclaré avec le mot clé WithEvents. WithEvents indique que la variable déclarée est susceptible de lancer des évènements, et donc que la clause Handles sera utilisable, comme ici dans la définition de la méthode AfficherHeure.

Si vous testez cette version de l’exemple, vous constaterez qu’elle fonctionne aussi bien, à quelques détails près.

Mission accomplie aussi.

Retour sur WithEvents / Handles

Le fonctionnement de WithEvents / Handles est un peu moins transparent que l’utilisation de AddHandler seul.

En effet, lors de la compilation, le compilateur VB effectue quelques modifications. Je me demandais comment il gérait cette façon de procéder, à quel moment l’ajout du Handler était fait.

J’ai donc utilisé l’incontournable Reflector pour voir le code généré, et je dois avouer que j’ai été un peu surpris, et pas très agréablement.

Voici comment procède vbc.exe :

Il renomme notre bouton et crée les accesseurs pour ce bouton avec le nom que nous lui avions donné.
Voilà ce que ça donne une fois désassemblé :

Private _b As Button    PrivateOverridable Property b As Button        Get            Return Me._b        End Get        Set(ByVal WithEventsValue AsButton)‘ …        Me._b = WithEventsValue‘ …        End Set    End Property

J’ai nettoyé un peu, je vais encore détailler plus ensuite. Donc si nous regardons maintenant notre constructeur, l’instance de notre bouton n’est plus crée directement, mais via l’accesseur de la propriété.

Nous n’aurons donc pas un _b = new Button() mais un b = new Button().

Et tout l’astuce réside dans le fait de passer par l’accesseur, puisque c’est durant l’affectation que .Net va créer les abonnements aux évènements, via AddHandler (je vous l’avais bien dit qu’on ne pouvait pas s’en passer). Voici donc le code complet de l’accesseur :

        Set(ByVal WithEventsValue AsButton)            If (Not Me._bIsNothing) Then                RemoveHandler Me._b.Click, New EventHandler(AddressOf Me.AfficherHeure)            End If            Me._b = WithEventsValue            If (Not Me._bIsNothing) Then                AddHandler Me._b.Click, New EventHandler(AddressOf Me.AfficherHeure)            End If        End Set

Ce qui me chiffonne dans cette façon de procéder, c’est qu’ensuite, sans le savoir, vous utiliserez l’accesseur et non le membre b de la Form.

Si nous reprenons le constructeur de notre Form, au runtime, la définition des propriétés Dock et Text de notre bouton ne se fera pas directement, mais bien via l’accesseur.

On ne retrouve pas ce genre de comportement en C#, puisque celui-ci, que ce soit via le designer ou en codant vous-même, les closes Handles et WithEvents n’existent pas, il utilise l’équivalent de AddHandler.

Donc quand on dit que les compilateurs VB.Net et C# génèrent sensiblement le même IL, ce n’est pas toujours vrai, la preuve.

Création et utilisation d’évènements personnalisés

Jusqu’à présent, nous avons vu comment utiliser des évènements déjà existants dans le Framework, enfin un. Dans tous les cas, il vous faudra surement un jour ou l’autre créer vos propres évènements.

Voyons un peu comment faire.

Je vous parlais de la signature du Handler de l’évènement Click du type Button. Celui-ci respecte une forme bien particulière, je vous conseille de respecter cette convention, même si ce n’est pas une obligation.

Pour illustrer mes propos, je vais me baser sur un contrôle utilisateur de mon cru qui possèdera un évènement TimeStampEvent et qui nous renverra la date et l’heure courante.

Le Delegate

La première chose à faire, c’est définir le delegate qui nous permettra de définir la signature de notre évènement.

    Public Delegate Sub TimeStampEventDelegate(ByVal sender AsObject, ByVal e As TimestampEventArgs)

Comme je vous le disais, il est fortement conseillé de respecter ce type de signature. Pourquoi ?

Le Sender

Tout d’abord, un Handler peut être le Handler de plusiurs évènements. Un exemple complètement inutile : vous avez une Form avec une multitude de boutons dessus (type Button) et vous voulez qu’à chaque clic sur n’importe quel bouton le titre de la fenêtre affiche le nom du bouton cliqué.

La façon la plus simple et la plus directe est celle-ci :

Public Class frmTest    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click        Me.Text = "Button1"    End Sub    Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click        Me.Text = "Button2"    End Sub    Private Sub Button3_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button3.Click        Me.Text = "Button3"    End Sub    Private Sub Button4_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button4.Click        Me.Text = "Button4"    End SubEnd Class

Mais que ce passe-t-il pour 100 boutons, vous rajoutez 100 fois le même bout de code ? Non, c’est trop compliqué et n’est pas facilement maintenable.

La solution est donc la suivante :

Public Class frmTest    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click, Button2.Click, Button3.Click, Button4.Click        Me.Text = DirectCast(sender, Button).Name    End SubEnd Class

Voici donc l’intérêt du sender, savoir qui à levé tel évènement. Et comme vous le remarquez, j’utilise le même handler pour 4 évènements différents.

Tant que j’y suis, un même évènement, au sens instance du terme, peut avoir plusieurs Handlers.

Pour compléter notre exemple précédent, nous voulons que le clic sur sur bouton 1 provoque la maximisation de la fenêtre et que le bouton 2 la fasse revenir à son état normal.

On pourrait donc modifier le code ainsi :

Public Class frmTest    Private Sub Button_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click, Button2.Click, Button3.Click, Button4.Click        Me.Text = DirectCast(sender, Button).Name        If Me.Text = "Button1" Then Me.WindowState = FormWindowState.Maximized        If Me.Text = "Button2" Then Me.WindowState = FormWindowState.Normal    End SubEnd Class

Mettons maintenant que vous modifiez les noms de boutons. Allez-vous penser à modifier le code en conséquence ? Dans cet exemple surement, mais dans un projet plus gros et complexe ? Certainement pas, ou alors vous allez en oublier.

Il vaut mieux utiliser plusieurs Handlers sur un même évènement :

Public Class frmTest    Private Sub Button_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click, Button2.Click, Button3.Click, Button4.Click        Me.Text = DirectCast(sender, Button).Name    End Sub    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click        Me.WindowState = FormWindowState.Maximized    End Sub    Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click        Me.WindowState = FormWindowState.Normal    End SubEnd Class

Quand les évènements Click des boutons 1 et 2 seront lancés, les 2 Handlers seront appelés : Button_Click et Button1_Click pour le bouton 1 et Button_Click et Button2_Click pour le bouton 2.

Le cas de l’EventArgs

Dans beaucoup de cas, le deuxième paramètre d’un Handler est de type EventArgs. Si vous regardez celui-ci, il ne fait pas grand-chose. Mais dans d’autres cas, il s’agit d’un héritier d’EventArgs.

Prenez par exemple l’évènement MouseDown d’un bouton. Le deuxième paramètre est de type MouseEventArgs.

MouseEventArgs

Il hérite bien de EventArgs, mais propose plus de chose : le(s) bouton(s) de la souris qui sont enfoncés, la position de la souris, …

Vous vous demanderez sans doute pourquoi mettre toutes ces informations, il est possible de les récupérer dans le Handler, et en plus vous n’avez peut-être pas besoin du nombre de tours qu’à fait la molette de la souris. Vous avez raison. Mais que se passe-t-il si vous avez plusieurs Handlers sur cet évènement et que l’un d’entre eux doit effectuer une tâche assez longue. Dans le premier, vous allez récupérer la bonne position de souris, mais dans le second, à moins que l’utilisateur n’ait pas bougé la souris, vous allez récupérer des coordonnées différentes.

Avec cet EventArg, vous serez sur que tous les abonnés à cet évènement auront bien les mêmes informations.

Implémentation de notre évènement

Le délégué :

    Public Delegate Sub TimeStampEventDelegate(ByVal sender AsObject, ByVal e As TimestampEventArgs)

Déclaration de l’évènement :

    Public Event TimeStampEvent As TimeStampEventDelegate

Le TimeStampEventArgs :

Public Class TimestampEventArgs    Inherits EventArgs    Private _Timestamp As DateTime = DateTime.Now    Public ReadOnlyProperty Timestamp() As DateTime        Get           Return _Timestamp        End Get    End PropertyEnd Class

Lancement de notre évènement

        RaiseEvent TimeStampEvent(Me, New TimestampEventArgs)

Il existe une implémentation qui peut se révéler bien utile dans le cas où votre évènement peut être lancé depuis plusieurs endroits différents dans votre code.

Plutôt que d’appeler à chaque fois RaiseEvent, on appellera une méthode OnTimeStamp par exemple, qui se chargera de tout et dont l’implémentation dans ce cas pourrait ressembler à ceci :

    Public Sub OnTimeStamp()        RaiseEvent TimeStampEvent(Me, New TimestampEventArgs)    End Sub

Cette solution offre énormément d’avantages :

  • Possibilité de factoriser le code de lancement de l’évènement.
  • Homogeneisation du lancement de l’évènement.
  • Simulation de l’évènement depuis l’extérieur du type. Il est en effet impossible de lancer un évènement depuis ailleurs que le type lui-même.

Le Handler pour notre évènement dans une Form de test :

    Private Sub SampleControl1_TimeStampEvent(ByVal sender As Object, ByVal e As TimestampEventArgs) Handles SampleControl1.TimeStampEvent    End Sub

Comme vous pouvez le constater, rien de bien sorcier.

Je vais juste détailler un peu la déclaration de l’évènement. Un évènement se déclare avec le mot clé Event et doit être de type Delegate. Mais en aucun cas un évènement ne s’instancie.

Voilà, nous en avons fini avec les évènements avec VB.Net. Ah non, je me disais bien que j’oubliais quelque chose : les Custom Events.

Custom Events

Vous trouvez que vous pouvez déjà en faire plus que suffisamment avec ce que nous venons de voir ? Il est possible de faire encore plus, sans rien changer à la manière de s’abonner à un évènement.

En fait, un Custom Event permet de se rapproche un peu plus du fonctionnement interne d’un évènement puisque c’est vous qui allez redéfinir le comportement de base.

Vous allez gérer vous-même les abonnements et les désabonnements. Comme la newsletter, il est aussi possible de se désabonner d’un évènement via le mot-clé contraire à AddHandler :

RemoveHandler. Il accepte exactement les mêmes paramètres et fait tout l’inverse.

C’est vous aussi qui allez lancer à proprement parler l’évènement.

Voici la base d’un Custom Event :

    Public Custom Event CustomTimeStampEvent As TimeStampEventDelegate        AddHandler(ByVal value As TimeStampEventDelegate)        End AddHandler        RemoveHandler(ByVal value As TimeStampEventDelegate)        End RemoveHandler        RaiseEvent(ByVal sender AsObject, ByVal e As TimestampEventArgs)       End RaiseEvent   End Event

AddHandler

AddHandler est appelé à la demande d’abonnement à l’évènement CustomTimeStampEvent. De cette manière, vous pouvez refuser l’abonnement à votre évènement : ou vous trouvez qu’un type n’a pas le droit d’y accéder, ou alors vous ne voulez pas dépasser X abonnements.

RemoveHandler

RemoveHandler est appelé elle à la demande de désabonnement.

Là aussi vous avez beaucoup de liberté, mais attention à ce que vous faîtes … ou ne faîtes pas.

RaiseEvent

RaiseEvent est appelé quand vous lancez l’évènement dans votre code.

Et c’est à vous de faire le nécessaire pour lancer l’évènement correctement.

Et vous pouvez aussi lancer l’évènement à certaines conditions, comme dans l’exemple suivant.

Implémentation

Voici l’implémentation complète de notre Custom Event :

    Private _TimeStampEventDelegates AsNew List(Of TimeStampEventDelegate)    Public CustomEvent CustomTimeStampEvent As TimeStampEventDelegate        AddHandler(ByVal value As TimeStampEventDelegate)            _TimeStampEventDelegates.Add(value)            Console.WriteLine("Abonnement : {0} abonnés", _TimeStampEventDelegates.Count)        End AddHandler        RemoveHandler(ByVal value As TimeStampEventDelegate)            _TimeStampEventDelegates.Remove(value)            Console.WriteLine("Désabonnement : {0} abonnés", _TimeStampEventDelegates.Count)        End RemoveHandler        RaiseEvent(ByVal sender AsObject, ByVal e As TimestampEventArgs)           ' On ne lancera l'évènement que si le nombre d'abonnés est impaire            If _TimeStampEventDelegates.Count Mod 2 = 0 Then Return           For i As Integer = 0 To _TimeStampEventDelegates.Count - 1                _TimeStampEventDelegates(i).Invoke(sender, e)           Next            Console.WriteLine("Levée par type : {0}" + vbCrLf + vbCrLf + "Heure : {1:D}", sender.GetType.Name, e.Timestamp)           Console.WriteLine("Envoyé à {0} abonnés", Me._TimeStampEventDelegates.Count)       End RaiseEvent   End Event

Conclusion

Comme vous pouvez le constater, Visual Basic .NET offre de très nombreuses possibilités pour la gestion des évènements.

Vous avez maintenant toutes les armes pour gérer les évènements dans vos applications.

Bon codage