La gestion des évènements en Visual Basic 2005Par 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 :
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.
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