mardi 16 septembre 2008

Programmation événementielle Acte III)

La definition d'un délégué parait plus facile que celle d'une classe et ne requiert pas plus qu'une ou deux lignes de code.

public delegate bool TemperatureChangeHandler(
float newTemperature);

Ce qui est moins evident au sujet de ces deux lignes de code c'est que la definition d'un type délégué est un raccourci syntaxtique pour la definition d'une classe

Le compilateur C# produit alors une classe qui derive de

// ERROR: 'GreaterThanHandler' cannot
// inherit from special class 'System.Delegate'
public class TemperatureChangeHandler: System.MulticastDelegate
{
...
}

 

Bien plus encore la classe System.MulticastDelegate derive de la classe System.Delegate, cependant la syntaxe de definition du délégué n'est pas tout juste un raccourci.  En effet le compilateur C# interdit la déclaration d'une classe qui dérive (directement ou indirectement) de System.Delegate et donc de System.MulticasDelegate.

Chaque niveau de la hierarchie du délégué fournit un ensemble de services different. System.Delegate est un conteneur de données pour une méthode à appeler sur un objet particulier. Avec System.MulticasDelegate viennent les capacités additionnelles non seulement d'invoquer une méthode sur un objet mais également sur une collection d'objets. Cela permet d'avoir plusieurs souscripteurs à un événement.

L'objectif d'une instance unique de délégué est très similaire à un pointeur de method du C++. Cependant en C# on n'utilise pas de pointeurs de méthode, on sauvegarde les métadonnées qui identifient la méthode cible à appeler.

System.Delegate contient deux éléments de données critiques. Premièrement il contient une instance de System.Reflection.MethodInfo – en d'autres termes, les métadonnées du .Net qui permettent l'invocation de méthode via la réflexion.

Le second aspect de System.Delegate est l'instance d'objet sur laquelle la method doit être invoquée. Etant donné un nombre illimité d'objets qui peuvent accepter une méthode correspondant à la signature du MethodInfo, nous avons également besoin d'être capable d'identifier quels objets notifier. La seule exception c'est quand la méthode identifiée par MethodInfo est statique – dans ce cas la référence d'objet stockée par System.Delegate est nulle.

 

Seconde application pratique des événements et délégués

Un événement permet à une classe de notifier que quelque chose d'intéressant se produit. Par exemple, une classe qui encapsule un contrôle d'interface utilisateur peut définir qu'un événement se produise lorsque l'utilisateur clique sur le contrôle. La classe de contrôle ne se soucie pas de ce qui arrive lorsque l'utilisateur clique sur le bouton, mais elle doit signaler aux classes dérivées que l'événement Click a eu lieu. Les classes dérivées peuvent ensuite choisir de quelle manière répondre.

Les événements utilisent des délégués pour fournir l'encapsulation de type sécurisé des méthodes qui seront appelées lors du déclenchement. Un délégué peut encapsuler des méthodes nommées et des méthodes anonymes.

Dans l'exemple suivant, la classe TestButton contient l'événement OnClick. Les classes qui dérivent de TestButton peuvent choisir de répondre à l'événement OnClick, ainsi que définir les méthodes devant être appelées pour gérer l'événement. Plusieurs gestionnaires, sous forme de délégués et de méthodes anonymes, peuvent être spécifiés.

 

// Declare le gestionnaire pour l'événement
public delegate void ButtonEventHandler();
class TestButton
{
  // OnClick est un événement, implementé par un délégué ButtonEventHandler.
 
public event ButtonEventHandler OnClick;
  // A method that triggers the event:
  public void Click()
  {
     OnClick();
  }
}

// Crée une instance de la classe TestButton.
TestButton mb = new TestButton();
// Spécifie la method qui sera déclenchée par l'événement OnClick
mb.OnClick += new ButtonEventHandler(TestHandler);
// Spécifie une méthode additionnelle anonyme
mb.OnClick += delegate
{
   System.Console.WriteLine("Hello, World!");
};
// Déclenche l'événement
mb.Click();

 

 

 

Vue d'ensemble des événements

Les événements ont les propriétés suivantes :

·                           Un événement permet à une classe de notifier aux objets qu'ils doivent effectuer une action quelconque.

·                           L'utilisation la plus commune des événements réside dans les interfaces utilisateur graphiques, bien que des événements puissent être utiles à d'autres moments, comme pour signaler des changements d'état.

·                           Les événements sont généralement déclarés à l'aide de gestionnaires d'événements délégués.

·                           Les événements peuvent appeler des méthodes anonymes à la place de délégués. Pour plus d'informations sur les méthodes, consultez Méthodes anonymes.

 

Utilisation des d'événements

Les événements sont utilisés sur les classes et les structures pour informer les utilisateurs d'un objet que quelque chose d'intéressant se produit concernant cet objet. Cette notification s'appelle déclencher un événement. Un objet déclenchant un événement est désigné par le nom de source ou expéditeur de l'événement. Les objets peuvent déclencher des événements pour diverses raisons : en réponse à une modification des données de l'objet, la fin d'un processus de longue durée ou une interruption de service par exemple, un objet qui utilise des ressources réseau peut déclencher un événement si la connectivité réseau est perdue. Les objets qui représentent des éléments d'interface utilisateur déclenchent habituellement des événements en réponse à des actions utilisateur comme un clic sur un bouton ou une sélection dans un menu.

Déclaration d'événements

Les événements, comme les méthodes, ont une signature qui inclut un nom et une liste de paramètres. Cette signature est définie par un type délégué, par exemple :

public delegate void TestEventDelegate(object sender, System.EventArgs e);

Il est très commun que les événements du .NET Framework aient une signature où le premier paramètre est un objet qui fait référence à la source de l'événement, et le deuxième paramètre est une classe qui transporte des données en rapport avec l'événement. Toutefois, cette conception n'est pas requise ou mise en application par le langage C#. Une signature d'événement peut être identique à toute signature de délégué valide, tant qu'elle retourne une valeur void.

Pour ajouter un événement à une classe, il faut utiliser le mot clé event et fournir un type de délégué et un nom à l'événement. Par exemple :

public class EventSource
{
   public event TestEventDelegate TestEvent; private void RaiseTestEvent()
   {
      /* ...
*/
   }

}

Les événements peuvent être marqués comme étant publics, privés, protégés, internes ou protected internal. Ces modificateurs d'accès définissent comment les utilisateurs de la classe peuvent accéder à l'événement. Pour plus d'informations, consultez Modificateurs d'accès.

Un événement peut être déclaré comme un événement statique à l'aide du mot clé static. Cela rend l'événement disponible aux appelants à tout moment, même si aucune instance de la classe n'existe. Pour plus d'informations, consultez Classes statiques et membres de classe statique.

Un événement peut être marqué comme un événement virtuel à l'aide du mot clé virtual. Cela permet aux classes dérivées de substituer le comportement de l'événement à l'aide du mot clé override. Pour plus d'informations, consultez Héritage (guide de programmation C#). Un événement qui se substitue à un événement virtuel peut également être sealed, en spécifiant que pour les classes dérivées il n'est plus virtuel. Enfin, un événement peut être déclaré abstract, ce qui signifie qu'il n'y a aucune implémentation dans la classe, et que les classes dérivées doivent écrire leur propre implémentation. Pour plus d'informations, consultez Classes et membres de classe abstract et sealed.

Déclenchement d'événements

Pour déclencher un événement, la classe peut appeler le délégué, en passant tous les paramètres liés à l'événement. Le délégué appellera ensuite tous les gestionnaires qui ont été ajoutés à l'événement. Lorsqu'il n'y a pas de gestionnaires pour l'événement, l'événement est null, ainsi, avant de déclencher un événement, les sources de l'événement doivent veiller à ce que l'événement ne soit pas null pour éviter NullReferenceException. Pour éviter une condition de concurrence critique où le dernier gestionnaire peut être supprimé entre le contrôle null et l'appel de l'événement, les sources de l'événement doivent également créer une copie de l'événement avant d'exécuter le contrôle de valeur null et de déclencher l'événement. Par exemple :

private void
RaiseTestEvent()
{
   // Invoque un événement de façon sécure:
   TestEventDelegate temp = TestEvent;
   if (temp != null)
   {
      temp(this, new System.EventArgs());
   }
}

Chaque événement peut avoir plusieurs gestionnaires assignés pour recevoir l'événement. Dans ce cas, l'événement appelle automatiquement chaque récepteur ; le déclenchement d'un événement ne nécessite qu'un appel à l'événement quel que soit le nombre de récepteurs.

Abonnement aux événements

Une classe dont vous voulez qu'elle reçoive un événement peut créer une méthode pour recevoir cet événement, puis ajouter un délégué pour cette méthode dans l'événement de la classe lui-même. Ce processus s'appelle l'abonnement à un événement.

Premièrement, il doit y avoir une méthode sur la classe réceptrice avec la même signature que l'événement lui-même, comme la signature du délégué. Cette méthode, appelée un gestionnaire d'événements, peut ensuite prendre les mesures qui s'imposent en réponse à l'événement. Par exemple :

public class EventReceiver
{
   public void ReceiveTestEvent(object sender, System.EventArgs e)
   {
      System.Console.Write("Event received from ");   
      System.Console.WriteLine(sender.ToString());
    }
}

Il peut y avoir plusieurs gestionnaires par événement. Plusieurs gestionnaires sont appelés séquentiellement par la source. Si un gestionnaire lève une exception, les gestionnaires qui n'ont pas été appelés n'auront pas la possibilité de recevoir l'événement. Pour cette raison, il est recommandé que les gestionnaires d'événements gèrent l'événement rapidement, et évitent de lever des exceptions.

Pour s'abonner un événement, le récepteur doit créer un délégué du même type que l'événement, utilisant le gestionnaire d'événements comme la destination du délégué. Ensuite, le récepteur doit ajouter ce délégué à l'événement sur l'objet source, à l'aide de l'opérateur d'assignation d'addition (+=). Par exemple :

public void Subscribe(EventSource source)
{
   TestEventDelegate temp = new TestEventDelegate(ReceiveTestEvent);
   source.TestEvent += temp;
}

Pour se désabonner d'un événement, le récepteur peut supprimer un délégué au gestionnaire d'événements de l'événement sur l'objet source, à l'aide de l'opérateur d'assignation de soustraction (-=). Par exemple :

public void UnSubscribe(EventSource source) { TestEventDelegate temp = new TestEventDelegate(ReceiveTestEvent); source.TestEvent -= temp; }

Déclaration d'accesseurs d'événement

Dans l'exemple précédent, l'événement TestEvent a été déclaré d'une manière semblable aux champs. Comme un champ, l'événement est directement disponible aux utilisateurs de l'objet, qui peuvent le modifier. Contrairement aux champs, les seules modifications autorisées sont celles opérées par le biais des opérateurs d'assignation d'addition (+=) et de soustraction (-=).

Il est possible de déclarer un événement à l'aide d'accesseurs d'événement. Les accesseurs d'événement utilisent une syntaxe très semblable aux accesseurs de propriété, puisqu'ils utilisent le mot clé add et un bloc de code pour ajouter des gestionnaires d'événements à l'événement, et le mot clé remove et un bloc de code pour supprimer les gestionnaires d'événements de l'événement. Par exemple :

public class EventSource2
{
   private TestEventDelegate TestEventHandlers;
   public event TestEventDelegate TestEvent
   {
      add
      {
         lock (TestEventHandlers)
         {
            TestEventHandlers += value;
         }
      }
      remove
      {
         lock (TestEventHandlers)
         {
            TestEventHandlers -= value;
         }
      }
   }
   private void RaiseTestEvent()
   {
      // Invoque de façon secure un événement.
      TestEventDelegate temp = TestEventHandlers;
      if (temp != null)
      {
         temp(this, new System.EventArgs());
      }
   }
}

Pour utiliser les accesseurs d'événement, la classe qui déclenche l'événement doit avoir un mécanisme pour stocker et récupérer les gestionnaires. L'exemple précédent utilise un champ de délégué privé, TestEventHandlers, et les opérateurs d'assignation d'addition et de soustraction pour ajouter et supprimer des gestionnaires dans la liste. Ce fonctionnement est très similaire au fonctionnement d'un événement déclaré sans accesseurs. Lorsqu'un récepteur d'événement utilise l'opérateur d'assignation d'addition (+=) pour ajouter un gestionnaire à l'événement, l'accesseur add est appelé, et le nouveau gestionnaire est disponible dans l'accesseur comme une valeur nommée de variable locale. Lorsque l'opérateur d'assignation de soustraction (-=) est utilisé, l'accesseur remove est appelé, et le gestionnaire à supprimer est disponible comme une valeur nommée de variable locale. Les accesseurs retournent la valeur void, donc aucune instruction return ne doit retourner de valeur.

L'abonnement et le désabonnement à un événement utilisent la même syntaxe que les accesseurs d'événement soient déclarés par la classe ou non.

Remarque

L'instruction lock est utilisée dans l'exemple précédent pour empêcher que plusieurs threads manipulent simultanément la liste d'événements. Pour plus d'informations, consultez Instructions lock et Threading.

Lorsque des accesseurs sont utilisés pour un événement, la classe peut stocker les gestionnaires d'événements de quelque manière que ce soit. Bien que l'exemple précédent utilise un délégué, d'autres mécanismes peuvent être utilisés. Pour obtenir un exemple, consultez Comment : utiliser une table de hachage pour stocker des instances d'événements.

Les événements qui ne déclarent pas d'accesseurs se voient automatiquement donner des accesseurs thread-safe par le compilateur C#. Les événements abstraits ne peuvent pas déclarer d'accesseurs. Les accesseurs statiques ne peuvent pas utiliser le mot clé this

--
Alain Lompo
Excelta - Conseils et services informatiques
MCT
MCSD For Microsoft .Net
MVP Windows Systems Server / Biztalk Server
Certifié ITIL et Microsoft Biztalk Server

Aucun commentaire: