Mise en place d'événements/delegates en VB.NET et C#.NET

Une question qui revient assez souvent dans les newsgroups est :

  • Comment mettre en place une communication entre mes classes métiers et mes IHM ?
  • Comment faire ça avec la philosophie .NET ?

(les exemples de code utilisés dans cet article sont en libre téléchargement ici )

Un exemple assez classique pour illustrer le propos reste la copie de fichier. L'idéal est de créer une classe métier qui va s'occuper de la copie du fichier puis de créer une IHM qui va consommer cette classe.

Cependant, si le fichier fait 10Mo, il peut être utile de présenter à l'utilisateur une gauge de progression pour le faire patienter. De plus, si il décide d'annuler la copie, il doit pouvoir le faire, ce qui veut dire que l'interface graphique ne doit pas être bloquée durant cette copie. A titre d'exemple, voici une classe "classique" (Librairie_CS.GestionFichiers ou Librairie_VB.GestionFichiers) permettant de réaliser cette opération de copie :

using System;
using System.IO;

namespace Librairie_CS {

 public class GestionFichiers {

  private string cheminFichier = string.Empty;
public string CheminFichier {

   get {

    return(this.cheminFichier);
}
}

  public GestionFichiers(string cheminFichier) {

   this.cheminFichier = cheminFichier;
}

  public void CopieFichier(string cheminFichierDestination, bool ecrasement) {

   // On vérifie si le fichier de destination n'existe pas déjà
   if (File.Exists(cheminFichierDestination)) {

// Si demandé, on écrase le fichier de destination
if (ecrasement) {

     File.Delete(cheminFichierDestination);
}
else {

     throw new IOException(string.Format("Le fichier de destination '{0}' existe déjà.", cheminFichierDestination));
}
}

   // On ouvre le fichier de destination en écriture
using (FileStream fileStreamDestination = new FileStream(cheminFichierDestination, FileMode.Create, FileAccess.Write)) {

    // On ouvre le fichier d'origine en lecture
using (FileStream fileStreamOriginal = new FileStream(this.cheminFichier, FileMode.Open, FileAccess.Read)) {

     // On lit le prochain octet
     int octet = fileStreamOriginal.ReadByte();
while (octet != -1) {

// On écrit l'octet
fileStreamDestination.WriteByte((byte)octet);

      octet = fileStreamOriginal.ReadByte();
}

     fileStreamOriginal.Close();
}
fileStreamDestination.Close();
}
}
}
}

Voici le même code écrit en VB.NET:

Imports System
Imports System.IO

Namespace Librairie_VB

    Public Class GestionFichiers

        Private privateCheminFichier As String = String.Empty
Public ReadOnly Property CheminFichier() As String
Get
Return (Me.privateCheminFichier)
End Get
End Property

        Public Sub New(ByVal fichier As String)
Me.privateCheminFichier = fichier
End Sub

        Public Sub CopieFichier(ByVal cheminFichierDestination As String, ByVal ecrasement As Boolean)

            ' On vérifie si le fichier de destination n'existe pas déjà
If File.Exists(cheminFichierDestination) Then

                ' Si demandé, on écrase le fichier de destination
If ecrasement Then
File.Delete(cheminFichierDestination)
Else
Throw New IOException(String.Format("Le fichier de destination '{0}' existe déjà.", cheminFichierDestination))
End If
End If

            ' On ouvre le fichier de destination en écriture
Dim fileStreamDestination As New FileStream(cheminFichierDestination, FileMode.Create, FileAccess.Write)

            ' On ouvre le fichier d'origine en lecture
Dim fileStreamOriginal As New FileStream(Me.privateCheminFichier, FileMode.Open, FileAccess.Read)

            ' On lit le prochain octet
Dim octet As Integer = fileStreamOriginal.ReadByte()

            While octet <> -1
' On écrit l'octet
fileStreamDestination.WriteByte(CType(octet, Byte))

                octet = fileStreamOriginal.ReadByte()
End While

            fileStreamOriginal.Close()
fileStreamDestination.Close()

        End Sub

    End Class

End Namespace

Ceci étant fait, il faudrait maintenant implémenter trois événements dans nos classes:

  • CopieDemarrage : cet événement devra être émis juste avant le démarrage proprement dit de l'opération de copie. Il devra permettre notament d'informer le client de la taille du fichier à copier
  • CopieEnCours : cet événement devra être émis à intervalle régulier de manière à indiquer la progression de la copie. Il devra également permettre au client d'annuler l'opération en cours
  • CopieFin : cet événement devra être émis en fin de processus en indiquant le statut final de l'opération (Succès, Echec, Annulation)

Pou mettre en place ce genre de choses, il faut passer par des Delegates:

  • CopieDemarrageEventHandler
  • CopieEnCoursEventHandler
  • CopieFinEventHandler

Notez que le plan de nommage utilisé en standard par le .NET Framework consiste à terminer le nom de ses Delegates par EventHandler. Une fois les Delegates déclarés, il faut ensuite déclarer les événements eux-mêmes. Les événements que nous allons déclarés auront pour type les Delegates précédents. Voici les déclarations que nous allons utiliser :

namespace Librairie_CS {

 public delegate void CopieDemarrageEventHandler(object sender, CopieDemarrageEventArgs e);
public delegate void CopieEnCoursEventHandler(object sender, CopieEnCoursEventArgs e);
public delegate void CopieFinEventHandler(object sender, CopieFinEventArgs e);

 public class GestionFichiers {

  public event CopieDemarrageEventHandler CopieDemarrage = null;
public event CopieEnCoursEventHandler CopieEnCours = null;
public event CopieFinEventHandler CopieFin = null;

Namespace Librairie_VB

    Public Delegate Sub CopieDemarrageEventHandler(ByVal sender As Object, ByVal e As CopieDemarrageEventArgs)
Public Delegate Sub CopieEnCoursEventHandler(ByVal sender As Object, ByVal e As CopieEnCoursEventArgs)
Public Delegate Sub CopieFinEventHandler(ByVal sender As Object, ByVal e As CopieFinEventArgs)

    Public Class GestionFichiers

        Public Event CopieDemarrage As CopieDemarrageEventHandler
Public Event CopieEnCours As CopieEnCoursEventHandler
Public Event CopieFin As CopieFinEventHandler

Notons au passage que la création de Delegate n'est pas obligatoire mais que le compilateur VB.NET en créera un de toute façon pour vous. Autant bien comprendre cette notion et les écrire nous même... Exemple :

<Aparté>
Imaginons que nous déclarions un Event en VB.NET comme suit :

        Public Event Test(ByVal monParam As Integer)

L'introspection de la DLL générée grâce à ILDASM , nous permet de découvrir que le compilateur a effectivement bossé pour nous en créant le Delegate directement au niveau du code IL :

   | | |___[CLS] TestEventHandler
| | | | .class nested public auto ansi sealed
| | | | extends [mscorlib]System.MulticastDelegate

puis

    | | |___[FLD] TestEvent : private class Librairie_VB.GestionFichiers/TestEventHandler

< /Aparté>

Revenons maintenant à nos moutons. Vous avez certainement remarqué que la déclaration des signatures de mes Delegates (et donc de mes événements in fine) avait l'air de suivre une règle commune:

  • (object sender, xxxEventArgs e)
  • (ByVal sender As Object, ByVal e As xxxEventArgs)

Le premier argument appelé sender permet à l'abonné de l'événement de connaître précisément l'instance de l'objet qui a émis l'événement. A quoi cela peut-il servir me direz-vous ?

Et bien imaginons un instant que je possède deux instances d'un même objet, Instance1 et Instance2, et que je souhaite m'abonner à l'événement NouveauButPourLOM. Comme dans mon cas le traitement que j'ai à faire dans cet événement côté abonné est identique pour les deux instances, je me dis que cela serait sympa de n'utiliser qu'une seule et même méthode pour traiter les deux événements. Et bien l'argument sender m'a justement me permettre de savoir si l'événement a été émis par Instance1 ou Instance2. A noter que si nous décidions que notre méthode CopieFichier était statique (static ou Shared), ce premier argument n'aurait pas de sens et ne figurerait pas dans la signature.

Généralement, l'événement émis par une classe à destination d'une autre classe abonnée transporte avec lui un certain nombre d'informations qui pourraient être utiles à l'abonné. On pourrait être tenté de créer autant de paramètres à la signature de notre Delegate/Event que nécessaire. Ce n'est pas l'approche qui a été choisie par les développeurs du framework .NET.

Lorsqu'on souhaite déclarer un évenement qui n'a aucun paramètre particulier à transmettre, on utilise comme deuxième paramètre du Delegate/Event la classe System.EventArgs. Voici ce que la documentation indique sur cette classe :

EventArgs est la classe de base des classes contenant des données d'événement.Cette classe ne contient pas de données d'événement; elle est utilisée par des événements qui ne passent pas d'informations d'état à un gestionnaire d'événements lorsqu'un événement est déclenché. Si le gestionnaire d'événements nécessite des informations d'état, l'application doit dériver une classe à partir de cette classe pour contenir les données.

Et voilà tout est dit, dès qu'on veut passer un certain nombre d'informations à un Delegate/Event, on écrit une classe qui dérive de la classe EventArgs à laquelle on ajoute les informations supplémentaires. Vous avez noté que le nom de ces classes se terminent généralement par EventArgs.

Pour l'événement CopieDemarrage, ce dont on a besoin est simplement la taille du fichier à copier. Ainsi cela permettra d'être notifié de l’imminence du démarrage de la copie et de pouvoir ajuster les valeurs de notre gauge de progression en fonction de la taille du fichier à copier. De plus, il pourrait être utile de pouvoir récupérer les chemins complets des fichiers source et destination. Cela nous donne :

namespace Librairie_CS {

 public class CopieDemarrageEventArgs : System.EventArgs {

  private string fichierSource = string.Empty;
public string FichierSource {

   get {

    return(this.fichierSource);
}
}

  private string fichierDestination = string.Empty;
public string FichierDestination {

   get {

    return(this.fichierDestination);
}
}

  private long tailleFichier = 0;
public long TailleFichier {

   get {

    return(this.tailleFichier);
}
}

  public CopieDemarrageEventArgs(string fichierSource, string fichierDestination, long tailleFichier) {

   this.fichierSource = fichierSource;
this.fichierDestination = fichierDestination;
this.tailleFichier = tailleFichier;
}
}
}

Namespace Librairie_VB

    Public Class CopieDemarrageEventArgs

        Inherits System.EventArgs

        Private privateFichierSource As String = String.Empty
Public ReadOnly Property FichierSource() As String
Get
Return (Me.privateFichierSource)
End Get
End Property

        Private privateFichierDestination = String.Empty
Public ReadOnly Property FichierDestination() As String
Get
Return (Me.privateFichierDestination)
End Get
End Property

        Private privateTailleFichier As Long = 0
Public ReadOnly Property TailleFichier() As Long
Get
Return (Me.privateTailleFichier)
End Get
End Property

        Public Sub New(ByVal source As String, ByVal destination As String, ByVal taille As Long
            Me.privateFichierSource = source
Me.privateFichierDestination = destination
Me.privateTailleFichier = taille
End Sub

    End Class

End Namespace

Pour l'événement CopieFin, ce dont on a besoin est simplement le statut final de la copie. On sait que la copie peut s'achever de trois manières différentes : Succes, Echec ou encore Annulation (par l'utilisateur). C'est un cas idéal de création d'une énumération :

namespace Librairie_CS {

 public enum StatutCopie {

  Aucun,
Succes,
Echec,
Annulation
}
}

Namespace Librairie_VB

    Public Enum StatutCopie
Aucun
Succes
Echec
Annulation
End Enum

End Namespace

De plus, il peut être utile d'indiquer dans cet événement en combien de temps s'est déroulé l'opération de copie. Cela nous donne finalement ceci:

namespace Librairie_CS {

 public class CopieFinEventArgs : System.EventArgs {

  private StatutCopie statutCopie = StatutCopie.Aucun;
public StatutCopie StatutCopie {

   get {

    return(this.statutCopie);
}
}

  private int duree = 0;
public int Duree {

   get {

    return(this.duree);
}
}

  public CopieFinEventArgs(StatutCopie statutCopie, int duree) {

   this.statutCopie = statutCopie;
this.duree = duree;
}
}
}

Namespace Librairie_VB

    Public Class CopieFinEventArgs

        Inherits System.EventArgs

        Private privateStatutCopie As StatutCopie = StatutCopie.Aucun
Public ReadOnly Property StatutCopie() As statutCopie
Get
Return (Me.privateStatutCopie)
End Get
End Property

        Private privateDuree As Integer = 0
Public ReadOnly Property Duree() As Integer
Get
Return (Me.privateDuree)
End Get
End Property

        Public Sub New(ByVal statutCopie As StatutCopie, ByVal duree As Integer)
Me.privateStatutCopie = statutCopie
Me.privateDuree = duree
End Sub

    End Class

End Namespace

Finalement, pour CopieEnCours, il faudrait fournir le nombre d'octets déjà copiés au moment de l'émission de l'événement. Aucune difficulté particulière ici. Par contre, on souhaite également donner la possibilité à l'abonné de pouvoir annuler la copie à tout le moment. L'utilisation de cet événement pour permettre l'annulation de l'opération est alors l'idéal car appelé très régulièrement durant le processus de copie. Voici ce que cela donne :

namespace Librairie_CS {

 public class CopieEnCoursEventArgs : System.EventArgs {

private long position = 0;
public long Position {

   get {

    return(this.position);
}
}

  private bool cancel = false;
public bool Cancel {

   get {

    return(this.cancel);
}
set {

    this.cancel = value;
}
}

  public CopieEnCoursEventArgs(long position) {

   this.position = position;
}
}
}

Namespace Librairie_VB

    Public Class CopieEnCoursEventArgs

        Inherits System.EventArgs

        Private privatePosition As Long = 0
Public ReadOnly Property Position() As Long
Get
Return (Me.privatePosition)
End Get
End Property

        Private privateCancel As Boolean = False
Public Property Cancel() As Boolean

            Get
Return (Me.privateCancel)
End Get
Set(ByVal Value As Boolean)
Me.privateCancel = Value
End Set
End Property

        Public Sub New(ByVal position As Long)
Me.privatePosition = position
End Sub

    End Class

End Namespace

Notez que dans ce cas, la propriété Cancel doit être en lecture ET écriture pour donner une chance à l'abonné de pouvoir modifier sa valeur.

A ce niveau là, on a fait le plus dur (et ce n'était pas bien dur, il faut bien l'avouer ;-) ). Il nous reste à modifier le code de la fonction CopieFichier pour déclencher l'émission des événements comme il faut. Cela donne ceci :

public void CopieFichier(string cheminFichierDestination, bool ecrasement) {

 // On vérifie si le fichier de destination n'existe pas déjà
if (File.Exists(cheminFichierDestination)) {

  // Si demandé, on écrase le fichier de destination
if (ecrasement) {

   File.Delete(cheminFichierDestination);
}
else {

   throw new IOException(string.Format("Le fichier de destination '{0}' existe déjà.", cheminFichierDestination));
}
}

 StatutCopie statutCopie = StatutCopie.Aucun;
int dureeCopie = System.Environment.TickCount;

 try {

  statutCopie = StatutCopie.Succes;

  // On ouvre le fichier de destination en écriture
using (FileStream fileStreamDestination = new FileStream(cheminFichierDestination, FileMode.Create, FileAccess.Write)) {

   // On ouvre le fichier d'origine en lecture
using (FileStream fileStreamOriginal = new FileStream(this.cheminFichier, FileMode.Open, FileAccess.Read)) {

    // On informe l'abonné de l'imminence de la copie
if (CopieDemarrage != null) {

     CopieDemarrage(this, new CopieDemarrageEventArgs(this.cheminFichier, cheminFichierDestination, fileStreamOriginal.Length));
}

    // On lit le prochain octet
int octet = fileStreamOriginal.ReadByte();
while (octet != -1) {

     // On écrit l'octet
fileStreamDestination.WriteByte((byte)octet);

     // On informe l'abonné de la progression de la copie
if (CopieEnCours != null) {

      CopieEnCoursEventArgs copieEnCoursEventArgs = new CopieEnCoursEventArgs(fileStreamOriginal.Position);
CopieEnCours(this, copieEnCoursEventArgs);

      // L'utilisateur a-t'il demandé l'annulation de la copie ?
if (copieEnCoursEventArgs.Cancel) {

       statutCopie = StatutCopie.Annulation;
break;
}
}

     octet = fileStreamOriginal.ReadByte();
}

    fileStreamOriginal.Close();
}
fileStreamDestination.Close();
}
}
catch {

  // On renvoit telle quelle l'exception qui a lieu,
// l'objectif étant simplement de pouvoir indiquer
// correctement le statut de la copie
statutCopie = StatutCopie.Echec;
throw;
}
finally {

  dureeCopie = System.Environment.TickCount - dureeCopie;

  // On informe l'abonné de la fin de la copie
if (CopieFin != null) {

   CopieFin(this, new CopieFinEventArgs(statutCopie, dureeCopie));
}
}
}

Public Sub CopieFichier(ByVal cheminFichierDestination As String, ByVal ecrasement As Boolean)

 ' On vérifie si le fichier de destination n'existe pas déjà
If File.Exists(cheminFichierDestination) Then

  ' Si demandé, on écrase le fichier de destination
If ecrasement Then
File.Delete(cheminFichierDestination)
Else
Throw New IOException(String.Format("Le fichier de destination '{0}' existe déjà.", cheminFichierDestination))
End If
End If

 Dim statutCopie As statutCopie = statutCopie.Aucun
Dim dureeCopie As Integer = System.Environment.TickCount

 Try

  statutCopie = statutCopie.Succes

  ' On ouvre le fichier de destination en écriture
Dim fileStreamDestination As New FileStream(cheminFichierDestination, FileMode.Create, FileAccess.Write)

  ' On ouvre le fichier d'origine en lecture
Dim fileStreamOriginal As New FileStream(Me.privateCheminFichier, FileMode.Open, FileAccess.Read)

  ' On informe l'abonné de l'imminence de la copie
RaiseEvent CopieDemarrage(Me, New CopieDemarrageEventArgs(Me.privateCheminFichier, cheminFichierDestination, fileStreamOriginal.Length))

  ' On lit le prochain octet
Dim octet As Integer = fileStreamOriginal.ReadByte()

  While octet <> -1
' On écrit l'octet
fileStreamDestination.WriteByte(CType(octet, Byte))

   ' On informe l'abonné de la progression de la copie
Dim copieEnCoursEventArgs As copieEnCoursEventArgs = New copieEnCoursEventArgs(fileStreamOriginal.Position)
RaiseEvent CopieEnCours(Me, copieEnCoursEventArgs)

   ' L'utilisateur a-t'il demandé l'annulation de la copie ?
If copieEnCoursEventArgs.Cancel Then
statutCopie = statutCopie.Annulation
Exit While
End If

   octet = fileStreamOriginal.ReadByte()
End While

  fileStreamOriginal.Close()
fileStreamDestination.Close()

 Catch

  ' On renvoit telle quelle l'exception qui a lieu,
' l'objectif étant simplement de pouvoir indiquer
' correctement le statut de la copie
statutCopie = statutCopie.Echec
Throw

 Finally

  dureeCopie = System.Environment.TickCount - dureeCopie

  ' On informe l'abonné de la fin de la copie
RaiseEvent CopieFin(Me, New CopieFinEventArgs(statutCopie, dureeCopie))

 End Try

End Sub

Ceci étant fait, il ne nous reste plus qu'à compiler l'ensemble de ce code dans une assembly (Librairie_CS.dll Librairie_VB.dll).

Enfin, pour illustrer l'utilisation de ces événements depuis une IHM, nous allons créer un projet de type Application Windows et déposer deux Label, deux TextBox, deux Button et une ProgressBar :

Puis voici le code client :

namespace ClientWindows_CS {

 public class Form1 : System.Windows.Forms.Form {

  ...

  private Librairie_CS.GestionFichiers gestionFichiers = null;
private float taille = 0;
private bool cancel = false;

  private void buttonCopie_Click(object sender, System.EventArgs e) {

gestionFichiers = new Librairie_CS.GestionFichiers(textBoxFichierSource.Text);

   gestionFichiers.CopieDemarrage += new Librairie_CS.CopieDemarrageEventHandler(gestionFichiers_CopieDemarrage);
gestionFichiers.CopieEnCours += new Librairie_CS.CopieEnCoursEventHandler(gestionFichiers_CopieEnCours);
gestionFichiers.CopieFin += new Librairie_CS.CopieFinEventHandler(gestionFichiers_CopieFin);

   gestionFichiers.CopieFichier(textBoxFichierDestination.Text, true);
}

  private void buttonAnnuler_Click(object sender, System.EventArgs e) {

   cancel = true;
buttonAnnuler.Enabled = false;
Application.DoEvents();
}

  private void gestionFichiers_CopieDemarrage(object sender, Librairie_CS.CopieDemarrageEventArgs e) {

   cancel = false;
taille = Convert.ToSingle(e.TailleFichier);
progressBar1.Minimum = 0;
progressBar1.Maximum = 100;
progressBar1.Value = 0;
buttonAnnuler.Enabled = true;

   Application.DoEvents();
}

  private void gestionFichiers_CopieEnCours(object sender, Librairie_CS.CopieEnCoursEventArgs e) {

   e.Cancel = cancel;
progressBar1.Value = Convert.ToInt32(Convert.ToSingle(e.Position) / taille * 100D);

   Application.DoEvents();
}

  private void gestionFichiers_CopieFin(object sender, Librairie_CS.CopieFinEventArgs e) {

   progressBar1.Value = 0;
buttonAnnuler.Enabled = false;
MessageBox.Show(this, string.Format("Statut : {0} - La durée du traitement a été de {1}", e.StatutCopie.ToString(), (new TimeSpan(0, 0, 0, 0, e.Duree)).ToString()), "Copie finie");
}

}

Public Class Form1
Inherits System.Windows.Forms.Form

    Private WithEvents gestionFichiers As Librairie_VB.GestionFichiers
Private taille As Single = 0
Private cancel As Boolean = False

    Private Sub buttonCopie_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles buttonCopie.Click
gestionFichiers = New Librairie_VB.GestionFichiers(textBoxFichierSource.Text)

        ' Autre moyen pour s'abonner aux évenements :
' -------------------------------------------
'AddHandler gestionFichiers.CopieDemarrage, AddressOf gestionFichiers_CopieDemarrage
'AddHandler gestionFichiers.CopieEnCours, AddressOf gestionFichiers_CopieEnCours
'AddHandler gestionFichiers.CopieFin, AddressOf gestionFichiers_CopieFin

        gestionFichiers.CopieFichier(textBoxFichierDestination.Text, True)
End Sub

    Private Sub ButtonAnnuler_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ButtonAnnuler.Click
cancel = True
ButtonAnnuler.Enabled = False
Application.DoEvents()
End Sub

    Private Sub gestionFichiers_CopieDemarrage(ByVal sender As Object, ByVal e As Librairie_VB.CopieDemarrageEventArgs) Handles gestionFichiers.CopieDemarrage
cancel = False
taille = Convert.ToSingle(e.TailleFichier)
progressBar1.Minimum = 0
progressBar1.Maximum = 100
progressBar1.Value = 0
ButtonAnnuler.Enabled = True

        Application.DoEvents()
End Sub

    Private Sub gestionFichiers_CopieEnCours(ByVal sender As Object, ByVal e As Librairie_VB.CopieEnCoursEventArgs) Handles gestionFichiers.CopieEnCours
e.Cancel = cancel
progressBar1.Value = Convert.ToInt32(Convert.ToSingle(e.Position) / taille * 100D)

Application.DoEvents()
End Sub

    Private Sub gestionFichiers_CopieFin(ByVal sender As Object, ByVal e As Librairie_VB.CopieFinEventArgs) Handles gestionFichiers.CopieFin
progressBar1.Value = 0
ButtonAnnuler.Enabled = False
MessageBox.Show(Me, String.Format("Statut : {0} - La durée du traitement a été de {1}", e.StatutCopie.ToString(), (New TimeSpan(0, 0, 0, 0, e.Duree)).ToString()), "Copie finie")
End Sub

End Class

A noter l'utilisation du mot-clé WithEvents en VB.NET lors de la déclaration de la variable gestionFichiers pour permettre d'utiliser la technique d'abonnement Handles :

Private Sub gestionFichiers_[Event](ByVal sender As Object, ByVal e As Librairie_VB.EventArgs) Handles gestionFichiers.[Event]

A noter également qu'il y'a une autre possibilité pour s'abonner à un événement en VB.NET :

AddHandler gestionFichiers.[Event], AddressOf gestionFichiers

et

Private Sub gestionFichiers_[Event](ByVal sender As Object, ByVal e As Librairie_VB.EventArgs)

 

Voilà, j'espère que cet article vous a été utile pour comprendre la notion Delegate/Event. Si vous avez des commentaires à faire sur cet article, positifs comme négatifs, n'hésitez pas en m'en faire part grâce à la section commentaire ci-dessous.

 

A bientôt pour un autre sujet technique !

 

(les exemples de code utilisés dans cet article sont en libre téléchargement ici )

[Initialement posté le 21/09/2004 à 21:52 ici]