mardi 16 septembre 2008

Programmation événementielle Actes V)

Comment : créer des gestionnaires d'événements dans l'éditeur de code de Visual C#

L'éditeur de code de Visual C# permet de créer facilement des gestionnaires d'événements pour vos Windows Forms lorsque vous travaillez en mode Design. L'alternative consiste à afficher le code en mode Source et d'ajouter le gestionnaire d'événements dans le code.

Remarque

Les boîtes de dialogue et les commandes de menu qui s'affichent peuvent être différentes de celles qui sont décrites dans l'aide, en fonction de vos paramètres actifs ou de l'édition utilisée. Pour modifier vos paramètres, choisissez Importation et exportation de paramètres dans le menu Outils. Pour plus d'informations, consultez Paramétrage de Visual Studio.

Pour créer un gestionnaire d'événements à l'aide de l'éditeur de code de Visual C#

1.               Si la fenêtre Propriétés n'est pas visible, en mode Design, cliquez avec le bouton droit sur le formulaire ou le contrôle pour lequel vous souhaitez créer un gestionnaire d'événements et sélectionnez Propriétés.

2.               Dans la partie supérieure de la fenêtre Propriétés, cliquez sur le bouton Événements.

3.               Double-cliquez sur l'événement que vous voulez créer, par exemple l'événement Load.

Visual C# crée une méthode de gestionnaire d'événements vide et l'ajoute à votre code. Vous pouvez également ajouter à la main le code en mode Code. Par exemple, les lignes de code suivantes déclarent un gestionnaire d'événements de charge pour une classe Form appelée Form1.

private void Form1_Load(object sender, System.EventArgs e)
{
   // Ajouter le code pour le gestionnaire d'événement du chargement du formulaire
 }

Création d'une application MDI avec Visual C#

Dans cette section nous allons aborder les bases de la construction d'une application MDI avec Visual C#. Le résultat devrait donner une application semblable à la copie d'écran ci-dessous.

 


Création de l'application :

Tout d'abord créons une nouvelle application. (Menu : 'Fichier' -> 'Nouveau' -> 'Application C#') Nommez là d'un nom explicite. La fiche principale nommée "WinForm" est créée automatiquement. Nous reviendrons plus tard sur cette fiche. Nous allons aborder en premier lieu la construction de la fiche enfant.

 

Construction de la fiche enfant MDI :

Nous créons donc la fiche comme n'importe quelle autre fiche. (Menu : 'Fichier' -> 'Nouveau' -> 'Autre', puis dans l'arborescence de la fenêtre : 'Projet C#' -> 'Nouveaux fichiers' choisir 'Windows Form'.) La nouvelle fiche nommée "WinForm1" étant créée, afin de l'agrémenter nous poserons dessus une TextBox "textBox1" multilignes (propriété Multiline à True) et remplissant la zone client de la fiche (propriété Dock à Fill).
Ensuite posons sur la fiche un MainMenu avec un menu principal nommé "Enfant" (nommé ainsi afin de le repérer quand il va se fusionner avec les menus de la fiche parent). Ajoutons lui deux options de menu nommées "Ecrire" et "Fermer".
Sur l'événement Click de Fermer nous fermerons la fiche :

    private void Fermer_Click(object sender, System.EventArgs e)
    {
      Close();
    }

Sur l'événement Click de Ecrire nous insérerons une ligne de texte dans sa TextBox "textBox1" :

    private void Ecrire_Click(object sender, System.EventArgs e)
    {
      textBox1.Text =
             textBox1.Text.Insert(0,"Texte ajouté depuis le menu enfant.\r\n");
    }

Pour différencier visuellement chaque fiche enfant nous ajouterons un numéro à son titre correspondant à son ordre de création :

     //.......
     private static int count = 0 ;
     //.......
     public WinForm1() // Constructeur
     {
       InitializeComponent();
 
       count++;
       Text = "Fenetre N° " + count;
     }

Nous déclarons pour cela une variable membre static (count), c'est-à-dire unique dans la classe, que nous incrémentons à chaque création d'une nouvelle fenêtre enfant et que nous ajoutons au titre de la fenêtre ceci dans le constructeur de la classe.

Ce que nous avons fait jusqu'ici et tout à fait semblable au traitement d'une fiche ordinaire et n'a rien d'exceptionnel mais va nous servir pour la suite.

 

Construction de la fiche parent MDI :

Revenons à la fiche principale "WinForm" qui avait été créée au début. Première chose à faire, lui donner la capacité d'être une fiche parent MDI, pour cela il suffit juste de passer sa propriété IsMdiContainer à True.
Maintenant ajoutons lui un MainMenu avec un Menu principal nommé "Parent" (On peut voir les options Menu que nous allons ajouter ultérieurement sur la copie d'écran au début de cet article)


Création d'une instance de la fiche enfant :

La première option de menu que nous ajoutons est "Nouveau", sur son événement Click nous créons une fiche enfant selon le modèle de notre fiche précédente "WinForm1":

    private System.Windows.Forms.Form form1;
  
    //.......
 
    private void Nouveau_Click(object sender, System.EventArgs e)
    {
      form1 = new WinForm1();
      form1.MdiParent = this;
      form1.Show();
    }

A la différence d'une fiche ordinaire, il faut lui donner une fiche parent MDI en affectant sa propriété MdiParent à this avant de la rendre visible. (this représentant bien sûr la fiche principale.)


Accès à la fiche enfant depuis la fiche parent :

Ajoutons une 2ème option de menu dans le menu "Parent" nommé "Fermer" sur son événement Click nous fermerons la fiche enfant active :

    private void Fermer_Click(object sender, System.EventArgs e)
    {
      // Fermer la fiche enfant active.
      if(ActiveMdiChild!=null) ActiveMdiChild.Close();
    }

Comme vous pouvez le remarquer dans ce morceau de code on accède à la fiche enfant active par l'intermédiaire de la propriété ActiveMdiChild de la fiche parent. Avant de faire toute intervention sur la fiche enfant depuis la fiche parent, il faut tester qu'elle existe bien d'où le test if dans ce code.


Fermer toutes les fiches enfant :

Sur l'événement Click d'une autre option de menu nommé "Tout Fermer" mettre :

    private void ToutFermer_Click(object sender, System.EventArgs e)
    {
      //Fermer toutes les fiches enfants.
      while (MdiChildren.Length > 0)  MdiChildren[0].Close();
    }

Comme vous pouvez le voir dans ce morceau de code on peut aussi accéder aux fiches enfant par un tableau indicé nommé MdiChildren.


Accès à un composant de la fiche enfant depuis la fiche parent :

Sur l'événement Click d'une autre option de menu nommé "Ecrire" nous ajoutons une ligne de texte sur la TextBox de la fiche enfant active :

    private void Ecrire_Click(object sender, System.EventArgs e)
    {
      int n = 0;
 
      while(ActiveMdiChild!=null && n < ActiveMdiChild.Controls.Count)
      {
         if( ActiveMdiChild.Controls[n].Name == "textBox1" )
                 ((TextBox)ActiveMdiChild.Controls[n]).Text =
                         ((TextBox)ActiveMdiChild.Controls[n]).Text.Insert(0,
                                   "Texte ajouté depuis le menu parent.\r\n");
         n++;
      }
    }

Nous n'avons pas d'accès direct aux contrôles des fiches enfant, nous sommes donc obligés de passer par une propriété tableau indicée Controls pour retrouver notre contrôle. Tester ici aussi qu'il existe bien une fiche enfant avant d'exécuter une quelconque action dessus.


Arrangement des fiches enfant dans la fiche parent :

Pour l'exemple nous allons créer un menu principal nommé "Fenêtres" contenant trois options de menu nommées "Cascade", "Horizontale" et "Verticale" dont voici le code de leurs événements Click respectifs:

    private void Cascade_Click(object sender, System.EventArgs e)
    {
      LayoutMdi(MdiLayout.Cascade);
    }
    
    private void Horizontal_Click(object sender, System.EventArgs e)
    {
      LayoutMdi(MdiLayout.TileHorizontal);
    }
    
    private void Vertical_Click(object sender, System.EventArgs e)
    {
      LayoutMdi(MdiLayout.TileVertical);
    }


Liste des fenêtres enfant dans le menu :

Pour obtenir cette liste dans un menu, il suffit tout simplement de passer la propriété MdiList du menu en question à True par exemple celle du menu "Fenêtre" précédemment créé.


Arrangement des menus :

Chaque Menu possède une propriété MergeOrder que vous pouvez initialiser à la conception pour organiser l'ordre de fusion des menus. Dans l'exemple je l'ai laissé à 0 pour le menu "Parent" mis à 2 pour le menu "Fenêtre" et je l'ai mis à 1 pour le menu "Enfant" de la fiche enfant.



 

 

 

 

 

 

 

 

 

 

 

 

 

Autre illustration de la mise en place des événements et délégués

Une question qui revient assez souvent dans les discussions au sujet des événements est

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

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 ) 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();
   }
  }
 }
}

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;

 

 

 

 

Imaginons que nous déclarions un Event comme suit :

        public event Test(integer monParam )

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

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)

    --
    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: