Routed commands en WPF en 2 min

Les commandes routées WPF (i.e. routed commands) correspondent à une fonctionnalité permettant de découpler les éléments qui déclenchent une commande des éléments cibles sur lesquels les commandes vont s’exécuter.

D’autres part les commandes routées permettent d’associer facilement des actions provenant de raccourcis clavier, d’actions réalisées avec la souris etc…
Enfin elles peuvent activer ou désactiver un control graphique en fonction de la disponibilité de la commande.

Caractéristiques des commandes routées

Interface ICommand

La fonctionnalité des commandes routées est accessible par l’intermédiare de l’interface System.Windows.Input.ICommand:

public interface ICommand 
{ 
    event EventHandler CanExecuteChanged; 
 
    bool CanExecute(object parameter); 
     
    void Execute(object parameter); 
}

Ainsi:

  • Execute: cette méthode contient le code qui sera exécuté au déclechement de la commande.
  • CanExecute: cette méthode sera exécutée avant Execute() pour déterminer si la commande peut être exécutée. Elle renvoie true si l’exécution est possible, false sinon.
  • CanExecuteChanged: cette évènement se déclenche quand la valeur de CanExecute change.

De nombreux éléments de base WPF fournissent cette interface de façon à y affecter une implémentation.

Par exemple:

  • ButtonBase.Command (dans System.Windows.Controls.Primitives)
  • MenuItem.Command (dans System.Windows.Controls)
  • CheckBox.Command (dans System.Windows.Controls)
  • etc…

De nombreux autres éléments de base proposent cette interface.

L’intérêt de cette propriété pour ces différents éléments est de pouvoir affecter une implémentation qui définit la commande qui sera déclenchée par l’élément.

Aspect routé

Les commandes routées ont la particularité d’être “routées” c’est-à-dire qu’elles sont associées à des évènements routés (cf. Routed events en WPF en 3 min).

Ainsi lorsque la commande est déclenchée, l’élément qui a déclenché cette commande ainsi que la commande elle même ignorent complêtement l’élément qui va exécuter réellement la commande. A vrai dire, il peut ne pas y avoir d’éléments qui va exécuter la commande.

Au déclenchement de la commande, des évènements routés seront déclenchés et se propageront le long de l’arbre visuel (cf. Arbre logique et arbre visuel WPF en 2 min) de façon à savoir si la commande peut être exécutée et ensuite pour l’exécuter réellement.

La propagation se fait de la même façon qu’un évènement routé normal. 2 séries de 2 évènements sont ainsi lancées. La première serie permet à un élément se trouvant dans l’arbre visuel s’il peut exécuté la commande:

  • PreviewCanExecute: évènement “tunnel” qui se propage de l’élément racine vers l’élément source (celui qui référence la commande).
  • CanExecute: évènement “bubble” qui se propage en sens inverse de la source vers l’élément racine de l’arbre.

Si un élément indique qu’il peut exécuter la commande, la 2e série d’évènements routés se déclenche:

  • PreviewExecuted: évènement “tunnel” se propageant de la racine de l’arbre vers l’élément source.
  • Executed: évènement “bubble” se propageant de l’élément source vers la racine de l’arbre.

Ainsi n’importe quel éléments capable d’intercepter ces évènements sera en mesure d’exécuter une action correspondant à la commande.

Routage des commandes

Lors du parcours des évènements le long de l’arbre visuel, les handlers (c’est-à-dire le code exécuté au déclenchement de la commande) sont exécutés successivement à la suite en fonction de leur position dans cet arbre.

Le parcours de fait jusqu’à l’élément qui a le focus. Dans le cas où cet élément se trouve dans un “container” avec FocusManager.IsFocusScope à true le parcours se fait de façon plus complexe que celui évoqué précédement. Pour plus de détails sur ce point, se reporter à The Truth about Routed Commands Routing.

Implémentations des commandes routées

Il existe des implémentations différentes permettant d’utiliser les commandes routées, toutefois dans tous les cas, les mêmes instantications sont nécessaires:

  • Définir la commande routée en elle-même: cette commande est une instance de System.Windows.Input.RoutedCommand. Cette définition permet d’indiquer la classe propriétaire de la commande et le nom de la commande.
    Il est aussi possible de définir des commandes étant des instances de System.Windows.Input.RoutedUICommand (RoutedUICommand dérive de RoutedCommand).
  • Définir un binding entre la commande et des handlers. Ces handlers correspondent à des implémentations pour CanExecute() et Execute(), ils seront exécutés lorsque les évènements correspondant seront déclenchés.
  • On indique l’élément qui va déclencher la commande c’est-à-dire l’invoker. Par exemple, ça peut être le bouton sur lequel on devra cliquer pour déclencher la commande.

Commandes prédéfinies

Un certain nombre de commandes sont déjà définies et peuvent être utilisées directement. On peut trouver une définition de ces commandes dans:

  • System.Windows.Input.ApplicationCommands,
  • System.Windows.Input.ComponentCommands,
  • System.Windows.Input.MediaCommands,
  • System.Windows.Input.NavigationCommands,
  • System.Windows.Documents.EditingCommands,

Par exemple, les commandes “Cut”, “Copy” et “Paste” se trouvent dans ApplicationCommands. On peut en voir la définition dans le code source de WPF: le fichier ApplicationCommands.cs

Pour utiliser ces commandes, il suffit de définir un binding et l’invoker.

Par exemple, si on veut exécuter une commande à l’exécution de la commande ApplicationCommands.Paste, il faut définir le binding avec un CommandBinding dans un user control:

<Window x:Class="RoutedCommandExample.Window1" 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:local="clr-namespace:RoutedCommandExample" 
    Title="Window1" Height="300" Width="300"> 
  <Window.CommandBindings> 
    <CommandBinding 
      Command="ApplicationCommands.Paste" 
      CanExecute="PasteCommandHandler_CanExecute" 
      Executed="PasteCommandHandler_Executed" /> 
  </Window.CommandBindings> 
  <!-- ... --> 
</Window>

Le code behind contiendra l’implémentation correspondant aux handlers:

public partial class Window1 : Window 
{ 
  public Window1() 
  { 
      InitializeComponent(); 
  } 
 
  private void PasteCommandHandler_CanExecute(object sender, CanExecuteRoutedEventArgs e) 
  { 
      e.CanExecute = true; 
  } 
 
  private void PasteCommandHandler_Executed(object sender, ExecutedRoutedEventArgs e) 
  { 
      // Code exécuté au déclenchement de la commande 
  } 
}

Les handlers se déclencheront à chaque fois que la commande ApplicationCommands.Paste est exécutée.

On peut complêter cet exemple en ajoutant un bouton pour déclencher l’évènement:

<Button  
      Command="ApplicationCommands.Paste"  
      Content="Raise Paste" />

Définir une commande

Si on souhaite définir sa propre commande sans passer par une commande prédéfinie, il suffit d’ajouter dans le code behind:

private static RoutedCommand customCommand =  
    new RoutedCommand("CustomCommand", typeof(Window1)); 
 
public static RoutedCommand CustomCommand 
{ 
    get { return customCommand; } 
}

Le binding et l’invoker sont similaires à l’exemple précédent et font référence à cette commande:

<Window.CommandBindings> 
    <CommandBinding  
        Command="{x:Static local:Window1.CustomCommand}" 
        CanExecute="PasteCommandHandler_CanExecute" 
        Executed="PasteCommandHandler_Executed" /> 
</Window.CommandBindings> 
<Grid> 
    <Button Command="{x:Static local:Window1.CustomCommand}" 
        Name="myButton" Content="Raise Custom command"/> 
</Grid>

RoutedUICommand

A la différence de RoutedCommand, une RoutedUICommand possède une propriété Text sur laquelle il est possible de binder des éléments. La définition d’une commande de ce type est semblable:

private static RoutedUICommand _pressMeCommand =  
    new RoutedUICommand("Custom Command", "CustomCommand", typeof(Window1));

On peut, par exemple, binder le texte du bouton sur le texte de la commande:

<Button Command="{x:Static local:Window1.CustomCommand}" 
        Content="{Binding RelativeSource={RelativeSource Self}, Path=Command.Text}" />

Effectuer le binding dans le code behind

Au lieu de définir le binding dans le code XAML, on peut le faire dans le code behind:

public partial class Window1 : Window  
{ 
  public readonly RoutedCommand customCommand; 
  public readonly CommandBinding binding; 
 
  public Window1()  
  { 
      customCommand = new RoutedCommand("CustomCommand", typeof(Window1)); 
 
      InitializeComponent(); 
      this.DataContext = this; 
 
      this.binding = new CommandBinding(customCommand); 
      this.CommandBindings.Add(this.binding); 
 
      this.binding.Executed += PasteCommandHandler_Executed; 
      this.binding.CanExecute += PasteCommandHandler_CanExecute; 
  } 
 
  private void PasteCommandHandler_CanExecute(object sender, CanExecuteRoutedEventArgs e)  
  { 
      e.CanExecute = true; 
  } 
 
  private void PasteCommandHandler_Executed(object sender, ExecutedRoutedEventArgs e)  
  { 
      // Code exécuté au déclenchement de la commande 
  } 
}

CommandTarget

Lors du déclenchement de la commande:

  • La source est l’élément qui a déclenché la commande,
  • Le “sender” est la fenêtre sur laquelle est définie le binding.

On peut changer la source de la commande en utilisant l’argument System.Windows.Input.ICommandSource.CommandTarget. La plupart des éléments implémentent cette propriété.

Dans l’exemple précédent, si on ajoute une TextBox:

<Window.CommandBindings> 
    <CommandBinding  
        Command="{x:Static local:Window1.CustomCommand}" 
        CanExecute="PasteCommandHandler_CanExecute" 
        Executed="PasteCommandHandler_Executed" /> 
</Window.CommandBindings> 
<Grid> 
    <TextBox Name="newTextBox" Width="200" Height="40" /> 
    <Button Command="{x:Static local:Window1.CustomCommand}" 
        Name="myButton" Content="Raise Custom command"/> 
</Grid>

On peut paramétrer l’argument CommandTarget du bouton pour que la source soit la TextBox:

<Button Command="{x:Static local:Window1.CustomCommand}" 
      CommandTarget="{Binding ElementName=newTextBox}" 
      Name="myButton" Content="Raise Custom command"/>

A l’exécution, dans le handler, la source de la commande sera la TextBox.

CommandParameter

On peut ajouter un paramètre qui sera renseigné au déclenchement de la commande en utilisant System.Windows.Input.ICommandSource.CommandParameter. La plupart des éléments de base implémentent cette propriété.

Si on l’utilise dans le cadre d’un bouton:

<Button Command="{x:Static local:Window1.CustomCommand}" 
      CommandParameter="Parameter1" 
      Name="myButton" Content="Raise Custom command"/>

Ce paramètre pourra être utilisé au déclenchement de la commande dans le handler:

public void PasteCommandHandler_Executed(object sender, ExecutedRoutedEventArgs e) 
{ 
    MessageBox.Show(e.Parameter); 
}

KeyBinding

Les key bindings permettent d’indiquer des raccourcis clavier ou des actions à la souris qui déclencheront des commandes.

On peut définir ces key bindings dans le code XAML:

<Window x:Class="RoutedCommandExample.Window1" 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:local="clr-namespace:RoutedCommandExample" 
    Title="Window1" Height="300" Width="300"> 
  <Window.CommandBindings> 
    <CommandBinding 
      Command="ApplicationCommands.Paste" 
      CanExecute="PasteCommandHandler_CanExecute" 
      Executed="PasteCommandHandler_Executed" 
      /> 
  </Window.CommandBindings> 
  <Window.InputBindings> 
    <KeyBinding Command="ApplicationCommands.Paste" 
      Gesture="Ctrl+M"/> 
  </Window.InputBindings> 
  <!-- ... --> 
</Window>

Il est possible de définir un key binding pour une commande qui n’est pas pré-définie:

<KeyBinding Command="{x:Static local:Window1.CustomCommand}" Gesture="Ctrl+M"/>

On peut aussi utiliser la syntaxe suivante pour définir le key binding:

<KeyBinding Command="{x:Static local:Window1.CustomCommand}" Modifiers="Control" Key="M"/>

On peut aussi définir ce key binding dans le code behind:

public Window1()  
{ 
    InitializeComponent(); 
 
    this.DataContext = this; 
  
    this.InputBindings.Add(new KeyBinding(this.customCommand, 
        new KeyGesture(Key.M, ModifierKeys.Control))); 
}
Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInEmail this to someonePrint this page

Git en 5 min

Git est un gestionnaire de code source initialement créé et développé par Linus Torvarlds. Même si la signication du mot Git est plutôt péjorative, il s’agit très puissant qui propose plus de fonctionnalités que la plupart des autres gestionnaires de code source.

Comparaison avec les autres gestionnaires de code source

Il est rare d’aborder Git sans jamais avoir touché à un autre gestionnaire de code source (i.e. VCS pour Version Control System). Toutefois même on étant aguérri à l’utilisation de logiciels de gestion de source, il est préférable d’aborder Git sans essayer de transposer ses connaissances aux commandes. Par exemple même si certaines commandes sont communes entre SVN et Git, leur fonction est pratiquement toujours très différente.

Il faut donc aborder Git en essayant d’avoir un œil neuf et s’intéresser à la fonction réelle de chaque commande.

Les principales caractéristiques de Git sont:

  • Git est un système de contrôle de version distribué: chaque client qui se connecte à Git possède une copie locale du dépôt distant. Si ce dernier disparait, il peut être restoré à partir d’une des copies des clients.
  • Le stockage des fichiers est différent de la plupart des autres VCS. Au lieu de stocker des copies différentes des fichiers pour chaque version, Git stocke des versions de fichier seulement s’ils sont modifiés. Chaque version du dépôt est un “instantané” contenant un ensemble de fichiers dans une version précise. Il n’y a pas de copies inutiles lorsqu’un fichier n’est pas modifié.
  • La plupart des actions de l’utilisateur sont exécutées localement, ce qui limite la propagation d’erreurs dans le versionnement. Il est facile de revenir en arrière puisque la plupart de ces commandes sont locales et n’affectent pas d’autres utilisateurs.

La plus grosse différence entre Git et les autres VCS, est que le répertoire de travail ne change pas si on passe d’une branche à l’autre.

Par exemple, dans SVN si on souhaite avoir la branche principale (i.e. le trunk) et une autre branche, il faut faire un checkout dans 2 répertoires différents. Avec Git, on travaille toujours dans le même répertoire, quelque soit la branche sur laquelle on travaille. C’est Git qui modifie les fichiers de ce répertoire en fonction de la branche sur laquelle on travaille.

Quelques éléments sont caractéristiques de Git:

  • Clé SHA-1: tous les commits sont identifiés avec une clé SHA-1 unique. Cette clé peut être utilisée pour récupérer un commit particulier.
  • HEAD: il s’agit d’une référence vers le nœud vers lequel pointe le répertoire de travail.
  • Dépôt distant (i.e. remote repository): il s’agit du répertoire de travail distant qui est partagé. Chaque client possède une copie locale de ce répertoire. Les clients ne travaillent pas directement à partir du dépôt distant, ils travaillent sur le dépôt local. Au moment de la livraison d’une modification sur le code source, le développeur envoie ces modifications sur le dépôt distant de façon à partager la modification avec d’autres développeurs.
  • Dépôt local (i.e. local repository): il s’agit de la copie locale du dépôt distant. Le développeur travaille généralement sur cette copie. A la différence des autres VCS, le développeur peut effectuer des commits, modifier ses commits, créer des branches sur son dépôt local. Ces modifications sont locales et ne sont pas visibles des autres développeurs. Au moment de la livraison de son travail, le développeur peut décider d’envoyer une partie de ses modifications de son dépôt local vers le dépôt distant, et ainsi partager son travail.

Installation

Sur windows, on peut installer Git for windows qui propose un bash pour utiliser les commandes Git et quelques outils graphiques accessibles avec le bouton droit de la souris (une fenêtre pour effectuer des commits, une fenêtre pour changer de branche, affichage des logs, etc…).

Git for windows se télécharge sur: https://git-for-windows.github.io/.

Premières étapes

Renseigner quelques paramètres

Il peut être nécessaire de préciser certains paramètres qui seront utilisés par la suite pour l’exécution de chaque commande: nom de l’utilisateur, adresse mail, coloration syntaxique du bash etc…

Pour préciser ces paramètres, on utilise la commande git config:

$ git config --global user.name "Robert Mitchoum" 
$ git config --global user.email robertmitchoum@caramail.com

Ces paramètres sont rajoutés dans le fichier .gitconfig. Pour savoir où se trouve ce fichier, on peut taper:

$ git -c 'user.cmdline=true' config --list --show-origin

On peut lister tous les paramètres qui se trouvent dans ce fichier en faisant:

$ git config --list

Pour indiquer la valeur d’un paramètre:

$ git config user.email 

Initialiser le répertoire de travail (git init)

Comme indiqué plus haut, avec Git, on reste toujours dans le même répertoire quelque soit la branche, le tag ou la version sur laquelle on travaille.

Pour initialiser le répertoire de travail:

$ mkdir working_dir 
$ cd working_dir 
$ git init 
Initialized empty Git repository in /Users/mitchoum/working_dir/.git/ 

A ce stade le répertoire est presque vide. En réalité il contient un répertoire appelé “.git” qui contient les fichiers de travail de Git.

Cloner un dépôt distant (git clone)

Dans le cas où un dépôt distant existe et contient déjà des fichiers versionnés, on peut récupérer le contenu de ce dépôt sur son dépôt local en effectuant une étape de “cloning” (cette étape est l’équivalent du checkout sur SVN). Cette étape effectue seulement une copie du dépôt distant sur le dépôt local.

On se place dans le répertoire de travail (après avoir exécuté git init) et on exécute l’instruction suivante:

$ git clone [adresse du dépôt distant]  

L’adresse du dépôt distant est généralement du type: https://github.com/repository/test.git

Par exemple:

$ git clone https://github.com/repository/test.git 
$ Cloning into 'test'... 
$ remote: Counting objects: 12, done. 
$ remote: Compressing objects: 100% (4/4), done. 
$ remote: Total 12 (delta 1), reused 11 (delta 0), pack-reused 0 
$ Unpacking objects: 100% (12/12), done. 

Afficher l’état du dépôt (git status)

L’état du dépôt est une information importante car elle permet d’indiquer dans quelle branche on se trouve et l’état des fichiers qui s’y trouvent.

Pour afficher l’état, on écrit:

$ git status 
On branch master 
 
Initial commit 
 
nothing to commit (create/copy files and use "git add" to track) 

Effectuer des commits

Quand le dépôt distant est copié localement (cf. git clone), le developeur peut modifier le code source et ensuite commiter ses modifications. A la fin de cette étape, les modifications restent locales et ne sont pas partagées.

Workflow pour effectuer un commit

Le workflow n’est pas tout à fait similaire aux autres VCS. Pour commiter dans Git, il faut effectuer les étapes suivantes:

  • Ajouter les fichiers à commiter: cette étape ajoute les fichiers à versionner mais elle est aussi nécessaire pour livrer une modification. Elle consiste à indexer certains fichiers (i.e. Stage) pour les commiter ensuite dans le HEAD.
  • Commiter les fichiers sur le dépôt local.

Ajouter les fichiers à commiter (git add)

Cette étape est nécessaire pour indiquer les fichiers à versionner mais elle permet aussi d’indiquer quels sont les fichiers à commiter lorsqu’ils sont déjà versionnés. En pratique, cette étape ajoute un fichier dans un “index”. Dans l’arbre Git, un “index” est un espace de transit où les fichiers sont stockés provisoirement avant d’être commités.

git add [chemin du fichier à ajouter] 

Enlever un fichier des fichiers à commiter

Si on a exécuté un git add par mégarde pour un fichier, on peut revenir en arrière en faisant:

git reset HEAD [chemin du fichier] 

Commiter des fichiers “stagés”

A la fin de cette étape, le commit est local et non visible des autres développeurs:

  • git commit pour commiter et ouvrir un éditeur de texte pour indiquer un commentaire.
  • git commit -a pour commiter tous les fichiers se trouvant dans le répertoire (il n’est pas nécessaire de les “stager” avant.
  • git commit -m pour commiter directement avec un message.

Supprimer des fichiers versionnés

Il faut d’abord supprimer le fichier de Git:

git rm [chemin du fichier à supprimer] 

Il faut ensuite commiter pour que la suppression de Git soit effective. Si le fichier a été modifié depuis sa version commitée, pour le supprimer, il faut forcer sa suppresion:

git rm -f [chemin du fichier à supprimer] 

On peut aussi supprimer un fichier parmi les fichiers “stagés” sans le supprimer complétement:

git rm --cached [chemin du fichier] 
Utilisation de vi

Pour chaque commande où il est nécessaire d’ajouter des commentaires (comme git commit ou git rebase) ou pour les commandes affichant beaucoup de lignes (comme git log), le bash ouvre un fenêtre vi. L’utilisation de cet éditeur n’est pas toujours facile.
Quand vi est ouvert, il est en mode commande, il faut indiquer des commandes pour modifier le fichier:

  • Pour passer en mode édition et modifier le fichier: taper "i", effectuer les modifications et taper “Echap” pour sortir du mode édition.
  • Pour passer en mode remplacement: taper "R", effectuer le remplacement et taper “Echap” pour sortir de ce mode.
  • Pour supprimer un ligne: taper "dd". "D" permet de supprimer jusqu’à la fin du fichier.
  • Pour annuler les dernières modifications: taper "u".
  • Pour chercher dans le fichier: taper "/" suivi de la chaîne à chercher. En tapant "n" on peut passer à la chaine suivante.
  • Pour enregistrer les modifications: taper "w".
  • Pour quitter sans enregistrer: taper ":q" si le fichier n’a pas été modifié sinon ":q!".
  • Pour quitter en enregistrant: taper ":wq".
  • Pour faire un copier/coller: taper "Y" permet de copier une ligne dans le tampon. "nY" permet de copie "n" lignes. "P" colle les lignes avant le curseur et "p" colle les lignes après le curseur.

Sur linux-france, on peut avoir une liste plus complête des commandes de vi.

Afficher les modifications d’un fichier avant de commiter (git diff)

Lorsqu’on modifie un fichier versionné et que l’on souhaite vérifier les modifications apportées avant de le “stager” ou de le commiter, on peut utiliser git diff de cette façon:

git diff [chemin du fichier]

Avec la commande précédente, on affichera les modifications pour un fichier n’étant pas encore “stagé”.

On peut aussi comparer les modifications par rapport au HEAD du dépôt local:

git diff HEAD [chemin du fichier]

On peut comparer le contenu de l’index avec le dépôt local:

git diff --cache [chemin du fichier]

Au lieu d’afficher les modifications pour un seul fichier, on peut aussi le faire pour tous les fichiers qui se trouvent dans le répertoire de travail. La plupart du temps, l’affichage pour tous les fichiers n’est pas forcément très lisible. Pour afficher ces modifications, il suffit de ne pas préciser le chemin d’un fichier, par exemple:

git diff

Afficher l’historique (git log)

On peut afficher l’historique en utilisant git log:

$ git log 
[22a0ff3ccccdd9a94f5e0fb59b6307ebee0c8d98] 
Author: Robert Mitchoum 
Date:   Sat Feb 11 19:02:29 2017 +0100 
    2e commit 
 
[24d49ab46717545a0c0f9f94ee83ac0d60808d3f] 
Author: Robert Mitchoum 
Date:   Sat Feb 11 18:57:36 2017 +0100 
    1er commit

Les logs sont affichés avec la clé SHA-1 et le nom de la personne qui a commité.

Par exemple, si on commence avec un dépôt local à jour par rapport au dépôt distant, le graphe se présente de cette façon:

Après 2 commits, le graphe devient:

On peut afficher les 5 derniers commits avec:

$ git log -5

On peut afficher l’historique sous forme de graphe simple:

$ git log --graph --oneline 

On peut complêter l’historique de la branche en cours avec les autres branches:

$ git log --graph –oneline --all 

Envoyer ses modifications vers le dépôt distant

Pour envoyer des modifications effectuées sur le dépôt local vers le dépôt distant, il faut exécuter:

$ git push origin master

origin est le nom du dépôt distant. Il s’agit du nom par défaut généralement utilisé lorsqu’on a qu’un seul dépôt distant.
master est le nom de la branche sur laquelle on travaille. master est l’équivalent du trunk sous SVN.

Pour que Git accepte de pousser les modifications vers le dépôt distant, il faut que le dépôt local soit à jour (avec git pull par exemple).

A la fin de cette étape, les modifications sont partagées et d’autres développeurs peuvent y accéder.

Si le dépôt local ne provient pas d’une copie d’une dépôt distant (cf. Clone), on peut connecter son dépôt local au dépôt distant en faisant:

git remote add origin [nom du serveur]  

Si la branche est nouvelle, on peut indiquer que cette branche est la branche “upstream” c’est-à-dire la branche par défaut lorsqu’on précise pas de paramètres avec git push.
Pour indiquer que la branche est la branche “upstream”, on exécute:

git push --set-upstream origin [nom de la branche] 

Après il suffit d’exécuter sans argument la commande suivante pour pousser dans la bonne branche:

git push 

Dans le cas où le message suivant s’affiche quand on essaie de pousser:

fatal: The current branch master has no upstream branch. 
To push the current branch and set the remote as upstream, use 
   git push --set-upstream origin master

On est pas obligé de paramétrer la branche en tant que branche “upstream”. On peut aussi pousser en indiquant dans quelle branche:

git push origin master

Dans l’exemple précédent, si on pousse 2 modifications sur le dépôt distant, le graphe devient:

Branche

Les branches sous Git sont similaires aux branches dans les autres VCS. Toutefois il est courant et plus pratique d’utiliser des branches sous Git car:

  • Elles ne sont pas coûteuses, seules les modifications sont sauvegardées, il n’y a pas de copie complète de tous les fichiers du projet.
  • D’autres part, on peut facilement travailler sur une branche localement sans livrer son travail sur une branche dans le dépôt distant. Les branches locales permettent de travailler sur des sujets différents en même temps (par exemple corrections de bugs ou implémentations de nouvelles fonctionnalités) et de livrer sur la branche principale ou une autre branche sur le dépôt distant.
  • Enfin on peut facilement modifier ces commits sur sa branche locale, les supprimer ou en modifier les commentaires (avec commit amend ou rebase).

Créer une branche et passer directement dessus:

git checkout -b [nom de la branche]

Répercuter la création de la branche sur le dépôt distant (le cas échéant):

git push origin [nom de la branche]  

Passer sur une branche lorsqu’elle est déjà créée:

git checkout [nom de la branche]  

Créer une branche sans passer dessus:

git branch [nom de la branche]  

Lister les branches locales:

git branch --list  

Si on crée la branche “new_branch” sur le dépôt local, le graphe est:

Après 2 commits dans la branche “new_branch”, le graphe devient:

Si on retourne dans la branche principale (cf. master) et qu’on effectue 2 commits dans la branche principale:

Supprimer une branche

D’abord supprimer la branche localement en faisant:

git branch --delete [nom de la branche]  

Ensuite pour pousser la suppression sur le dépôt distant (le cas échéant):

git push origin :[nom de la branche]  

Renommer une branche

D’abord localement:

git branch -m [nom existant de la branche] [nouveau nom de la branche]  

Pousser le renommage sur le dépôt distant:

git push origin :[ancien nom de la branche]  

Effectuer des merges

Mettre à jour le dépôt local (git pull)

La façon la plus simple de rapatrier localement les modifications se trouvant sur le dépôt distant est d’exécuter:

git pull
PULL = FETCH + MERGE

Cette étape ne récupére pas seulement les modifications sur dépôt distant, elle effectue un merge des branches distantes avec les branches locales. Ces merges modifient les fichiers locaux et peuvent mener à des conflits. Avant de pouvoir commiter, il faudra résoudre ces conflits.

Une méthode plus progressive pour mettre à jour le dépôt local est d’effectuer:

git fetch

Cette commande récupére seulement les modifications distantes sans effectuer de merge.

Après un git fetch, on peut soit merger en exécutant git merge. On peut aussi utiliser git rebase qui une commande plus sure pour éviter les conflits.

Merger 2 branches (git merge)

Pour fusionner 2 branches, on peut les merger comme avec les autres VCS (ce n’est pas la seule méthode, on peut aussi utiliser rebase qui est une méthode provoquant moins d’erreurs et moins de conflits).

Pour merger 2 branches:
Il faut se placer sur la branche de destination (avec git checkout) puis exécuter:

git merge [nom de la branche à merger]

Des conflits peuvent résulter de ce merge, il faut les résoudre avant de commiter. Les conflits sont indiquées dans les fichiers avec des chevrons comme pour les autres VCS. Pour résoudre les conflits, il faut supprimer les chevrons avec un éditeur de texte en indiquant la partie du fichier à conserver.
Après résolution du conflit, on peut ajouter le fichier à l’index pour le commiter en faisant:

git add [nom du fichier]  

Dans l’exemple précédent, si on merge la branche principale (cf. master) avec la branche “new_branch”:

Tags

Les tags sont aussi très similaires aux tags des autres VCS. Ils permettent d’apporter un nom à un commit particulier. Il faut avoir en tête que tous les commits dans Git sont identifiés avec une clé SHA-1.

Il existe 2 types de tag:

  • Les tags légers (i.e. lightweight tag): ce sont des tags censés être provisoires. Ils ne contiennent qu’une clé SHA-1 vers un commit.
  • Les tags annotés (i.e. annotated tag): ils contiennent un SHA-1 comme les tags légers et aussi un objet qui est une copie de ce qui est taggué.

Tags légers

Créer un tag léger (i.e. lightweight tag):

git tag [nom du tag] [clé SHA-1 du commit à tagguer]

Pour tagguer le dernier commit:

git tag [nom du tag] -1

Pour “pousser” le tag créé vers le dépôt distant:

git push origin –tags

Supprimer un tag local:

git tag –d [nom du tag] 

Supprimer un tag distant (après l’avoir supprimé localement):

git push origin :refs/tags/[nom du tag]

Liste les tags existants:

git tag --list 

Récupérer un tag se trouvant sur le dépôt distant:

git checkout tags/[nom du tag] 

Tags annotés

Créer un tag annoté (i.e. Annotated tag):

git tag –a [nom du tag] -m [commentaire sur le tag] 

Pour “pousser” le tag annoté vers le dépôt distant:

git push origin [nom du tag] 

Voir le détail d’un tag:

git show [nom du tag] 

Pour récupérer un tag annoté se trouvant sur dépôt distant, la syntaxe est la même que pour les tags légers:

git checkout tags/[nom du tag]

Rebase

git rebase est une commande très puissante capable d’effectuer de nombreuses opérations sur le dépôt local:

  • Modifier des commits
  • Réunir 2 branches

Le grand intérêt de git rebase est de faire en sorte d’avoir un historique plus linéaire et d’éviter de laisser apparaître des commits inutils qui n’apportent pas d’informations.

Modifier des commits

Modifier des commits localement

git rebase peut être utilisé d’abord pour modifier facilement des commits dans son dépôt local. L’intérêt est de ne laisser apparaître que les commits utiles.

Par exemple, si on effectue un premier commit dans son dépôt local modifiant un fichier et qu’on se rende compte d’une erreur avant d’effectuer un git push. Les modifications sont encore dans le dépôt local. On corrige cette erreur et on effectue un nouveau commit. Sachant que les 2 modifications concernent la même fonctionnalité, il n’y a pas d’intérêt que les 2 commits apparaissent de façon distincte. Un seul commit suffit.

Ainsi, si on affiche l’historique d’un fichier en affichant les 3 derniers commits:

$ git log -3 
[e70d0a17f93e2609462db5593046ff1b2a7eb738] 
Date:   Sat Feb 11 19:05:05 2017 +0100 
    2e commit 
 
[2f222d1ad01a36ac02a8e9cc9359a81217644f69] 
Date:   Sat Feb 11 19:02:29 2017 +0100 
    1er commit 
 
[24d49ab46717545a0c0f9f94ee83ac0d60808d3f] 
Date:   Sat Feb 11 18:57:36 2017 +0100 
    Modification

Si on veut modifier les 2 derniers commits pour qu’ils n’apparaissent que sur la forme d’un seul commit:

$ git rebase -i HEAD~2 

Cette commande permet de modifier les 2 derniers commits par rapport à la référence (HEAD~2) en utilisant l’invite de commande interactive (avec "-i").
La commande ouvre une fenêtre vi qu’il est possible d’éditer pour écrire des instructions:

pick 2f222d1 Autre modif 
pick e70d0a1 Modif dans le master 
 
# Rebase 24d49ab..e70d0a1 onto 24d49ab (2 commands) 
# 
# Commands: 
# p, pick = use commit 
# r, reword = use commit, but edit the commit message 
# e, edit = use commit, but stop for amending 
# s, squash = use commit, but meld into previous commit 
# f, fixup = like "squash", but discard this commit's log message 
# x, exec = run command (the rest of the line) using shell 
# d, drop = remove commit 
# 
# These lines can be re-ordered; they are executed from top to bottom. 
# 
# If you remove a line here THAT COMMIT WILL BE LOST. 
# 
# However, if you remove everything, the rebase will be aborted. 
# 
# Note that empty commits are commented out
Ordre d’affichage inversé

Les commits sont indiqués du plus vieux au plus récent (l’ordre est inversé).

Les pick sont des commandes à effectuer sur les lignes de commit. D’autres instructions sont possibles:

  • pick: permet de garder le commit inchangé,
  • reword: permet d’indiquer qu’on souhaite modifier le commentaire du commit. Il n’est pas nécessaire de modifier le texte directement dans cette fenêtre. Il faut juste remplacer pick par reword. Après fermeture de la fenêtre vi, une autre fenêtre vi s’ouvrira pour modifier le commentaire.
  • squash: le commit sera regroupé avec le commit précédent. Il n’y aura pas de pertes dans le contenu des fichiers. Au lieu d’avoir 2 commits, il n’en restera qu’un seul. Il n’est pas nécessaire de modifier le texte directement dans cette fenêtre. Il faut juste remplacer pick par squash. Après fermeture de la fenêtre vi, une autre fenêtre vi s’ouvrira pour modifier le commentaire.
  • fixup: même modification que squash sans la prise en compte du commentaire.
  • drop: supprime le commit. Les modifications effectuées par ce commit seront perdues.
  • edit: cette commande permet d’afficher par la suite une invite de commande qui sera appliquée seulement au commit pour lequel on a indiqué edit.

Pour réunir le 2e commit au 1er, on remplace pick par squash. On passe en mode édition dans vi en tapant "i", on replace les termes et on enregistre en tapant ":wq":

pick 2f222d1 1er commit 
squash e70d0a1 2e commit 
 
# Rebase 24d49ab..e70d0a1 onto 24d49ab (2 commands) 
# 
# ...

Un écran suivant propose de modifier le commentaire puis:

[detached HEAD 22a0ff3] 1er commit 
 Date: Sat Feb 11 19:02:29 2017 +0100 
 1 file changed, 2 insertions(+) 
Successfully rebased and updated refs/heads/master.

Si on affiche l’historique:

$ git log -2 
Seulement 2 lignes apparaissent: 
[22a0ff3ccccdd9a94f5e0fb59b6307ebee0c8d98] 
Date:   Sat Feb 11 19:02:29 2017 +0100 
    Autre modif 
    Modif dans le master 
 
[24d49ab46717545a0c0f9f94ee83ac0d60808d3f] 
Date:   Sat Feb 11 18:57:36 2017 +0100 
    Commentaire de test.

Changer l’histoire dans le dépôt distant

Si on modifie l’histoire en utilisant git rebase, on peut pousser les modifications sur le dépôt distant en forçant avec "-f":

$ git push -f

Cette étape peut compliquer les merges futures pour les autres développeurs puisqu’on peut changer l’historique du dépôt distant.

Réunir 2 branches

Branche ayant un nœud commun avec le master

Le terme “réunir” est utilisé içi à la place de merger pour éviter confusion avec git merge. git rebase est complêtement différent de git merge:

  • git merge réunit 2 branches en mergeant les fichiers. Dans l’historique, la branche principale (i.e. branche “master”) et la branche mergée dans la branche principale sont visibles (cf. git merge).
  • git rebase ne merge pas 2 branches mais rejoue les commits de la branche à la suite du dernier commit du “master”. Git cherche alors, le nœud commun entre la branche et le master puis il rejoue les commits à la suite du master à partir de ce nœud. L’historique apparaît linéaire puisque les commits sont simplement ajoutés à ceux du master.

Si on considère le master avec une branche appelée “featureBranch”, on a un graphe de ce type après quelques commits:

Dans un premier temps, on se synchroniser par rapport à la branche master et on rejoue les commits à la fin du master:

$ git checkout featureBranch 
$ git rebase master

Le graphe devient:

Ensuite, on se place dans la branche master:

$ git checkout master

Le graphe est alors:

On merge la branche “featureBranch” dans le master qui ne va rajouter que les nouveaux commits:

$ git merge featureBranch 

Le graphe devient:

Comme on peut le voir, l’historique de la branche “master” reste linéaire et les commits sont ajoutés à la fin des commits du master.
Pour réunir 2 branches, il est préférable d’utiliser git rebase par rapport à git merge pour minimiser les conflits et avoir un historique plus clair.

Branche séparée du master

Dans le cas où la branche est complêtement séparée du master c’est-à-dire qu’il n’y a pas des nœuds commun avec le master. L’utilisation de git rebase est un peu différente car il ne pourra pas trouver de nœud commun avec le master.

Par exemple, si on considère les branches suivantes:

La branche “otherBranch” n’a pas de nœud commun avec le master. “otherBranch” a un nœud commun “featureBranch”.

Pour effectuer un “rebase” sur le master, il faut rajouter l’argument --onto:

 $ git rebase –onto master featureBranch otherBranch 

Le graphe devient:

Il suffit ensuite de merger la branche “otherBranch”:

$ git checkout master

Le graphe est:

Enfin après le merge:

$ git merge otherBranch

Le graphe après le merge:

Si on supprime la branch “featureBranch”, l’historique de la branche “master” est linéaire:

$ git branch –d featureBranch

Le graphe après suppresion de la branche “featureBranch”:

Annuler des modifications

Il est possible d’annuler la plupart des opérations effectuées avec Git.

Avant d’effectuer un commit

Si un fichier a été modifié et qu’on ne désire pas le commiter, on peut annuler les modifications en exécutant:

git checkout -- [chemin du fichier]
Les modifications seront perdues

Après cette étape, les modifications effectuées sur le fichier seront perdues.

On peut aussi ramener un fichierà un commit précédent:

git checkout [SHA-1 du commit] -- [chemin du fichier]

Pour supprimer tous les fichiers qui ne sont pas trackés (i.e. fichiers non versionnés):

git clean -f

Pour annuler, supprimer tous les changements locaux et récupérer les modifications effectuées sur le dépôt distant:

git fetch origin 
git reset --hard origin/master 

Modifier le dernier commit

Il est possible de modifier facilement le dernier commit effectué même dans le cas il a été poussé vers le dépôt distant avec la commande git commit --amend.

Par exemple pour modifier pour le commentaire utilisé pour le commit:

git commit --amend –m [nouveau commentaire du commit]

On peut pousser la modification vers le dépôt distant en faisant:

git push origin master –f 

On peut aussi effectuer cette étape en utilisant git rebase.

On peut utiliser plus généralement git commit –amend par exemple pour rajouter un fichier au dernier commit:

$ git commit -m [commentaire du premier commit] 
$ git add [fichier à rajouter au commit] 
 
$ git commit --amend

La dernière ligne va commiter l’ajout du fichier.

Annuler un commit

On peut annuler un commit, en replaçant le HEAD sur un commit particulier en utilisant la clé SHA-1 de ce commit:

git reset [label ou SHA-1 du commit]

Pour pousser, il faut ajouter l’argument “-f” pour forcer:

git push –f 

Il est préférable que personne n’ait récupéré le dépôt distant avant d’avoir effectué cette étape. Si un développeur a effectué un git pull entre le dernier commit et le git push, il travaillera avec un HEAD qui n’est plus le HEAD du dépôt distant.

Cherry-pick

git cherry-pick est une commande utile pour récupérer un commit effectué sur une autre branche complêtement séparée de la branche courante. Elle permet d’appliquer les modifications correspondant à un seul commit sans effectuer de merge. On utilise la clé SHA-1 pour identifier ce commit et on l’applique sur la branche courante. Cette opération peut mener le cas échéant à des conflits.

Pour utiliser git cherry-pick, il suffit de:

  • se placer dans la branche sur laquelle on veut appliquer le commit,
  • il faut avoir le SHA-1 du commit

On exécute ensuite:

$ git cherry-pick [SHA-1 du commit]

Il n’est pas nécessaire d’effectuer un commit après cette étape, git cherry-pick effectue lui-même un commit.

Stash

git stash permet de mettre de coté des fichiers lorsqu’ils sont modifiés mais qu’ils ne sont pas placés dans l’index et qu’ils ne sont pas encore commités. Dans une branche donnée, on va donc mettre ces fichiers de coté de façon à effectuer d’autres modifications. On pourra ensuite récupérer les fichiers mis de coté. L’historique n’est pas affecté lorsqu’on utilise git stash. D’autre part, cette opération s’effectue seulement sur le dépôt local.

Si le dépôt local comprends des fichiers modifiés:

$ git status 
On branch master 
Your branch is up-to-date with 'origin/master'. 
Changes not staged for commit: 
  (use "git add <file>..." to update what will be committed) 
  (use "git checkout -- <file>..." to discard changes in working directory) 
 
modified:   test.txt 
 
no changes added to commit (use "git add" and/or "git commit -a")

Si on met de coté les modifications en “stashant”:

$ git stash 
Saved working directory and index state WIP on master: e9bc55e Merge branch 'master' of https://github.com/repository/test 
HEAD is now at e9bc55e Merge branch 'master' of https://github.com/repository/test 

Le répertoire ne contient plus de modifications:

$ git status 
On branch master 
Your branch is up-to-date with 'origin/master'. 
nothing to commit, working tree clean 

On peut voir la liste des stash en exécutant:

$ git stash list 
 stash@{0}: WIP on master: e9bc55e Merge branch 'master' of https://github.com/repository/test 

Le nom du stash est “stash@{0}”.

On peut appliquer le premier stash sans le supprimer de la liste en exécutant:

$ git stash apply

Pour appliquer un stash particulier sans le supprimer de la liste:

$ git stash apply [nom du stash] 

Pour appliquer le premier stash en le supprimant de la liste, on peut exécuter:

$ git stash pop 

De même pour appliquer un stash particulier en le supprimant de la liste:

$ git stash pop [nom du stash]
Références

Pour s’exercer:

Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInEmail this to someonePrint this page

“Mocker” une dépendance statique

Les objets statiques sont souvent utilisés pour mutualiser rapidement l’implémentation d’un comportement ou plus rarement pour partager des instances d’objets entre plusieurs classes.
L’utilisation d’objets statiques peut être un choix d’architecture maitrisé. Dans ce cas, on a la possibilité de modifier l’implémentation des objets statiques ainsi que des objets consommateurs.

Dans d’autres cas, l’utilisation d’objets statiques peut être imposée par l’implémentation d’une bibliothèque par exemple. C’est le plus souvent dans ce cas qu’il est plus difficile de mocker les objets statiques pour tester l’implémentation des classes consommatrices.

Cet article présente quelques méthodes pour mocker des objets statiques:

  • Dans le cas où l’implémentation de l’objet statique est maitrisé: en injectant une dépendance,
  • Dans le cas où l’implémentaiton est contrainte.

On considère l’exemple suivant permettant de calculer l’âge en fonction de la date actuelle:

public class AgeCalculator 
{ 
    public int GetAge(DateTime dateOfBirth) 
    { 
        DateTime now = DateTime.Now; 
        if (dateOfBirth > now) 
            throw new InvalidOperationException(
               "Date of birth shall be before current date."); 
     
        return now.Year - dateOfBirth.Year; 
    } 
}

Cette implémentation très simpliste ne prends pas en compte les mois et donc peut s’avèrer faux toutefois si on souhaite tester AgeCalculator.GetAge(), on pourrait écrire le test suivant:

[Test] 
public void Age_Shall_Be_14_When_DateOfBirth_Is_In_2002() 
{ 
    DateTime fixedDateOfBirth = new DateTime(2002, 06, 06); 
 
    var ageCalculator = new AgeCalculator(); 
    int age = ageCalculator.GetAge(fixedDateOfBirth); 
     
    Assert.AreEqual(14, age); 
}

Ce test fonctionnera jusqu’au 1 janvier 2017. A partir de cette date, il sera faux. On peut donc modifier le test pour être moins dépendant de la date en cours.

[Test] 
public void Age_Shall_Be_14_When_DateOfBirth_Is_14_Years_Ago() 
{ 
    DateTime now = DateTime.Now; 
    DateTime dateOfBirth = new DateTime(now.Year - 14, now.Month, now.Day); 
 
    var ageCalculator = new AgeCalculator(); 
    int age = ageCalculator.GetAge(dateOfBirth); 
 
    Assert.AreEqual(14, age); 
}

Ce test fonctionnera tout le temps mais peut échouer à certaines occasions. Par exemple, si le test est exécuté le 31 décembre 2016 à 23h59min59sec999ms. La date de naissance 14 ans plus tôt sera le 31 décembre 2002 à 23h59min59sec999ms. Toutefois pendant l’exécution de AgeCalculator.GetAge(), l’horloge continue d’avancer et au moment d’exécuter System.DateTime.Now dans la fonction AgeCalculator.GetAge(), la date actuelle devient: 1er janvier 2017 à 0h00min00sec001ms. L’âge calculé devient 15 et le test échoue.

Pour que le test réussisse toujours et ne soit plus dépendant de la date en cours, il faudrait pouvoir maitriser la date renvoyée par la propriété statique DateTime.Now. Or DateTime est une classe du framework.

Injecter une dépendance

La première solution consiste à injecter une dépendance dans la classe consommatrice plutôt que de permettre d’utiliser directement la propriété statique. L’injection de la dépendance permettra, ainsi, de casser la dépendance vers la propriété statique.

L’implémentation de AgeCalculator devient:

public class AgeCalculator 
{ 
    private ICurrentDateHandler currentDateHandler; 
 
    public AgeCalculator(ICurrentDateHandler currentDateHandler) 
    { 
        this.currentDateHandler = currentDateHandler; 
    } 
 
    public int GetAge(DateTime dateOfBirth) 
    { 
        DateTime now = this.currentDateHandler.GetCurrentDate(); 
        if (dateOfBirth > now) 
            throw new InvalidOperationException(
               "Date of birth shall be before current date."); 
     
        return now.Year - dateOfBirth.Year; 
    } 
}

Avec:

public interface ICurrentDateHandler 
{ 
    DateTime GetCurrentDate(); 
}

Et:

public class CurrentDateHandler : ICurrentDateHandler 
{ 
    public DateTime GetCurrentDate() 
    { 
        return DateTime.Now; 
    } 
}

On casse le couplage entre AgeCalculator et System.DateTime en déportant la dépendance à System.DateTime dans une autre classe:
Cette implémentation est plus en accord avec le principe SOLID (http://cdiese.fr/principe-de-developpement-oriente-objet-solid/) puisqu’elle sépare les responsabilités.
Elle facilite les tests unitaires puisqu’on peut maintenant maitrisé la date actuelle fournie à AgeCalculator.

On peut facilement mocker ICurrentDateHandler, par exemple, avec Moq:

using Moq; 
// ...

DateTime expectedCurrentDate = new DateTime(2016, 6, 6); 
 
Mock<ICurrentDateHandler> currentDateHandlerMock = new Mock<ICurrentDateHandler>(); 
currentDateHandlerMock.SetUp(h => h.GetCurrentDate()).Returns(expectedCurrentDate);

Le test devient:

[Test]  
public void Age_Shall_Be_14_When_DateOfBirth_Is_14_Years_Ago()  
{  
    DateTime expectedCurrentDate = new DateTime(2016, 6, 6); 
 
    Mock<ICurrentDateHandler> currentDateHandlerMock = new Mock<ICurrentDateHandler>(); 
    currentDateHandlerMock.SetUp(h => h.GetCurrentDate()).Returns(expectedCurrentDate); 
 
    DateTime dateOfBirth = new DateTime(expectedCurrentDate.Year - 14,  
        expectedCurrentDate.Month, expectedCurrentDate.Day);  
  
    var ageCalculator = new AgeCalculator(currentDateHandlerMock.Object);  
    int age = ageCalculator.GetAge(dateOfBirth);  
  
    Assert.AreEqual(14, age);  
}

Le test ne dépend plus de la date actuelle et réussit toujours.

Utiliser une Factory

L’inconvénient de l’exemple précédent est que l’injection de ICurrentDateHandler se fait dans le constructeur de AgeCalculator ce qui contraint l’extensibilité de la classe AgeCalculator.

Si on doit créer un objet ICurrentDateHandler suivant certains critères connus au moment de l’exécution de AgeCalculator.GetAge(), on peut passer par l’intermédiaire d’une Factory. Cette Factory est injectée par le constructeur toutefois, elle permet de créer l’objet ICurrentDateHandler suivant une logique implémentée directement dans la Factory. La création peut aussi être exécutée au moment de l’appel à AgeCalculator.GetAge() et non à la construction de AgeCalculator.

Par exemple, si on doit introduire une notion de fuseaux horaires sans modifier la signature de AgeCalculator.GetAge(), on peut le faire par l’intermédiaire de la Factory:

public class DateHandlerFactory : IDateHandlerFactory 
{ 
    private TimeZoneInfo sourceTimeZone; 
    private TimeZoneInfo destinationTimeZone; 
 
    public CurrentDateHandlerFactory(TimeZoneInfo sourceTimeZone,  
        TimeZoneInfo destinationTimeZone) 
    { 
         this.sourceTimeZone = sourceTimeZone; 
         this.destinationTimeZone = destinationTimeZone; 
    } 
 
    public ICurrentDateHandler GetCurrentDateHandler() 
    { 
        return new CurrentDateHandler(this.sourceTimeZone, this.destinationTimeZone); 
    } 
}

Avec:

public interface IDateHandlerFactory 
{ 
    ICurrentDateHandler GetCurrentDateHandler(); 
}

urrentDateHandler devient:

public class CurrentDateHandler : ICurrentDateHandler 
{ 
    private TimeZoneInfo sourceTimeZone; 
    private TimeZoneInfo destinationTimeZone; 
 
    public CurrentDateHandlerFactory(TimeZoneInfo sourceTimeZone,  
        TimeZoneInfo destinationTimeZone) 
    { 
         this.sourceTimeZone = sourceTimeZone; 
         this.destinationTimeZone = destinationTimeZone; 
    } 
 
    public DateTime GetCurrentDate() 
    { 
        return TimeZoneInfo.ConvertTime(DateTime.Now, this.sourceTimeZone,  
            this.destinationTime); 
    } 
}

De même AgeCalculator prend en compte la Factory toutefois son implémentation ne contient aucune modifications relatives au fuseau horaire:

public class AgeCalculator 
{ 
    private IDateHandlerFactory dateHandlerFactory; 
 
    public AgeCalculator(IDateHandlerFactory dateHandlerFactory)  
    { 
        this.dateHandlerFactory = dateHandlerFactory; 
    } 
 
    public int GetAge(DateTime dateOfBirth) 
    { 
        var currentDateHandler = this.dateHandlerFactory.GetCurrentDateHandler(); 
 
        DateTime now = currentDateHandler.GetCurrentDate(); 
        if (dateOfBirth > now) 
            throw new InvalidOperationException(
                "Date of birth shall be before current date."); 
     
        return now.Year - dateOfBirth.Year; 
    } 
}

On peut toujours mocker ICurrentDateHandler ainsi que la Factory IDateHandlerFactory pour maitriser la date actuelle dans le test:

[Test]   
public void Age_Shall_Be_14_When_DateOfBirth_Is_14_Years_Ago()   
{   
    DateTime expectedCurrentDate = new DateTime(2016, 6, 6);  
  
    Mock<ICurrentDateHandler> currentDateHandlerMock = new Mock<ICurrentDateHandler>();  
    currentDateHandlerMock.SetUp(h => h.GetCurrentDate()).Returns(expectedCurrentDate);  
 
    Mock<IDateHandlerFactory> dateHandlerFactoryMock = new Mock<IDateHandlerFactory>();  
    dateHandlerFactoryMock.SetUp(f => f.GetCurrentDateHandler()) 
        .Returns(currentDateHandlerMock.Object); 
  
    DateTime dateOfBirth = new DateTime(expectedCurrentDate.Year - 14,   
        expectedCurrentDate.Month, expectedCurrentDate.Day);   
   
    var ageCalculator = new AgeCalculator(dateHandlerFactoryMock.Object);   
    int age = ageCalculator.GetAge(dateOfBirth);   
   
    Assert.AreEqual(14, age);   
}

Utiliser une classe statique Proxy

L’inconvénient majeur des méthodes précédentes est qu’elles imposent de modifier en profondeur la classe AgeCalculator car:

  • Il faut modifier le constructeur pour permettre l’injection de la dépendance,
  • Il faut modifier la fonction AgeCalculator.GetAge() pour qu’elle utilise ICurrentDateHandler.GetCurrentDate() plutôt que DateTime.Now.

On pourrait explorer d’autres méthodes pour injecter la dépendance:

  • Injecter la dépendance par appel d’une méthode pour éviter la modification du constructeur, par exemple SetCurrentDateHandler():
    public class AgeCalculator 
    { 
        private ICurrentDateHandler currentDateHandler; 
     
        public AgeCalculator() {} 
     
        public void SetCurrentDateHandler(ICurrentDateHandler currentDateHandler) 
        { 
            this.currentDateHandler = currentDateHandler; 
        } 
     
        // ... 
    }
    
  • Injection par un accesseur:
    public class AgeCalculator 
    { 
        public AgeCalculator() {} 
     
        public ICurrentDateHandler CurrentDateHandler { get; set; }  
     
        public int GetAge(DateTime dateOfBirth) 
        { 
            DateTime now = this.CurrentDateHandler.GetCurrentDate(); 
            if (dateOfBirth > now) 
                throw new InvalidOperationException(
                    "Date of birth shall be before current date."); 
         
            return now.Year - dateOfBirth.Year; 
        } 
    }
    
  • Injecter directement dans la fonction AgeCalculator.GetAge():
    public class AgeCalculator 
    { 
        public AgeCalculator() {} 
     
        public int GetAge(ICurrentDateHandler currentDateHandler, DateTime dateOfBirth) 
        { 
            DateTime now = currentDateHandler.GetCurrentDate(); 
            if (dateOfBirth > now) 
                throw new InvalidOperationException(
                    "Date of birth shall be before current date."); 
         
            return now.Year - dateOfBirth.Year; 
        } 
    }
    

Ces solutions sont très mauvaises car elles imposent que l’appelant de AgeCalculator.GetAge() crée une instance d’un objet ICurrentDateHandler et qu’il l’injecte au bon moment dans AgeCalculator. On va donc explorer une autre possibilité qui consiste à continuer à utiliser une classe statique dans la fonction AgeCalculator.GetAge().

L’intérêt de cette approche est:

  • Elle ne modifie pas le constructeur de AgeCalculator,
  • Elle ne nécessite pas une injection de ICurrentDateHandler,
  • La modification n’implique que la fonction AgeCalculator.GetAge().

Ainsi si on considère la classe suivante:

public static class DateHandler 
{ 
    public static Func<DateTime> Now = () => DateTime.Now; 
}

On peut l’utiliser directement dans AgeCalculator sans injection:

public class AgeCalculator 
{ 
    public AgeCalculator() {} 
 
    public int GetAge(DateTime dateOfBirth) 
    { 
        DateTime now = DateHandler.Now; 
        if (dateOfBirth > now) 
            throw new InvalidOperationException(
                "Date of birth shall be before current date."); 
     
        return now.Year - dateOfBirth.Year; 
    } 
}

On peut toujours maitriser la date actuelle dans les tests. Notre test devient:

[Test]  
public void Age_Shall_Be_14_When_DateOfBirth_Is_14_Years_Ago()  
{  
    DateTime expectedCurrentDate = new DateTime(2016, 6, 6); 
    DateHandler.Now = () => expectedCurrentDate; 
 
    DateTime dateOfBirth = new DateTime(expectedCurrentDate.Year - 14,  
        expectedCurrentDate.Month, expectedCurrentDate.Day);  
  
    var ageCalculator = new AgeCalculator();  
    int age = ageCalculator.GetAge(dateOfBirth);  
  
    Assert.AreEqual(14, age);  
}

Le constructeur de AgeCalculator et la signature de AgeCalculator.GetAge() ne sont pas modifiés.

Smocks

Une dernière possibilité consiste à ne pas modifier du tout la classe AgeCalculator et continuer à utiliser la propriété statique DateTime.Now. On peut contrôler la valeur de DateTime.Now dans les tests en utilisant des bibliothèques comme Smocks.

Le gros intérêt de ce type de bibliothèque est de modifier directement les valeurs renvoyées par une propriété ou des fonctions statiques, par exemple pour le test précédent:

[Test]  
public void Age_Shall_Be_14_When_DateOfBirth_Is_14_Years_Ago()  
{  
    Smock.Run(context => 
    { 
        DateTime expectedCurrentDate = new DateTime(2016, 6, 6); 
 
        context.Setup(() => DateTime.Now).Returns(expectedCurrentDate); 
 
       DateTime dateOfBirth = new DateTime(expectedCurrentDate.Year - 14,  
            expectedCurrentDate.Month, expectedCurrentDate.Day);  
  
        var ageCalculator = new AgeCalculator();  
        int age = ageCalculator.GetAge(dateOfBirth);  
  
        Assert.AreEqual(14, age); 
     }); 
}

Le code de AgeCalculator n’est pas modifié:

public class AgeCalculator 
{ 
    public AgeCalculator() {} 
 
    public int GetAge(ICurrentDateHandler currentDateHandler, DateTime dateOfBirth) 
    { 
        DateTime now = DateTime.Now; 
        if (dateOfBirth > now) 
            throw new InvalidOperationException(
                "Date of birth shall be before current date."); 
     
        return now.Year - dateOfBirth.Year; 
    } 
}

Smocks est disponible sur Nuget: https://www.nuget.org/packages/Smocks/.

Microsoft Fakes

Une autre alternative est d’utiliser Microsoft Fakes.
L’inconvénient de Microsoft Fakes est qu’il n’est disponible que dans Visual Studio Enterprise ce qui en fait un produit cher.
Le projet d’origine Moles était gratuit toutefois depuis Visual Studio 2010, il n’a pas évolué au profit de Microsoft Fakes.

Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInEmail this to someonePrint this page

Télécharger le contenu d’un répertoire listé par Apache

Pour télécharger le contenu d’un répertoire sur un serveur Apache, on peut s’aider d’un outil provenant d’Unix: wget. Les gros intérêts de cet outil sont:

  • Son installation est rapide,
  • On peut l’utiliser à partir d’une ligne de commandes, dans un script batch ou dans un script powershell,
  • wget dipose de beaucoup d’options, par exemple, pour parcourir récursivement un répertoire HTTP

A l’origine wget est un outil Unix mais il existe une implémentation pour Windows téléchargeable sur: http://gnuwin32.sourceforge.net/packages/wget.htm.

Pour télécharger un répertoire HTTP récursivement

Par exemple, si le répertoire est http://mysite.org/internal_directory/subfolder/, on peut écrire à la ligne de commandes:

wget -r -np http://mysite.org/internal_directory/subfolder/

Les options sont:

  • -r: permet de parcourir récursivement tous les répertoires, y compris les répertoires situés au-dessus du répertoire subfolder comme, par exemple, internal_directory.
  • -np: permet d’empêcher le parcours des répertoires au-dessus du répertoire subfolder. Le parcours sera donc limité au répertoire subfolder et à ses sous-répertoires.

D’autres options peuvent être utiles comme:

  • -nH: permet de ne pas préfixer le répertoire de destination avec le host name du site http://mysite.org/
  • -cut-dirs: cette option permet d’éviter de ranger le contenu des répertoires dans les mêmes répertoires de destination. Ainsi --cut-dirs=1 rangera le contenu de http://mysite.org/internal_directory/subfolder/ dans mysite.org/subfolder/. Par suite, --cut-dirs=2 rangera le contenu dans mysite.org/.
  • -R: permet d’excluse des fichiers suivant leur nom. -R index.html évitera de récupérer les fichiers index.html; -R html permettra d’éviter de récupérer tous les fichiers html.
  • -A: permet de récupérer seulement les fichiers avec un nom particulier. -A html permet de ne récupérer que les fichiers html.

Toute la documentation se trouve sur: http://www.gnu.org/software/wget/manual/wget.html.

VisualWget

Sur Windows, on peut s’aider d’une interface graphique pour utiliser wget: VisualWget.

Références
Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInEmail this to someonePrint this page

Trouver les processus utilisant un fichier ou un répertoire

Assez souvent on peut être confronter à une erreur très désagréable lorsqu’on veut supprimer un répertoire ou fichier qui est utilisé par un processus:

"Cette action ne peut pas être réalisée car le fichier est ouvert
    dans un autre programme"

ou

"The action can't be completed because the file is open in another program"

Ce message est souvent déconcertant car il n’indique pas quel est le programme qui utilise le fichier.

Pour connaitre les processus coupables, on peut s’aider de ProcessExplorer (cet outil peut être téléchargé sur: https://technet.microsoft.com/en-us/sysinternals/bb896653.aspx):

  1. Dans ProcessExplorer, cliquer sur “Find”,
  2. Cliquer sur “Find Handler or DLL”
  3. Dans “Handle or DLL substring”, il faut indiquer le chemin absolu du fichier ou du répertoire (par exemple: C:\MyDirecitory\MyFile.txt),
  4. Cliquer sur “Search”
  5. La liste des processus sera affichée, il suffit d’arrêter l’exécution de ces processus pour qu’il soit possible de supprimer le fichier ou le répertoire.
Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInEmail this to someonePrint this page

Quelques outils pour résoudre les problèmes de chargement d’assemblies

Dans le cas de BadImageFormatException, parmi toutes les dépendances d’un exécutable, il est parfois difficile d’identifier l’assembly dont l’architecture cible est incompatible avec celle de l’exécutable. Certains outils permettent d’avoir plus d’informations sur les dépendances d’une assembly et de visualiser le détail de chargement des assemblies pour un exécutable donné.

Fusion

“Fusion” est le nom de code de l’élément qui charge les dépendances d’une assembly en .NET. Lorsque le code contenu dans une assembly nécessite le chargement d’une autre assembly, à partir du nom et de la version requise, “Fusion” va chercher cette assembly dans différent répertoire: dans le répertoire de l’assembly d’origine puis dans le GAC…
L’intérêt de “Fusion” est qu’il loggue les assemblies qu’il recherche ainsi que les différents répertoires dans lesquels il a effectué cette recherche. En cas de problème lors du chargement d’une assembly, on peut arriver à en connaître la cause.

Activer les logs de “Fusion”

D’abord, il faut activer les logs de “Fusion” en modifiant certaines valeurs dans la base de registre (regedit.exe):

  1. Affecter la valeur DWORD “1” à la clé HKLM\Software\Microsoft\Fusion\ForceLog
  2. Affecter le chemin d’un répertoire à la valeur de .HKLM\Software\Microsoft\Fusion\LogPath de façon à indiquer l’endroit où les logs seront générés. Par exemple: C:\FusionLog\.
  3. Affecter la valeur DWORD “1” à la clé HKLM\Software\Microsoft\Fusion\LogFailures (Facultatif)
  4. Affecter la valeur DWORD “1” à la clé HKLM\Software\Microsoft\Fusion\LogResourceBinds (Facultatif)

Visionner les logs

Pour visionner les logs, on peut s’aider de fuslogvw.exe (fusion log viewer). Cet outil est livré avec le SDK Windows et est accessible en utilisant la ligne de commandes Visual Studio. Le chemin de l’exécutable est, par exemple pour Windows 8.1 avec le framework 4.5.1:

C:\Program Files (x86)\Microsoft SDKs\Windows\v8.1a\bin\NETFX 4.5.1 Tools\fuslogvw.exe

En regardant les logs, on peut avoir une idée de l’assembly qui a été la source de l’erreur et la raison de l’échec du chargement. Par exemple:

The operation failed.
Bind result: hr = 0x80070002. The system cannot find the file specified.

Assembly manager loaded from:  C:\WINDOWS\Microsoft.NET\Framework\v2.0.60425\mscorwks.dll
Running under executable  C:\Program Files\Microsoft Visual Studio 8\Team Tools\Performance Tools\XXXXviewer.exe
— A detailed error log follows.

=== Pre-bind state information ===
LOG: User = REDMOND\XXXX
LOG: DisplayName = XXXXvisualization, Version=8.0.0.0, Culture=neutral, PublicKeyToken=XXXXXXXXXXXX
 (Fully-specified)
LOG: Appbase = file:///C:/Program Files/Microsoft Visual Studio 8/Team Tools/Performance Tools/
LOG: Initial PrivatePath = NULL
LOG: Dynamic Base = NULL
LOG: Cache Base = NULL
LOG: AppName = XXXXviewer.exe
Calling assembly : XXXXviewer, Version=8.0.0.0, Culture=neutral, PublicKeyToken=XXXXXXXXXXXX
===
LOG: This bind starts in default load context.
LOG: No application configuration file found.
LOG: Using machine configuration file from C:\WINDOWS\Microsoft.NET\Framework\v2.0.60425\config\machine.config.
LOG: Post-policy reference: XXXXvisualization, Version=8.0.0.0, Culture=neutral, PublicKeyToken=XXXXXXXXXX
LOG: GAC Lookup was unsuccessful.
LOG: Attempting download of new URL file:///C:/Program Files/Microsoft Visual Studio 8/Team Tools/Performance Tools/XXXXvisualization.DLL.
LOG: Attempting download of new URL file:///C:/Program Files/Microsoft Visual Studio 8/Team Tools/Performance Tools/XXXXvisualization/XXXXvisualization.DLL.
LOG: Attempting download of new URL file:///C:/Program Files/Microsoft Visual Studio 8/Team Tools/Performance Tools/XXXXvisualization.EXE
LOG: Attempting download of new URL file:///C:/Program Files/Microsoft Visual Studio 8/Team Tools/Performance Tools/XXXXvisualization/XXXXvisualization.EXE.
LOG: All probing URLs attempted and failed.

Ici le chargement a échoué car l’assembly n’a pas été trouvée.

Plus d’informations à propos de fuslogvw.exe sur MSDN.

Ne pas oublier de désactiver les traces après debug

L’écriture des logs détaillés se fait au prix d’une petite baisse de performance. D’autre part, ces logs peuvent occuper beaucoup d’espace disque si on ne les supprime pas régulièrement.

DependancyWalker

DependancyWalker” est un utilitaire qui permet de voir toutes les dépendances d’une assembly, on peut ainsi vérifier que les dépendances existent et sont de la bonne version ou architecture.

“DependancyWalker” peut être téléchargé sur www.dependencywalker.com/.

Identifier les dépendances par programmation

On peut lister les dépendances d’une assembly de la façon suivante:

using System.Reflection;
...
public void DisplayReferencedAssemblies(string assemblyPath)
{
  Assembly assembly = Assembly.LoadFrom(assemblyPath);
  AssemblyName[] referencedAssemblyNames = assembly.GetReferencedAssemblies();
  foreach (var referencedAssemblyName in referencedAssemblyNames)
  {
    Console.WriteLine("Assembly name: {0}, Version={1}", 
      referencedAssemblyName.FullName,
      referencedAssemblyName.Version);
  }
}
Dépendances dans un contexte managé

Cette méthode permet de référencer les dépendances dans un contexte purement managé. Si il y a des dépendances natives ne seront pas listées.

Chargement d’un assembly

Quand on écrit Assembly.LoadFrom(assemblyPath), on charge l’assembly dans le domaine d’application courant. Cette ligne ne peut s’exécuter que si l’assembly à charger est compatible avec l’architecture de l’exécutable:

  • “x86″ ou “AnyCPU” si l’exécutable est exécuté en 32 bits,
  • “x64″ ou “AnyCPU” si l’exécutable est exécuté en 64 bits.

Pour afficher la version du framework

Pour obtenir la version du framework, on peut exécuter le code suivant:

Assembly assembly = Assembly.LoadFrom(assemblyPath);
object[] attributes = assembly.GetCustomAttributes(true);
Type targetFrameworkAttributeType = 
    typeof(System.Runtime.Versioning.TargetFrameworkAttribute);
System.Runtime.Versioning.TargetFrameworkAttribute fwkAttribute = 
    (System.Runtime.Versioning.TargetFrameworkAttribute)attributes
        .FirstOrDefault(p => p.GetType() == targetFrameworkAttributeType);

// Pour afficher la version sous la forme ".NETFramework,Version=vX.X"
Console.WriteLine(fwkAttribute.FrameworkName);

// Pour afficher la version sous la forme ".NET Framework X.X"
Console.WriteLine(fwkAttribute.FrameworkDisplayName);

Pour afficher la version du CLR d’une assembly

Assembly assembly = Assembly.LoadFrom(assemblyPath);
Console.WriteLine(assembly.ImageRuntimeVersion);
Références
Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInEmail this to someonePrint this page

Routed events en WPF en 3 min

Les évènements routés WPF (i.e. routed events) sont très similaires aux évènements classiques en .NET:

  • Ils sont définis dans un objet,
  • Peuvent être déclenchés par un objet différent du premier et
  • Conduisent à l’exécution d’une callback ou plusieurs callbacks définies dans une 3e série d’objets.

La grande différence entre les évènements routés WPF et les évènements .NET est l’aspect “routé” puisqu’ils suivent l’arbre des éléments WPF.

Caractéristiques des évènements routés

Comme indiqué plus haut, la caractéristique la plus importante des évènements routés est qu’ils suivent l’arbre des éléments. Au runtime, l’interface graphique d’une application WPF est composée de plusieurs éléments visuels qui sont organisés hiérarchiquement en une structure en arbre.

On distingue 2 types d’arbres:

  • L’arbre logique qui définit les relations entre les éléments implémentés par le développeur
  • L’arbre visuel définissant les relations entre les éléments au runtime de l’application.

Pour plus de détails sur ces 2 types d’arbres: arbre logique et arbre visual WPF en 2 min.

Stratégie de propagation

Lorsqu’un évènement routés est déclenché, il est propagé le long de l’arbre visuel. Cette propagation dépend de la stratégie utilisée au moment de la définition de l’évènement:

  • System.Windows.RoutingStrategy.Tunnel: suivant cette stratégie, un évènement routé sera propagé de la racine de l’arbre (l’élément le plus haut dans l’arbre ou celui englobant le plus les autres) vers l’élément source (control dans les feuilles de l’arbre). Par exemple pour un évènement de type click sur un bouton, l’élément racine pourrait être l’élément Window et l’élément source pourrait être un objet Button.
  • System.Windows.RoutingStrategy.Bubble: cette stratégie est l’opposé de la stratégie Tunnel, elle consiste à propager un évènement routé de l’élément source vers l’élément racine. Par exemple pour un évènement de click sur un bouton, la propagation se fait du bouton vers la racine de l’arbre, comme l’élément Window.
  • System.Windows.RoutingStrategy.Direct: cette stratégie correspond à celle d’un évènement .NET classique, il est directement exécuté par le handler auxquel il est rattaché.

En plus de la stratégie de propagation qui indique le sens de parcours de l’évènement, certaines caractériques sont très spécifiques:

  • Il n’est pas nécessaire que tous les éléments de l’arbre s’attachent à l’évènement pour qu’ils soient traversés par cet évènement au moment de sa propagation. Même sans implémentation particulière, un évènement routé parcourera l’arbre visuel de la racine vers l’élément source (ou l’inverse).
  • Il est possible d’arrêter cette propagation à un niveau de l’arbre en indiquant qu’il est handled.
  • N’importe quel objet de type UIElement est capable de s’abonner à un évènement routé même s’il ne le définit pas. Ainsi un élément de type StackPanel peut s’abonner à un évènement correspondant à un clique sur un bouton.

Convention de nommage des évènements routés

Par convention les évènements routés sont nommés de la façon suivante:

  • PreviewXXX: correspond à un évènement dont la stratégie de propagation est Tunnel. Par exemple l’évènement UIElement.PreviewKeyDownEvent.
  • XXX: sans préfixe “Preview”, un évènement possède une stratégie de propagation Bubble. Par exemple, l’évènement UIElement.KeyDownEvent.

Evènements routés définis par pair

La plupart du temps, les évènements routés sont définis par pair:

  • Un évènement Tunnel préfixé par “Preview”,
  • Un évènement Bubble sans prefixe.

Par exemple, les évènements suivants sont définis dans System.Windows.UIElement et dans System.Windows.ContentElement:

Evènement Tunnel Evènement Bubble Déclenché quand:
PreviewDragEnter DragEnter Un objet commence à être glissé avec la sourisà partir d’un élément
PreviewDragLeave DragLeave Un objet est déposé avec la souris dans un élément
PreviewDragOver DragOver Un objet est glissé avec la souris au dessus d’un élément
PreviewDrop Drop Un objet est déposé avec la souris au dessus d’un élément
PreviewGotKeyboardFocus GotKeyboardFocus Un élément obtient le focus de saisie
PreviewKeyDown KeyDown Une touche du clavier est enfoncée
PreviewKeyUp KeyUp Une touche du clavier est relachée
PreviewLostKeyboardFocus LostKeyboardFocus Un élément perd le focus de saisie
PreviewMouseDown MouseDown Un bouton de la souris est enfoncé
PreviewMouseLeftButtonDown MouseLeftButtonDown Le bouton gauche de la souris est enfoncé
PreviewMouseLeftButtonUp MouseLeftButtonUp Le bouton gauche de la souris est relâché
PreviewMouseMove MouseMove La souris est déplacée au dessus d’un élément
PreviewMouseRightButtonDown MouseRightButtonDown Le bouton droit de la souris est enfoncé
PreviewMouseRightButtonUp MouseRightButtonUp Le bouton droit de la souris est relâché
PreviewMouseUp MouseUp Un bouton de la souris est relâché
PreviewMouseWheel MouseWheel La roulette de la souris est actionnée
PreviewTextInput TextInput Une touche correspodant à du texte est frappés.

Toutefois certains évènements ne respectent pas cette convention. Par exemple, l’évènement Bubble ButtonBase.ClickEvent n’a pas d’équivalent Tunnel.

Séquencement des évènements définis par pair

Lorsque des évènements sont définis par pair, ils sont déclenchés dans un ordre précis:

  • L’évènement Tunnel (avec le préfixe “Preview”) est déclenché en premier: cet évènement se propage de la racine de l’arbre jusqu’à l’élément source.
  • L’évènement Bubble (sans le préfixe “Preview”) est déclenché ensuite: cet évènement se propage de l’élément source jusqu’à la racine de l’arbre.

L’intérêt de cet ordre est de pouvoir arrêter la propagation d’un évènement lors de la propagation de l’évènement Tunnel:

  • La propagation d’un évènement s’arrête lorsqu’il est marqué handled dans un handler.
  • Si on indique qu’un évènement Tunnel est handled, sa propagation s’arrête et son équivalent Bubble ne sera pas déclenché.

Par exemple, si on écrit:

<Window>  
    <StackPanel>  
        <Label Content="Label" />  
        <Button Content="Button" />  
    </StackPanel>  
</Window>

Si on clique sur le bouton, l’évènement UIElement.PreviewMouseRightDown va parcourir l’arbre de la racine vers l’élément ayant le focus:

  • Window,
  • StackPanel,
  • Button

Ensuite l’évènement UIElement.MouseRightDown parcourera l’arbre de l’élément ayant le focus vers la racine:

  • Button,
  • StackPanel,
  • Window.

Types d’évènements routés

Il existe 5 types d’évènements routés:

  • Evènements du cycle de vie (i.e. Lifetime events),
  • Evènements de la souris (i.e. Mouse events),
  • Evènements du clavier (i.e. Keyboard events),
  • Evènements du stylet (i.e. Stylus events) et
  • Evènements Multitouch.

Evènements du cycle de vie

La classe System.Windows.FrameworkElement définit des évènements liés au cycle de vie des éléments (i.e. lifetime event):

  • FrameworkElement.Initialized: déclenché lorsqu’un élément est initialisé.
  • FrameworkElement.Loaded: cet évènement est déclenché après initialisation, utilisation des styles et data binding des propriétés.
  • FrameworkElement.Unloaded: lorsqu’un élément est libéré.

D’autres évènements sont définis par la classe System.Window:

  • Window.SourceInitialized: se déclenche lorsque l’objet HwndSource permettant de s’interfacer avec l’API Win32 est instancié.
  • Window.ContentRendered: se déclenche après l’affichage de la fenêtre.
  • Window.Activated: se déclenche quand la fenêtre devient active.
  • Window.Deactivated: déclenché lorsque la fenêtre n’a plus le focus.
  • Window.Closing: se déclenche à la fermeture d’une fenêtre (ne se déclenche en cas de déconnexion ou d’arrêt du système).
  • Window.Closed: évènement déclenché lorsqu’une fenêtre est fermée.

Evènements du clavier

Les évènements du clavier (i.e. Keyboard events) sont:

  • UIElement.KeyDown et UIElement.PreviewKeyDown: évènements respectivement bubble et tunnel correspondant à un enfoncement de touche sur le clavier.
  • UIElement.TextInput et UIElement.PreviewTextInput: évènements respectivement bubble et tunnel déclenchés lorsque des touches correspondant à du texte sont frappées.
  • UIElement.KeyUp et UIElement.PreviewKeyUp: évènements respectivement bubble et tunnel déclenchés quand des touches du clavier sont relachées.

Des combinaisons des touches comme “Control + A”, “Majuscule + A”, “Alt + A”, “Windows + A” sont renseignées, aux déclenchements des évènements, dans la classe System.Windows.Input.KeyEventArgs. Les combinaisons correspondront à des évènements successifs (par exemple, un évènement pour “Control” et un autre pour “A”). Il faut s’aider de la classe statique System.Windows.Input.Keyboard et de la propriété Keyboard.Modifiers pour identifier une combinaison.

Evènements de la souris

Les évènements provoquées par la souris (i.e. Mouse events) sont:

  • UIElement.MouseEnter et UIElement.MouseLeave: ces évènements sont déclenchés respectivement, quand la souris commence à passer au dessus d’un élément et quand il le quitte.
  • UIElement.MouseMove et UIElement.PreviewMouseMove: évènements respectivement bubble et tunnel correspondant au déplacement de la souris au dessus d’un élément.
  • UIElement.MouseRightButtonDown et UIElement.PreviewMouseRightButtonDown: évènements respectivement bubble et tunnel correspondant à l’enfoncement du bouton droit de la souris.
  • UIElement.MouseLeftButtonDown et UIElement.PreviewMouseLeftButtonDown: évènements respectivement bubble et tunnel correspondant à l’enfoncement du bouton gauche de la souris.
  • UIElement.MouseRightButtonUp et UIElement.PreviewMouseRightButtonUp: évènements respectivement bubble et tunnel correspondant au relâchement du bouton droit de la souris.
  • UIElement.MouseLeftButtonUp et UIElement.PreviewMouseLeftButtonUp: évènements respectivement bubble et tunnel correspondant au relâchement du bouton gauche de la souris.

Implémentation des évènements routés

La syntaxe utilisée pour définir et utiliser les évènements routés est un peu particulière. Il existe plusieurs implémentations possibles si on ajoute une callback coté Xaml ou coté code behind ou pour la définition de l’évènement.

Le but de cette partie est de lister toutes les syntaxes possibles.

Ajouter une callback à un évènement

Pour s’abonner à un évènement routé et exécuter une callback à son déclenchement, il faut ajouter handler à cet évènement. La définition d’un handler peut se faire de plusieurs façons.

Handler défini dans UIElement

Dans la hiérarchie de classe des objets en WPF (cf. arbre logique et arbre visual WPF en 2 min), System.Windows.Controls.Control dérive de System.Windows.FrameworkElement qui dérive de System.Windows.UIElement. Donc si UIElement permet de rajouter un handler on peut s’y abonner facilement de n’importe quel control dans le code Xaml.

Par exemple, on peut s’abonner directement à UIElement.KeyDown car:

partial class UIElement 
{ 
    public event KeyEventHandler KeyDown 
    { 
        add { AddHandler(Keyboard.KeyDownEvent, value, false); } 
        remove { RemoveHandler(Keyboard.KeyDownEvent, value); } 
    } 
}

Ainsi on peut écrire:

<Window x:Class="WpfApplication.MainWindow" 
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
        Title="MainWindow" Height="400" Width="400" KeyDown="WindowKeyDown"> 
    <StackPanel KeyDown="StackPanelKeyDown"> 
        <Label Content="Text: "/> 
        <TextBox KeyDown="TextBoxKeyDown" /> 
    </StackPanel> 
</Window>

Les handlers doivent être implémentés dans le code behind:

namespace WpfApplication 
{ 
    public partial class MainWindow : Window 
    { 
        public MainWindow() 
        { 
            InitializeComponent(); 
        } 
 
        private void WindowKeyDown(object sender, KeyEventArgs e) 
        { 
            MessageBox.Show("WindowKeyDown"); 
        } 
 
        private void StackPanelKeyDown(object sender, KeyEventArgs e) 
        { 
            MessageBox.Show("StackPanelKeyDown"); 
        } 
 
        private void TextBoxKeyDown(object sender, KeyEventArgs e) 
        { 
            MessageBox.Show("TextBoxKeyDown"); 
        } 
    } 
}

Evènement défini dans une autre classe

On peut s’abonner à un évènement même s’il est définit dans une autre classe.

Par exemple, l’évènement Keyboard.KeyDown est, en réalité, défini dans la classe System.Windows.Input.Keyboard:

public static class Keyboard 
{ 
    public static readonly RoutedEvent KeyDownEvent = EventManager.RegisterRoutedEvent("KeyDown", RoutingStrategy.Bubble, typeof(KeyEventHandler), typeof(Keyboard)); 
  
    public static void AddKeyDownHandler(DependencyObject element, KeyEventHandler handler) 
    { 
        UIElement.AddHandler(element, KeyDownEvent, handler); 
    } 
  
    public static void RemoveKeyDownHandler(DependencyObject element, KeyEventHandler handler) 
    { 
        UIElement.RemoveHandler(element, KeyDownEvent, handler); 
    } 
}

On peut s’abonne à cet évènement en écrivant dans le code Xaml (on remplace KeyDown par Keyboard.KeyDown):

<Window x:Class="WpfApplication.MainWindow" 
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
        Title="MainWindow" Height="400" Width="400" Keyboard.KeyDown="WindowKeyDown"> 
    <StackPanel Keyboard.KeyDown="StackPanelKeyDown"> 
        <Label Content="Text: "/> 
        <TextBox Keyboard.KeyDown="TextBoxKeyDown" /> 
    </StackPanel> 
</Window>

Le code behind reste identique.

Ajouter un handler dans le code behind

Comme pour tous les évènements .NET, on peut s’abonner directement dans le code behind à condition qu’un handler y soit défini.

Ainsi l’évènement Keyboard.KeyDown possède un handler dans la classe UIElement:

partial class UIElement 
{ 
    public event KeyEventHandler KeyDown 
    { 
        add { AddHandler(Keyboard.KeyDownEvent, value, false); } 
        remove { RemoveHandler(Keyboard.KeyDownEvent, value); } 
    } 
}

Ainsi si le code Xaml est:

<Window x:Class="WpfApplication.MainWindow" 
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
        Title="MainWindow" Height="400" Width="400" Keyboard.KeyDown="WindowKeyDown"> 
    <StackPanel Name="stackPanel" Keyboard.KeyDown="StackPanelKeyDown"> 
        <Label Content="Text: "/> 
        <TextBox Name="textBox" Keyboard.KeyDown="TextBoxKeyDown" /> 
    </StackPanel> 
</Window>

On peut écrire:

namespace WpfApplication 
{ 
    public partial class MainWindow : Window 
    { 
        public MainWindow() 
        { 
            InitializeComponent(); 
 
            this.KeyDown += new RoutedEventHandler(WindowKeyDown); 
            this.stackPanel.KeyDown += new RoutedEventHandler(StackPanelKeyDown); 
            this.textBox.KeyDown += new RoutedEventHandler(TextBoxKeyDown); 
        } 
 
        private void WindowKeyDown(object sender, RoutedEventArgs e)  
        {  
            MessageBox.Show("WindowKeyDown");  
        }  
  
        private void StackPanelKeyDown(object sender, RoutedEventArgs e)  
        {  
            MessageBox.Show("StackPanelKeyDown");  
        }  
  
        private void TextBoxKeyDown(object sender, RoutedEventArgs e)  
        {  
            MessageBox.Show("TextBoxKeyDown");  
        } 
    } 
}

On peut aussi utiliser une notation plus concise:

this.KeyDown += WindowKeyDown; 
this.stackPanel.KeyDown += StackPanelKeyDown; 
this.textBox.KeyDown += TextBoxKeyDown;

Utiliser UIElement.AddHandler()

Dans le cas où il n’y a pas d’handlers, on peut utiliser UIElement.AddHandler() pour abonne n’importe quel élément à un évènement (de même on peut supprimer l’abonnement avec UIElement.RemoveHandler()).

Par exemple, l’évènement System.Windows.Controls.Primitives.ButtonBase.Click ne possède pas de handler dans la classe UIElement, on peut donc utiliser UIElement.AddHandler() dans le code behind.

Dans le cas de l’exemple précédent, on peut écrire:
this.textBox.AddHandler(ButtonBase.ClickEvent, (RoutedEventHandler)ButtonClick);

Avec:

private void ButtonClick(object sender, RoutedEventArgs e)  
{  
    MessageBox.Show("TextBoxKeyDown");  
}

Utiliser EventManager.RegisterClassHandler()

Pour abonner une callback à un évènement, on peut utiliser la classe statique System.Windows.EventManager avec la méthode:

EventManager.RegisterClassHandler(Type classType, RoutedEvent routedEvent, Delegate handler,  
    bool handledEventsToo)

Par exemple, à partir du constructeur statique d’un élément:

public class CustomControl : ContentControl  
{  
    static CustomControl()  
    {  
        EventManager.RegisterClassHandler(typeof(CustomControl), 
            ButtonBase.ClickEvent, new RoutedEventHandler(ButtonClick)); 
    }  
  
    private void ButtonClick(object sender, RoutedEventArgs e)  
    {  
        MessageBox.Show("ButtonClick");  
    } 
}

Interrompre la propagation d’un évènement

On peut interrompre la propagation d’un évènement routé quel que soit la stratégie de propagation en marquant l’évènement handled. Il faut modifier la valeur de la propriété RoutedEventArgs.Handled:

private void ButtonClick(object sender, RoutedEventArgs e)  
{  
    e.Handled = true; 
}
La propagation des évènements routés n’est pas réellement stoppée

En réalité, l’évènement routé n’est pas vraiment arrêté. Il continue sa propagation mais, par défaut, les éléments qui s’y sont abonnés ne seront pas déclenchés.

Pour être notifié même dans le cas où un évènement a déjà été marqué handled par un autre élément, il faut renseigner le paramètre handledEventsToo dans la méthode:
UIElement.AddHandler(RoutedEvent routedEvent, Delegate handler, bool handledEventsToo)

Par exemple:

this.textBox.AddHandler(ButtonBase.ClickEvent, (RoutedEventHandler)ButtonClick, true);

Définir un évènement routé

On peut définir un évènement routé en utilisant la classe statique System.Windows.EventManager avec la méthode:

EventManager.RegisterRoutedEvent(string name, RoutingStrategy routingStrategy, 
    Type handlerType, Type ownerType)

Par exemple, si on souhaite définir l’évènement ClickEvent dans un control particulier:

public class CustomControl : ContentControl 
{ 
    public static readonly RoutedEvent ClickEvent; 
 
    static CustomControl() 
    { 
        CustomControl.ClickEvent = EventManager.RegisterRoutedEvent( 
          "ClickOnCustomControl", RoutingStrategy.Bubble, 
          typeof(RoutedEventHandler), typeof(CustomControl)); 
    } 
 
    public event RoutedEventHandler Click 
    { 
        add 
        { 
            base.AddHandler(CustomControl.ClickEvent, value); 
        } 
        remove 
        { 
            base.RemoveHandler(CustomControl.ClickEvent, value); 
        } 
    } 
}

On peut s’abonner à cet évènement en utilisant le handler:

this.customControl.Click += new RoutedEventHandler(ButtonClick);

Avec:

private void ButtonClick(object sender, RoutedEventArgs e)  
{  
    MessageBox.Show("ButtonClick");  
}

Partager un évènement routé entre plusieurs classes

Lorsqu’on évènement routé est défini avec System.Windows.EventManager.RegisterRoutedEvent(), il est défini pour un type particulier. Si on réexécute cette fonction pour un type différent avec le même nom d’évènement, une exception sera levée. Pour éviter cette exception, un évènement peut être partagé entre plusieurs classes avec:

System.Windows.RoutedEvent.AddOwner(Type ownerType)

Par exemple, dans l’exemple précédent, pour partager l’evènement avec une autre classe:

RoutedEvent newRoutedEvent = CustomControl.AddOwner(typeof(OtherCustomControl));
Evènements partagés entre UIElement et ContentElement

De nombreux évènements sont partagés entre System.Windows.UIElement et System.Windows.ContentElement comme par exemple Mouse.MouseDownEvent.

Ainsi cet évènement est partagé en utilisant:

public static readonly RoutedEvent MouseDownEvent = 
    Mouse.MouseDownEvent.AddOwner(UIElement);

Et:

public static readonly RoutedEvent MouseDownEvent = 
    Mouse.MouseDownEvent.AddOwner(ContentControl);

Déclencher un évènement routé

Déclencher un évènement se fait en utilisant la méthode:

UIElement.RaiseEvent(RoutedEventArgs e)

Ainsi pour déclencher l’évènement System.Windows.Controls.Primitives.ButtonBase.ClickEvent, on peut écrire:

RoutedEventArgs e = new RoutedEventArgs(ButtonBase.ClickEvent, this); 
this.RaiseEvent(e);
Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInEmail this to someonePrint this page

Arbre logique et arbre visuel WPF en 2 min

En WPF, les éléments visuels sont organisés hiérarchiquement avec une structure en arbre: des éléments sont ajoutés au contenu d’autres éléments formant des nœuds dans un arbre d’objets. Le developpeur implémente les relations entre les objets formant ainsi un arbre logique (i.e. logical tree). Lorsque les objets sont dessinés puis affichés au runtime, les relations entres les objets peut différer de celles de l’arbre logique, ces relations utilisées pendant l’affichage est l’arbre visuel (i.e. visual tree).

Hiérarchie des classes d’objets

Avant de rentrer dans les détails des arbres d’objets, on peut rappeler la hiérarchie des classes des objets principaux en WPF. Il ne faut pas confondre la hiérarchie des classes avec les arbres logiques et visuels:

  • Hiérarchie des classes: arbre d’héritage des types d’objets.
  • Arbre logique et arbre visuel: structures de composition des objets valables seulement au runtime.

La hiérarchie des classes des objets est:

Les classes se décomposent de la façon suivante:

  • System.Object: toutes les classes dérivent de ce type.
  • System.Windows.Threading.DispatcherObject: ce sont des objets utilisant le Dispatcher WPF (boucle de messages WPF). L’accès à ces objets est limité au thread ayant créé l’objet.
  • System.Windows.DependencyObject: classe de base des objets supportant les i>dependency properties.
  • System.Windows.Freezable: classe de base des objets pouvant être figés dans un état en lecture seule pour des raisons de performance. Ce sont, le plus souvent, primitives graphiques comme les brushes, les pens, les classes géométriques ou les classes d’animation. Lorsque ces objets sont figés, ils peuvent être partagés entre plusieurs threads (contrairement aux objets de type DispatcherObject). Ces objets restent toujours figés et peuvent être cloné si besoin.
  • System.Windows.Media.Visual: classe de base des objets ayant une représentation visuel 2D.
  • System.Windows.UIElement: classe de base des objets 2D supportant les fonctionnalités WPF de routed events, des command bindings, du système de layout et de focus.
  • System.Windows.ContentElement: classe de base similaire à UIElement n’ayant pas un comportement de rendu visuel. Ces objets sont contenus par un objet de type Visual qui peut avoir un rendu graphique. Un objet de type ContentElement a besoin de plusieurs objets Visual pour avoir un rendu graphique complet.
  • System.Windows.FrameworkElement: ajoute au UIElement une gestion des styles, du data binding, des ressources, des mécanismes de tooltip et de menu contextuel.
  • System.Windows.FrameworkContentElement: similaire à FrameworkElement pour des objets ayant un contenu.
  • System.Windows.Controls.Control: classe de base des controls WPF. Cette classe permet de rajouter des propriétés comme Foreground, Background ou FontSize.

Arbre logique

Comme indiqué plus haut, les objets WPF sont organisés dans une structure en arbre c’est-à-dire que les instances d’objets sont créées et rangées dans d’autres objets suivant une relation de composition.

Il y a donc des éléments contenus dans d’autres éléments, une relation d’enfant entre un objet et un autre et inversement une notion de parent.

Le but de l’arbre logique est de gérer de façon uniforme les objets de type FrameworkElement et FrameworkContentElement suivant leurs relations d’appartenance.

Ainsi la notion d’arbre logique est responsable des fonctionnalités:

  • La propriété FrameworkElement.Parent: cette propriété présente dans tous les objets de type FrameworkElement permet d’avoir une relation Parent-Enfant pour tous ces objets, definissant ainsi la relation d’appartenance.
  • Héritage des valeurs des DependencyProperty: certaines propriétés sont héritables comme FontFamily ou DataContext c’est-à-dire que la valeur de ces propriétés est hérité du parent logique lorsqu’il existe (i.e. logical ancestor). L’héritage des valeurs se fait entre les objets de type FrameworkElement ou FrameworkContentElement.
  • Références {DynamicResource}: lorsque le code Xaml comporte une référence {DynamicResource}, la recherche de la ressource se fait suivant l’arbre logique sur les objets possédant une propriété Resources de type ResourceDictionary. Cette recherche se fait sur les “ancêtres logiques” (i.e. logical ancestors). Resources est une propriété des objets de type FrameworkElement ou FrameworkContentElement (mais aussi System.Windows.Application).
  • Recherche du nom: quand il faut chercher le nom d’un élément, par exemple, quand on écrit dans le code Xaml: {Binding ElementName=OtherElement}, la recherche se fait aussi suivant les “ancêtres logiques”.
  • Routed events: les “évènements routés” (i.e. routed events) lorsqu’ils sont déclenchés, suivent l’arbre logique du haut vers le bas pour les évènements “tunnel” (i.e. tunneling routed events) ou du bas vers le haut pour les évènements “bubble” (i.e. bubbling routed events).

L’ajout ou la suppression se fait par l’intermédiaire des fonctions FrameworkElement.AddLogicalChild ou FrameworkElement.RemoveLogicalChild de façon implicite lorsqu’on affecte une valeur aux objets, par exemple, par l’intermédiaire de la propriété ContentControl.Content.

L’ajout d’enfants se ne fait forcement de la même façon pour tous les objets mais varie suivant leurs spécifités:

Namespace Classe Propriétés
System.Windows.Controls AdornedElementPlaceholder Child
ContentControl Content
Decorator Child
Grid Children (hérité de Panel)
Columns
Rows
HeaderedItemsControl Items (hérité de ItemsControl)
Header
ItemsControl Items
Page Content
Panel Children
RichTextBox Document
TextBlock Text
TextBox Text
ViewBox Child
System.Windows.Controls.Primitives DocumentViewerBase Document
Popup Child
System.Windows.Documents PageContent Child
Table RowGroups
Columns
Span Inlines

Arbre visuel

L’arbre visuel est une notion de relations entre les objets lorsqu’ils ont un rendu graphique. Comme pour l’arbre logique, il existe une relation d’appartenance toutefois cette relation se fait sur les objets de type System.Windows.Media.Visual.

L’arbre visuel peut être similaire à l’arbre logique toutefois, d’une façon générale, il comporte plus d’éléments car à un élément de l’arbre logique correspondant un ou plusieurs éléments de l’arbre visuel. L’arbre logique peut ne pas exister mais l’arbre visuel existe toujours.

Par exemple quand on écrit:

<Window> 
    <StackPanel> 
        <Label Content="Label" /> 
        <Button Content="Button" /> 
    </StackPanel> 
</Window>

L’arbre visuel est:

L’arbre logique comporte les éléments écrits dans le code Xaml: Window, StackPanel, Label et Button alors que l’arbre visuel comporte d’autres éléments nécessaires à l’affichage.

Ainsi l’arbre visuel sert à:

  • Effectuer le rendu visuel des éléments,
  • Héritage des valeurs des propriétés: si un élément n’a pas de parent dans l’arbre logique alors la règle d’héritage de la valeur d’une propriété héritable se fait suivant l’arbre visuel au lieu de l’arbre logique.
  • Références {DynamicResource}: de même, s’il n’y a pas d’arbre logique, c’est l’arbre visuel qui est utilisé pour la recherche d’une ressource.
  • Routed events: les “évènements routés” parcourent aussi l’arbre visuel s’il est différent de l’arbre logique.
  • Propagation des propriétés UIElement.Opacity, UIElement.IsEnabled: la recherche de valeur de ces propriétés se fait suivant l’arbre visuel.
  • Transformations: les transformations UIElement.RenderTransform et UIElement.LayoutTransform sont prises en compte dans un élément suivant les transformations appliqués à son parent dans l’arbre visuel.

LogicalTreeHelper et VisualTreeHelper

Il est possible de circuler le long des arbres logiques et visuels en s’aidant des fonctions des classes statiques System.Windows.LogicalTreeHelper et System.Windows.Media.VisualTreeHelper.

La classe System.Windows.LogicalTreeHelper possède les fonctions:

  • GetChildren(): pour chercher les enfants logiques d’un élément de type DependencyObject, FrameworkContentElement ou FrameworkElement.
  • GetParent(): pour trouver le parent logique d’un élément de type DependencyObject.
  • FindLogicalNode(): pour un objet enfant suivant son nom dans l’arbre logique.

De même, le classe System.Windows.Media.VisualTreeHelper possède des fonctions similaires:

  • GetChild(): renvoie l’élément enfant d’un objet de type DependencyObject situé à un index donné dans une collection d’objets enfants.
  • GetChildCount(): renvoie le nombre d’éléments enfant.
  • GetParent(): renvoie le parent d’un objet de type DependencyObject.

Chercher un type particulier parmi les enfants

Pour chercher récursivement parmi les enfants d’un objet de type DependencyObject, un objet suivant son type:

public static T FindChild<T>(DependencyObject parent)  
    where T : DependencyObject 
{ 
    if (parent == null) return null; 
 
    for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++) 
    { 
        var child = VisualTreeHelper.GetChild(parent, i); 
 
        var result = (child as T) ?? FindChild<T>(child); 
        if (result != null) return result; 
    } 
    return null; 
}

Cherche un élément avec un nom particulier parmi les enfants

De même on peut effectuer la recherche de façon récursive en cherchant un type et un nom particulier:

public static T FindChild<T>(DependencyObject parent, string childName) 
   where T : DependencyObject 
{     
  if (parent == null) return null; 
 
  T foundChild = null; 
 
  int childrenCount = VisualTreeHelper.GetChildrenCount(parent); 
  for (int i = 0; i < childrenCount; i++) 
  { 
    var child = VisualTreeHelper.GetChild(parent, i); 
 
    T childType = child as T; 
    if (childType == null) 
    { 
      foundChild = FindChild<T>(child, childName); 
 
      if (foundChild != null) break; 
    } 
    else if (!string.IsNullOrEmpty(childName)) 
    { 
      var frameworkElement = child as FrameworkElement; 
      if (frameworkElement != null && frameworkElement.Name == childName) 
      { 
        foundChild = (T)child; 
        break; 
      } 
    } 
    else 
    { 
      foundChild = (T)child; 
      break; 
    } 
  } 
 
  return foundChild; 
}

Chercher tous les enfants ayant un type particulier

Pour chercher tous les descendants ayant un type particulier:

public static IEnumerable<T> FindVisualChildren<T>(DependencyObject parent)  
    where T : DependencyObject 
{ 
    if (parent != null) 
    { 
        for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++) 
        { 
            DependencyObject child = VisualTreeHelper.GetChild(parent, i); 
            if (child != null && child is T) 
            { 
                yield return (T)child; 
            } 
 
            foreach (T childChildren in FindVisualChildren<T>(child)) 
            { 
                yield return childChildren; 
            } 
        } 
    } 
}

Débugguer l’arbre visuel

Il est possible de voir l’arbre visuel des objets WPF d’une application au runtime avec des outils particuliers. Par exemple Snoop 2.8.0 est un outil très puissant:

  • Voir les objets de l’arbre visuel,
  • Voir et modifier les propriétés des objets de l’arbre visuel,
  • Voir le trajet des évènement routés dans le sens “tunnel” (généralement préfixés par Preview comme PreviewKeyDown) et dans le sens “bubble” (par exemple KeyDown),
  • Localiser un objet directement sur l’interface de l’application à partir de l’arbre visuel: cette fonctionnalité est particulièrement utile pour débugguer l’héritage des propriétés, l’application de styles…
Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInEmail this to someonePrint this page

Quelques méthodes pour cloner des objets en .NET

Parfois pour certains traitements, il est nécessaire de cloner l’instance d’un objet en une ou plusieurs autres instances distinctes. Suivant la complexité de l’objet ou suivant le nombre de copies nécessaires, cette copie peut être plus ou moins complexe à effectuer. D’autre part, dans le cas où on veut dupliquer un objet en plusieurs instances, il peut être nécessaire d’optimiser la duplication pour qu’elle soit la plus rapide possible. Enfin, suivant le type des objets à dupliquer, on peut vouloir rendre générique le procédé de duplication pour qu’il puisse s’adapter à plusieurs types d’objets.

Le but de cet article est de présenter quelques procédés de duplication d’instances d’un objet.

Quelque soit le procédé choisi pour dupliquer une instance d’un objet:

  • On souhaite que les instances dupliquées comporte des valeurs identiques concernant les champs (données membres privées) et les propriétés. De même que pour les champs, les valeurs des propriétés doivent être identiques même si l’accès en écriture à la propriété est privé.
  • Il faudrait que le procédé de duplication minimise les changements nécessaires à l’objet pour qu’il puisse être dupliqué: présence d’un constructeur sans paramètres, présence de l’attribut Serializable ou données membres accessibles dont l’accès en lecture et écriture est publique.
Choix de la méthode pour effectuer la duplication:

Il n’y a pas de méthodes parfaites pour effectuer la duplication d’objets, la méthode choisie dépend du contexte:

  • Possibilité d’adapter les objets à dupliquer: certaines méthodes nécessitent d’adapter les objets à dupliquer, par exemple, en ajoutant un constructeur sans paramètres ou en ajoutant un attribute particulier. La méthode choisie dépend de la façon dont on peut intervenir sur l’objet pour le rendre duplicable.
  • Nombre d’objets à dupliquer: si on doit dupliquer un grand nombre d’objets, on peut chercher à optimiser la vitesse de duplication.
  • Hétérogénéïté des objets: si les types des objets sont très différents, on peut privilégier une méthode générique plutôt qu’une méthode qui nécessite de modifier les classes à dupliquer.
  • Complexité des objets: suivant les couches d’abstractions des objets à dupliquer, on peut privilégier des méthodes plus génériques.

Dans tous les cas, une bonne approche est d’utiliser des tests pour vérifier le comportement de la méthode choisie.

On distingue 2 familles de méthodes pour dupliquer des objets:

  • Des méthodes nécessitant de modifier les objets à dupliquer: ces méthodes sont généralement faciles à mettre en œuvre. Elles sont possibles si on peut modifier l’implémentation des objets et si les modifications n’impactent pas trop d’objets.
  • Des méthodes génériques: ces méthodes ne nécessitent pas de connaître les objets à dupliquer. Elles sont à privilégier si on ne peut pas modifier l’implémentation des objets ou si la duplication s’effectue sur des objets de types très hétérogènes.

Quelques définitions en préambule

“Shallow copy” et “deep copy”

On distingue 2 types de duplication:

  • Shallow copy (i.e. copie superficielle): ce sont avant tout des copies simples et rapides d’un objet. Dans un contexte C#, ce type de duplication copie les valeurs des membres qui sont de type valeur (struct et enum). Pour les objets de type référence (classes, interfaces et delegates), seule la référence est dupliquée ce qui signifie que la référence d’origine et la référence dupliquée pointe vers le même objet.
    Les objets de type System.String sont des objets de type référence (car ils dérivent de Object). Toutefois sachant que les strings sont immutables (c’est-à-dire qu’il n’est pas possible de les modifier sans créer une nouvelle instance), dans le cadre d’une copie superficielle, elles sont dupliquées complêtement au même titre que les objets de type valeur.
  • Deep copy (i.e. copie en profondeur): tous les membres de l’objet d’origine sont dupliqués vers des valeurs et des instances distinctes quelque soit la complexité du membre.

Interface System.ICloneable

A partir du framework 2.0, l’interface System.ICloneable permet de décorer les objets de façon à ce qu’ils implémentent la méthode Clone():

public interface ICloneable 
{ 
    object Clone(); 
}

Cette interface n’indique pas si la copie est superficielle ou en profondeur, le choix est laissé au soin du développeur. Le 2e inconvénient de cette interface est qu’elle impose que l’objet cloné est de type Object. Il faut donc effectuer un cast sur le résultat de ICloneable.Clone() pour l’utiliser:

ICloneable cloneableObject = new CloneableObject(); 
CloneableObject objectClone = (CloneableObject)cloneableObject.Clone();

Méthodes nécessitant de modifier les objets à dupliquer

Implémenter ICloneable.Clone()

La méthode la plus directe pour dupliquer un objet est de le prévoir dans l’implémentation de cet objet. On peut alors implémenter la méthode ICloneable.Clone().

Par exemple, si on considère la classe:

public class Car 
{ 
    private string brand; 
    private string modelName; 
    private string reference; 
    private decimal price; 
    private Engine engine; 
    private Person owner; 
 
    public string Brand { get { return this.brand; } } 
    public string ModelName { get { return this.modelName; } } 
    public string Reference { get { return this.reference; } } 
    public decimal Price { get { return this.price; } } 
    public Engine Engine { get { return this.engine; } } 
    public Person Owner { get { return this.owner; } } 
 
 
    public Car(string brand, string modelName, string reference,  
        decimal price, Engine engine, Person owner) 
    { 
        this.brand = brand; 
        this.modelName = modelName; 
        this.reference = reference; 
        this.price = price; 
        this.engine = engine; 
        this.owner = owner; 
    } 
}

avec:

public class Engine  
{ 
    private string reference; 
    private string serialNumber; 
 
    public int Power { get; set; } 
    public string Reference { get { return this.reference; } } 
    public string SerialNumber { get { return this.serialNumber; } } 
 
    public Engine(int power, string reference, string serialNumber) 
    { 
        this.Power = power; 
        this.reference = reference; 
        this.serialNumber = serialNumber; 
    } 
}

Et:

public struct Person 
{ 
    public string FirstName { get; set; } 
    public string LastName { get; set; } 
}

On peut implémenter ICloneable.Clone() dans la classe Car:

public class Car : ICloneable 
{ 
    // [...] 
 
    public object Clone() 
    { 
        return new Car(this.brand, this.modelName, this.reference, 
            this.price, this.engine, this.Owner); 
    } 
}

Les membres brand, modelName, reference et price seront bien dupliqués mais pas engine (puisqu’on ne fait que dupliquer les références). Il faut donc implémenter aussi ICloneable.Clone() pour la classe Engine:

public class Engine : ICloneable 
{ 
    // [...] 
 
    public object Clone() 
    { 
        return new Engine(this.power, this.reference, this.serialNumber); 
    } 
}

Et modifier Car.Clone() en conséquence:

public class Car : ICloneable 
{ 
    // [...] 
 
    public object Clone() 
    { 
        return new Car(this.brand, this.modelName, this.reference, this.price,  
            this.engine.Clone() as Engine, this.Owner); 
    } 
}

La structure Person est un objet de type valeur donc lorsqu’on écrit this.Owner, on obtient déjà une copie de l’objet d’origine. Il n’est donc pas nécessaire d’implémenter une duplication explicite.

L’intéret de cette méthode est sa simplicité. Si on rajoute un membre dans une classe et qu’on modifie le constructeur, on sera obligé de modifier l’implémentation de la fonction ICloneable.Clone() correspondante.
En revanche, à chaque modification des membres d’une des classes, reporter la modification dans la fonction ICloneable.Clone() peut s’avérer fastidieux. Les méthodes ICloneable.Clone() impose un cast systématique lorsqu’elles sont utilisées.

Enfin, dans le cas où on implémente des tests pour vérifier la duplication, le moindre changement dans les membres des classes va imposer de modifier aussi les tests des duplications.

Duplication dans le constructeur

Une autre méthode est de prévoir la duplication directement dans le constructeur.

Par exemple, pour les classes Engine et Car:

public class Engine  
{ 
    // [...] 
 
    public Engine(Engine templateEngine) 
        : this(templateEngine.Power, templateEngine.reference, templateEngine.SerialNumber) 
    {  
    } 
} 
 
public class Car 
{ 
    // [...] 
 
    public Car(Car templateCar) : 
        this(templateCar.Brand, templateCar.ModelName, templateCar.Reference, 
        templateCar.Price, new Engine(templateCar.Engine), templateCar.Owner) 
    { 
    } 
}

De même cette méthode est facile à mettre en œuvre et n’impose pas un cast à son utilisation.

Object.MemberwiseClone()

Cette fonction permet d’effectuer une shallow copy d’un objet de type référence. Elle va donc créer une nouvelle instance et y copier les champs non statiques en effectuant une copie bit à bit des membres de type valeur. Comme indiqué plus haut, une shallow copy ne duplique pas les membres de type référence.
Object.MemberwiseClone() est protected, elle ne peut donc pas être appelée directement à l’extérieur de la classe à dupliquer.

Si on utilise cette méthode pour la classe Car, on peut réimplémenter la méthode ICloneable.Clone():

public class Car : ICloneable 
{ 
    private string brand; 
    private string modelName; 
    private string reference; 
    private decimal price; 
    private Engine engine; 
    private Person owner; 
 
    // [...] 
 
    public object Clone() 
    { 
        return this.MemberwiseClone(); 
    } 
}

Avec MemberwiseClone(), la structure Person est dupliquée au même titre que les autres objets de type valeur. En revanche, la classe Engine n’est pas dupliquée, seule la référence est dupliquée.

Ainsi si on exécute le code suivant:

var car = new Car("Ford", "Mustang", "GT", 3000m, new Engine(300, "BigEngine", "FE34F3"), 
    new Person() { FirstName = "Paul", LastName = "Ryan" }); 
 
var clonedCopy = car.Clone() as Car; // en utilisant MemberwiseClone() 
clonedCopy.Engine.Power = 340; 
 
var originalPower = car.Engine.Power; 
var clonedPower = clonedCopy.Engine.Power;

originalPower et clonedPower ont la même valeur.

L’intérêt de Object.MemberwiseClone() est qu’il n’est pas nécessaire d’implémenter un constructeur sans paramètre pour dupliquer un objet toutefois cette méthode comporte quelques inconvénients:

  • Elle n’effectue qu’une shallow copy, donc on utilise l’implémentation précédente de Car et qu’on exécute Car.Clone(), les membres brand, modelName, reference et price seront correctement dupliqués dans la nouvelle instance. En revanche, les membres Car.engine et Car.owner ne seront pas dupliqués car ce sont des objets de type référence. Ces membres dans la classe dupliquée pointeront vers les mêmes objets que l’objet d’origine.
  • Dans le cas où le développeur ne sait pas que Object.MemberwiseClone() n’effectue qu’une shallow copy et qu’il rajoute des membres de type référence, l’exécution pourra mener à des comportements inattendus car, dans le cas de la classe Car, le membre engine pointe vers le même objet que l’instance d’origine.

Méthodes génériques

Ces méthodes sont plus génériques car elles peuvent être utilisées sans avoir une connaissance des membres des objets à dupliquer à la compilation. La découverte des membres de l’objet à dupliquer se fait directement à l’exécution.

Reflection

On peut utiliser la reflection pour effectuer certains traitements:

  • Vérifier qu’une classe satisfait l’interface ICloneable pour exécuter la méthode ICloneable.Clone() qu’elle contient,
  • Accéder à la méthode Object.MemberwiseClone() d’une classe pour l’exécuter,
  • Pouvoir accéder aux données membres privées d’une classe de façon à en dupliquer la valeur dans la copie.

Les inconvénients de la reflection sont la lenteur d’exécution et il n’est pas autorisé de l’exécuter dans un environnement partial trust.

Instanciation de la copie avec Activator.CreateInstance()

Une première approche consiste à instancier la classe sans utiliser Object.MemberwiseClone() avec System.Activator.CreateInstance(). Le gros inconvénient de cette méthode est qu’elle nécessite d’implémenter un constructeur sans paramètres.

Dans le cadre de la classe Car (définie plus haut), il faudrait modifier l’objet de cette façon:

public class Car 
{ 
    private string brand; 
    private string modelName; 
    private string reference; 
    private decimal price; 
    private Engine engine; 
    private Person owner; 
 
    public string Brand { get { return this.brand; } } 
    public string ModelName { get { return this.modelName; } } 
    public string Reference { get { return this.reference; } } 
    public decimal Price { get { return this.price; } } 
    public Engine Engine { get { return this.engine; } } 
    public Person Owner { get { return this.owner; } } 
 
    public Car() 
    { 
    } 
 
    public Car(string brand, string modelName, string reference, 
        decimal price, Engine engine, Person owner) 
    { 
        this.brand = brand; 
        this.modelName = modelName; 
        this.reference = reference; 
        this.price = price; 
        this.engine = engine; 
        this.owner = owner; 
    } 
}

L’instanciation de la copie se fait en exécutant:

var originalCar = new Car(); 
Type carType = originalCar.GetType(); 
object copiiedCar = Activator.CreateInstance(carType);

“Shallow copy” avec Object.MemberwiseClone()

Exécuter la fonction MemberwiseClone() en utilisant la reflection permet d’éviter d’implémenter un constructeur sans paramètres:

MethodInfo memberwiseCloneMethod = typeof(Car).GetMethod("MemberwiseClone", 
    BindingFlags.Instance | BindingFlags.NonPublic); 
car copiiedCar = (Car)memberwiseCloneMethod.Invoke(input, null);

Copie des propriétés

Comme indiqué plus haut Object.MemberwiseClone() effectue une shallow copy d’un objet. Les objets de type référence ne sont pas vraiment dupliqués.

Une solution consiste à utiliser la reflection pour parcourir les membres de la classe:

  • On écarte de ce parcours les membres de type valeur qui sont des types primitifs ainsi que les objets System.String puisqu’ils sont pris en charge par MemberwiseClone(). En revanche on parcourt les structures car même si elles sont des objets de type valeur, elles peuvent contenir des des membres de type référence.
  • On parcourt d’abord les propriétés publiques et protected de la classe et des éventuelles classes mère (en cas d’héritage) pour effectuer la copie.
  • On parcourt ensuite les propriétés privés et les champs pour exécuter récursivement la copie.

Pour vérifier si un objet est de type primitif et qu’il n’est pas une System.String:

public static bool IsPrimitive(Type type) 
{ 
    if (type == typeof(String)) return true; 
    return type.IsValueType & type.IsPrimitive; 
}

Pour parcourir les propriétés publiques et protected de la classe et des classes mère, on utilise les BindingFlags suivant:

  • BindingFlags.Instance: permet de sélectionner des membres instanciables (données membres).
  • Bindings.NonPublic: sélectionner les membres private et protected.
  • Bindings.Public: permet de sélectionner les membres public.
  • Binding.FlattenHierarchy: permet de sélectionner les champs, les méthodes, les évènements et les propriétés public et protected de la classe et des classes mère (dans le cas d’un héritage). Les membres privés et les types encapsulés (i.e. Nested types) ne sont pas retournées.

Avec Binding.FlattenHierarchy, il n’est pas nécessaire de parcourir récursivement les propriétés de la classe et des classes mère car ce paramètre renvoie toutes les propriétés public et protected héritées.

Pour parcourir les données membres privés, on utilise les BindingFlags BindingFlags.Instance et Bindings.NonPublic et on filtre ensuite pour ne garder que les membres privés. Le parcours doit s’effectuer sur la classe mais aussi récursivement sur les membres privés des classes mère.

On peut effectuer le parcours des membres avec les méthodes suivantes:

private static readonly MethodInfo memberwiseCloneMethod = 
    typeof(Object).GetMethod("MemberwiseClone", BindingFlags.NonPublic |  
        BindingFlags.Instance); 
 
public static Object Copy(Object objectToCopy) 
{ 
    return InternalCopy(objectToCopy); 
} 
 
private static Object InternalCopy(Object objectToCopy) 
{ 
    var objectTypeToCopy = objectToCopy.GetType(); 
    var cloneObject = memberwiseCloneMethod.Invoke(objectToCopy, null); 
 
    CopyNonPrivateMembers(objectTypeToCopy, objectToCopy, cloneObject); 
    CopyPrivateFieldsForObjectBaseType(objectTypeToCopy, objectToCopy, cloneObject); 
 
    return cloneObject; 
} 
 
private static void CopyPrivateFieldsForObjectBaseType(Type objectTypeToCopy, 
    object objectToCopy, object objectCopy) 
{ 
    if (objectTypeToCopy.BaseType != null) 
    { 
        CopyPrivateFieldsForObjectBaseType(objectTypeToCopy.BaseType, objectToCopy, objectCopy); 
        CopyPrivateMembers(objectTypeToCopy.BaseType, objectToCopy, objectCopy); 
    } 
} 
 
private static void CopyNonPrivateMembers(Type objectTypeToCopy, 
    object objectToCopy, object objectCopy) 
{ 
    BindingFlags nonPrivateMemberFlags = BindingFlags.Instance | BindingFlags.NonPublic |  
        BindingFlags.Public | BindingFlags.FlattenHierarchy; 
    CopyMembers(objectTypeToCopy, objectToCopy, objectCopy, nonPrivateMemberFlags, false); 
} 
 
private static void CopyPrivateMembers(Type objectTypeToCopy, object objectToCopy, 
    object objectCopy) 
{ 
    BindingFlags privateMemberFlags = BindingFlags.Instance | BindingFlags.NonPublic; 
    CopyMembers(objectTypeToCopy, objectToCopy, objectCopy, privateMemberFlags, true); 
} 
 
private static void CopyMembers(Type objectTypeToCopy, object objectToCopy, 
    object objectCopy, BindingFlags bindingFlags, bool getPrivateMembers) 
{ 
    foreach (FieldInfo fieldInfo in objectTypeToCopy.GetFields(bindingFlags)) 
    { 
        if (getPrivateMembers && !fieldInfo.IsPrivate) continue; 
        var originalFieldValue = fieldInfo.GetValue(objectToCopy); 
        var clonedFieldValue = InternalCopy(originalFieldValue); 
        fieldInfo.SetValue(objectCopy, clonedFieldValue); 
    } 
}
Implémentation complète d’une duplication par reflection

On peut trouver une implémentation complète de la duplication par reflection dans le projet net-object-deep-copy de Burtsev Alexey sur Github.

Sérialisation

La duplication d’objets en utilisant la sérialisation se fait en deux temps:

  • On sérialise l’objet à dupliquer
  • Puis on désérialise l’objet sérialisé.

On obtient alors une copie en profondeur (i.e. deep copy) de l’objet d’origine. Pour utiliser cette méthode, il faut que la classe à dupliquer soit sérialisable et qu’elle soit décorée avec l’attribut: [Serializable].

Il est possible d’utiliser tous les types de “serializer” (binaire, Soap, JSON, XML, etc…). Toutefois pour que la duplication soit rapide et provoque le moins d’erreur possible, utiliser le sérialisation binaire permet d’avoir un bon compromis.

L’intérêt de cette méthode est qu’elle est très simple à mettre en œuvre et son implémentation est rapide. En revanche, c’est la méthode de duplication la plus lente.

Sérialisation simple

L’implémentation la plus simple de la sérialisation consiste à sérialiser tout l’objet.

Une duplication par serialisation binaire peut s’implémenter de cette façon:

using System.IO; 
using System.Runtime.Serialization; 
using System.Runtime.Serialization.Formatters.Binary; 
 
public static T Clone<T>(T objectToCopy) 
{ 
    if (!typeof(T).IsSerializable) 
    { 
        throw new InvalidOperationException("The type shall be serializable."); 
    } 
 
    // Don't serialize a null object, simply return the default for that object 
    if (Object.ReferenceEquals(objectToCopy, null)) 
    { 
        return default(T); 
    } 
 
    IFormatter formatter = new BinaryFormatter(); 
    using (Stream stream = new MemoryStream()) 
    { 
        formatter.Serialize(stream, objectToCopy); 
        stream.Seek(0, SeekOrigin.Begin); 
        return (T)formatter.Deserialize(stream); 
    } 
}

Cette méthode peut s’avérer compliquée à mettre en œuvre si on ne maîtrise pas tous les types des membres de l’objet à dupliquer en particulier s’ils ne sont pas sérialisables.

Serialization surrogate

Comme indiqué plus haut, la sérialisation est parfois impossible car certains membres de l’objet à dupliquer peuvent ne pas être sérialisables. Dans ce cas, on souhaiterait avoir un comportement différent suivant si un membre est sérialisable ou non.

La solution consiste à utiliser un serialization surrogate (i.e. “assistant de sérialisation”). Un serialization surrogate doit satisfaire System.Runtime.Serialization.ISerializationSurrogate:

  • GetObjectData(): permet d’indiquer dans un objet de type SerializationInfo les données nécessaires pour sérialiser l’objet.
  • SetObjectData(): utilise les données présentes dans un objet de type SerializationInfo pour construire l’objet.

Si on considère les objets suivants:

public class Car 
{ 
    public string Brand { get; set; } 
    public string ModelName { get; set; } 
    public string Reference { get; set; } 
    public decimal Price { get; set; } 
    public Engine Engine { get; set; } 
    public Person Owner { get; set; } 
} 
public class Engine 
{ 
    public int Power { get; set; } 
    public string Reference { get; set; } 
    public string SerialNumber { get; set; } 
} 
public class Person 
{ 
    public string FirstName { get; set; } 
    public string LastName { get; set; } 
}

On peut définir des serialization surrogates particuliers pour la classe Car et la classe Engine:
CarSerializationSurrogate: il assiste pour la sérialisation de la classe Car.
EngineSerializationSurrogate: il assiste pour la sérialisation de la classe Engine.
Il n’y a pas de serialization surrogate pour la classe Person. D’autre part, on n’implémente rien de particulier pour cette classe.

L’implémentation des serialization surrogates est:

public class CarSerializationSurrogate : ISerializationSurrogate 
{ 
    private const string BrandValueName = "Brand"; 
    private const string ModelNameValueName = "ModelName"; 
    private const string ReferenceValueName = "Reference"; 
    private const string PriceValueName = "Price"; 
    private const string EngineValueName = "Engine";

    public void GetObjectData(object obj, SerializationInfo info, StreamingContext context) 
    { 
        Car car = (Car)obj; 
        info.AddValue(BrandValueName, car.Brand); 
        info.AddValue(ModelNameValueName, car.ModelName); 
        info.AddValue(ReferenceValueName, car.Reference); 
        info.AddValue(PriceValueName, car.Price); 
        info.AddValue(EngineValueName, car.Engine); 
    } 
 
    public object SetObjectData(object obj, SerializationInfo info, 
       StreamingContext context, ISurrogateSelector selector) 
    { 
        Car car = (Car)obj; 
        car.Brand = info.GetString(BrandValueName); 
        car.ModelName = info.GetString(ModelNameValueName); 
        car.Reference = info.GetString(ReferenceValueName); 
        car.Price = info.GetDecimal(PriceValueName); 
        car.Engine = (Engine)info.GetValue(EngineValueName, typeof(Engine)); 
        return car; 
    } 
} 
 
public class EngineSerializationSurrogate : ISerializationSurrogate 
{ 
    private const string PowerValueName = "Power"; 
    private const string ReferenceValueName = "Reference"; 
    private const string SerialNumberValueName = "SerialNumber";

    public void GetObjectData(object obj, SerializationInfo info, StreamingContext context) 
    { 
        Engine engine = (Engine)obj; 
        info.AddValue(PowerValueName, engine.Power); 
        info.AddValue(ReferenceValueName, engine.Reference); 
        info.AddValue(SerialNumberValueName, engine.SerialNumber); 
    } 
 
    public object SetObjectData(object obj, SerializationInfo info, 
        StreamingContext context, ISurrogateSelector selector) 
    { 
        Engine engine = (Engine)obj; 
        engine.Power = info.GetInt32(PowerValueName); 
        engine.Reference = info.GetString(ReferenceValueName); 
        engine.SerialNumber = info.GetString(SerialNumberValueName); 
        return engine; 
    } 
}

Pour prendre en compte les serialization surrogates lors de la sérialisation:

public class Serializer 
{ 
    public static Car CloneWithSerializationSurrogates(Car carToCopy) 
    { 
        var surrogateSelector = new SurrogateSelector(); 
        surrogateSelector.AddSurrogate(typeof(Car), 
            new StreamingContext(StreamingContextStates.All), 
            new CarSerializationSurrogate()); 
        surrogateSelector.AddSurrogate(typeof(Engine), 
            new StreamingContext(StreamingContextStates.All), 
            new EngineSerializationSurrogate());

        IFormatter formatter = new BinaryFormatter(); 
        formatter.SurrogateSelector = surrogateSelector;

        using (Stream stream = new MemoryStream()) 
        { 
            formatter.Serialize(stream, carToCopy); 
            stream.Seek(0, SeekOrigin.Begin); 
            return (Car)formatter.Deserialize(stream); 
        } 
    }
}

Si on exécute le code suivant:

var originalCar = new Car { 
    Brand = "Ford", ModelName = "Mustang", Reference = "GT",  
    Price = 3000m,  
    Engine = new Engine { Power = 300, Reference = "BigEngine",   
        SerialNumber = "FE34F3" }, 
    Owner = new Person { FirstName = "Paul", LastName = "Ryan" } 
}; 
Car duplicatedCar = Serializer.CloneWithSerializationSurrogates(originalCar);

Tous les membres de la classe originalCar seront sérialisés et correctement dupliqués à l’exception du membre Owner puisqu’on a omis la prise en compte de ce membre dans les fonctions CarSerializationSurrogate.GetObjectData() et CarSerializationSurrogate.SetObjectData().

Serialization surrogate selector

L’implémentation précédente permettait de choisir le serialization surrogate à utiliser suivant le type d’objet à sérialiser. Il est toutefois possible d’avoir une implémentation particulière quant au choix du serialization surrogate à utiliser, ce choix se fait un serialization surrogate selector (i.e. “sélectionneur d’assistant de sérialisation”).

Un serialization surrogate selector doit satisfaire l’interface System.Runtime.Serialization.ISurrogateSelector.

Les surrogate selectors sont organisés en chaîne, c’est-à-dire si un surrogate selector ne peut pas sélectionner un serialization surrogate alors il fournit un autre surrogate selector qui sera sollicité pour sélectionner un serialization surrogate et ainsi de suite. L’interface ISurrogateSelector permet d’organiser cette chaîne:

  • ChainSelector(): permet d’indiquer une instance d’un surrogate selector qui sera le suivant dans la chaine si le surrogate selector actuel ne peut sélectionner un serialization surrogate.
  • GetNextSelector(): retourne le surrogate selector suivant dans la chaîne. S’il n’y a pas de surrogate selector suivant, alors cette fonction renvoie null.
  • GetSurrogate(): fournit le serialization surrogate qui assistera la sérialisation. S’il le surrogate selector ne sélectionne pas de serialization surrogate alors cette fonction renvoie null.

Une implémentation intéressante d’un surrogate selector serait de fournir un serialization surrogate particulier seulement si le membre est sérialisable.

Ainsi, dans le cas où l’objet à sérialiser est de type System.String, un type primitif ou si il est sérialisable, on ne fournit pas de serialization surrogate. Le comportement est normal et les objets sont sérialisés. Dans le cas où les objets ne sont pas sérialisables et s’ils sont des classes ou des structures alors on fournit un serialization surrogate particulier.

L’implémentation du surrogate selector est:

public class SurrogateSelectorForNonSerialiazableType : ISurrogateSelector 
{ 
    private ISurrogateSelector nextSelector; 
    private ISerializationSurrogate serializationSurrogateForNonSerializableType; 
    public SurrogateSelectorForNonSerialiazableType( 
        ISerializationSurrogate serializationSurrogateForNonSerializableType) 
    { 
        this.serializationSurrogateForNonSerializableType = 
            serializationSurrogateForNonSerializableType; 
    } 
    #region ISurrogateSelector members 
    public void ChainSelector(ISurrogateSelector selector) 
    { 
        this.nextSelector = selector; 
    } 
    public ISurrogateSelector GetNextSelector() 
    { 
        return this.nextSelector; 
    } 
    public ISerializationSurrogate GetSurrogate(Type type, StreamingContext context, 
        out ISurrogateSelector selector) 
    { 
        selector = null; 
        ISerializationSurrogate surrogate = null; 
        if (!IsKnownType(type) && (type.IsClass || type.IsValueType)) 
        { 
            selector = this; 
            surrogate = this.serializationSurrogateForNonSerializableType; 
        } 
        return surrogate; 
    } 
    #endregion 
    private static bool IsKnownType(Type type) 
    { 
        return type == typeof(string) || type.IsPrimitive 
            || type.IsSerializable; 
    } 
}

Une implémentation du serialization surrogate pourrait être:

public class SerializationSurrogateForNonSerializableType : ISerializationSurrogate 
{ 
    private static IEnumerable<FieldInfo> GetSerializableFieldInfos(object obj) 
    { 
        return obj.GetType().GetFields(BindingFlags.Instance | 
            BindingFlags.Public | BindingFlags.NonPublic) 
            .Where(fi => fi.FieldType == typeof(string) || fi.FieldType.IsPrimitive || 
                fi.FieldType.IsSerializable || fi.FieldType.IsClass); 
    }

    public void GetObjectData(object obj, SerializationInfo info, StreamingContext context) 
    { 
        foreach (var fi in GetSerializableFieldInfos(obj)) 
        { 
            info.AddValue(fi.Name, fi.GetValue(obj)); 
        } 
    }

    public object SetObjectData(object obj, SerializationInfo info, StreamingContext context, 
        ISurrogateSelector selector) 
    { 
        foreach (var fi in GetSerializableFieldInfos(obj)) 
        { 
            fi.SetValue(obj, info.GetValue(fi.Name, fi.FieldType)); 
        } 
        return obj; 
    } 
}

La duplication est effectuée en exécutant:

public static Car Clone(Car carToCopy) 
{ 
     
    IFormatter formatter = new BinaryFormatter(); 
    formatter.SurrogateSelector = new SurrogateSelector(); 
    formatter.SurrogateSelector.ChainSelector( 
        new SurrogateSelectorForNonSerialiazableType( 
            new SerializationSurrogateForNonSerializableType())); 
    using (Stream stream = new MemoryStream()) 
    { 
        formatter.Serialize(stream, carToCopy); 
        stream.Seek(0, SeekOrigin.Begin); 
        return (Car)formatter.Deserialize(stream); 
    } 
}
Implémentation complête d’une méthode de duplication par sérialisation:

On peut trouver une implémentation plus complète de la duplication par sérialisation en utilisant un surrogate selector dans A Generic Method for Deep Cloning in C# 3.0.

Expression Trees

Il existe plusieurs exemples de code de duplication générique en utilisant des arbres d’expressions (i.e. expression trees).

L’intérêt des expression trees est de pouvoir construire dynamiquement une suite d’instructions. On peut ensuite, compiler cette suite d’instructions en code IL afin de l’exécuter.

La méthode duplication par reflection décrite plus haut s’exécute en découvrant les types et caractéristiques des membres de l’objet à dupliquer. De façon à effectuer une copie en profondeur de l’objet (i.e. deep copy), l’algorithme va parcourir récursivement ses membres afin de les dupliquer. S’il y a des duplications à effectuer sur 10 objets de même type, l’algorithme parcourera l’arbre des 10 objets de la même façon. Parcourir les arbres d’objets par reflection est couteux en performance. Dans le cas où un type d’objet est connu, une optimisation pourrait être de copier la valeur des membres directement avec leur nom et non en parcourant systématiquement tous les membres.

Utiliser les expression trees permet d’optimiser la méthode de duplication par reflection puisqu’on peut générer une expression effectuant la duplication pour chaque type d’objet. On peut ensuite compiler cette expression et l’exécuter à chaque fois qu’on détecte un objet de même type. La duplication est ainsi plus rapide puisqu’on exécute directement du code IL et ensuite, on ne fait un parcours systématique de tous les membres de l’objet. La duplication se fait en copiant les valeurs avec les noms des membres directement.

Les méthodes de duplication par expression tree, sont plus rapides que les méthodes utilisant la reflection pure et la sérialisation à partir du moment où les expressions ont été générées. Ces méthodes sont intéressantes lorsqu’il y a beaucoup d’objets de même type à dupliquer.

La contrepartie de l’utilisation des expression trees est qu’elles sont difficilement débugables et que le code pour générer les expressions est complexes.

Comme pour la méthode par reflection, la première étape de la duplication par expression trees est d’appeler la copie superficielle avec Object.MemberwiseClone(). Ainsi à titre d’exemple, l’exécution de cette méthode par reflection se fait de la façon suivante:

MethodInfo memberwiseCloneMethod = typeof(Object).GetMethod("MemberwiseClone",
    BindingFlags.Instance | BindingFlags.NonPublic); 
 
ExampleClass output = (ExampleClass)memberwiseCloneMethod.Invoke(input, null);

En utilisant les expression trees, l’implémentation devient:

ParameterExpression inputParameter = Expression.Parameter(
    typeof(ExampleClass)); 
ParameterExpression outputParameter = Expression.Parameter(typeof(ExampleClass)); 
 
MethodInfo memberwiseCloneMethod = typeof(Object).GetMethod("MemberwiseClone", 
    BindingFlags.Instance | BindingFlags.NonPublic); 
 
Expression lambdaExpression = Expression.Assign(outputParameter, 
    Expression.Convert(
        Expression.Call(inputParameter, memberwiseCloneMethod), 
        typeof(ExampleClass)));

Ainsi l’intérêt est qu’après compilation de l’expression tree, l’exécution est plus rapide puisqu’il n’y a plus l’étape de reflection pour récupérer les informations sur la méthode Object.MemberwiseClone().

D’une façon générale, tous les traitements effectués pour la méthode de duplication par reflection sont adaptées de la même façon pour utiliser les expression trees. Il faut donc être familié, dans un premier temps, avec les implémentations de duplication par reflection pour comprendre plus facilement les implémentation avec les expression trees.

Quelques implémentations de duplication avec des expression trees:

L’implémentation suivante de la duplication par expression trees: Fast Deep Copy by expression trees (C#) s’inspire de cette méthode de duplication par reflection: https://github.com/Burtsev-Alexey/net-object-deep-copy.

Il existe d’autres implémentations de la duplication par expression trees: FastClone, Fast Deep Cloning ou le projet CloneExtensions sur GitHub.

Tester la méthode de duplication

Avant de procéder à l’implémentation d’une méthode pour dupliquer, une bonne approche est d’implémenter un test qui permettrait de vérifier que:

  • La duplication produit bien une instance distincte de même type,
  • Les données membres de l’objet d’origine sont correctement dupliquées.

Dans le cas d’une méthode générique, il faut vérifier le comportement pour la duplication:

  • De membres privés et publics
  • De membres readonly
  • Des classes et des structures
  • Dans le cas d’héritage
  • Des tableaux d’objets
Références:

Duplication par reflection:
Project net-object-deep-copy sur Github: https://github.com/Burtsev-Alexey/net-object-deep-copy

Duplication par sérialisation binaire:

Duplication avec des expression trees:

Environnement partial trust:

Autres:

Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInEmail this to someonePrint this page

“Expression trees” en 2 min

Quelques définitions en préambule

Avant de rentrer dans les détails des arbres d’expression (i.e. expression trees), il faut définir quelques termes.

Delegate

Il s’agit du type d’une référence vers une méthode comportant une signature particulière. Le delegate définit donc le type de la référence et non pas la référence elle-même. Par exemple, en C# un delegate peut se définir de cette façon:

public delegate int AddDelegate(int a, int b);

La méthode suivante possède une définition compatible avec le delegate en raison de sa signature:

public static int Add(int a, int b) 
{ 
    return a + b; 
}

On peut donc instancier le delegate et l’exécuter de cette façon:

AddDelegate delegateInstance = Add; 
int result = delegateInstance(3, 5);

A partir du C# 2.0, il est possible d’avoir une notation plus directe pour définir les delegates:

AddDelegate delegateInstance = delegate(int a, int b)  
{
    return a + b; 
};

Expression lambda

Une expression lambda est une notation permettant de créer des types de delegates ou d’arbres d’expression. Les expressions lambda utilise une notation utilisant l’opérateur “=>”.

Si on prends l’exemple précédent, on peut utiliser une expression lambda pour définir le delegate:

AddDelegate delWithLambda = (a, b) => a + b;

Cette notation est un raccourci pour:

AddDelegate delWithLambda = (a, b) => { return a + b; };

Le delegate s’exécute de la même façon que précédemment:

int result = delWithLambda(3, 5);

Les expressions lambda sont apparues avec C# 3.0.

Action<T> et Func<T, TResult>

Il s’agit de delegates prédéfinis pour faciliter l’utilisation de delegates et d’expression lambda. L’inconvénient de l’exemple précédent est qu’il nécessite la définition du delegate AddDelegate:

public delegate int AddDelegate(int a, int b);

Pour éviter de définir des delegates avant d’utiliser des expressions lambda, on peut utiliser Action et Func:

  • Action<T> correspond à des delegates de méthodes (par de type de retour) de 0 ou plusieurs arguments.
  • Func<T, TResult> correspond à des delegates de fonctions de 0 ou plusieurs arguments avec un résultat.

Dans l’exemple précédent, si on utilise Func<T, TResult>:

Func<int, int, int> addWithFunc = (a, b) => a + b;

Une autre notation est équivalente (peu utilisée car plus lourde):

Func<int, int, int> addWithFunc = delegate(a, b) { return a + b; };

Les types Action<T> et Func<T, TResult> sont apparus avec le framework .NET 3.5.

Expression

En C#, le type Expression désigne un objet permettant de représenter une expression lambda sous la forme d’un arbre d’expressions (i.e. expression tree). Ce type se trouve dans le namespace System.Linq.Expressions, il s’utilise sous la forme:
Expression<Func<TResult>> ou Expression<TDelegate>TDelegate est un delegate définit au préalable.

Ainsi Expression<Func<TResult>> correspond à la représentation fortement typée d’une expression lambda, elle ne contient pas seulement sa définition mais aussi toute sa description.
Expression<TDelegate> dérive de la classe abstraite System.Linq.Expressions.LambdaExpression qui correspond à la classe de base pour représenter une expression lambda sous forme d’un arbre d’expressions:

public sealed class Expression<TDelegate> : LambdaExpression
Expression<TDelegate> et Expression

Il ne faut pas confondre les classes Expression<TDelegate> et Expression car Expression<TDelegate> dérive de LambdaExpression et LambdaExpression dérive de Expression.

Intérêt de Expression<Func<TResult>> par rapport à Func<TResult>

L’intérêt de ce type est de pouvoir intervenir sur cette description pour l’utiliser pour comprendre ce que contient l’expression, la modifier ou simplement l’exécuter. A la différence, un delegate Func<TResult> peut seulement être exécuté. Par exemple, il sera plus compliqué de modifier l’implémentation d’un delegate Func<TResult> au runtime alors qu’il est possible de la faire avec Expression<Func<TResult>>.

Il n’est pas facile de comprendre la différence entre Expression<Func<TResult>> et Func<TResult>:

  • Func<TResult> désigne un delegate prédéfini correspondant à un fonction sans arguments et retournant un paramètre de type TResult.
  • Expression<Func<TResult>> désigne toute la description du delegate Func<TResult>.

Par exemple, si on considère l’expression:

Expression<Func<int, int, int>> expression = (a, b) => a + b;

On peut voir que le contenu de l’objet expression par rapport à un Func équivalente.
Par exemple le contenu de:

Func<int, int, int> func = (a, b) => a + b;

est:

On peut voir que func est bien un delegate, il n’y a plus de traces de l’implémentation d’origine: (a, b) => a + b.

Le contenu de l’objet expression est:

Dans cet objet, il y a tout le détail de l’implémentation de l’expression lambda (a, b) => a + b.

Ainsi, Expression<Func<TResult>> décrit ce que fait l’expression lambda alors que Func<TResult> permet seulement de l’exécuter.

Manipulation des Expression<Func<TResult>>

Construction

Il est possible de construire un arbre d’expressions de 2 façons: avec une expression lambda ou avec l’API Expressions.

Par exemple, en utilisant une expression lambda:

Expression<Func<int, int, int>> expression = (a, b) => a + b;

De même en utilisant l’API:

Using System.Linq.Expressions;
 
// ...

// "a" ne sert que pour la documentation
ParameterExpression aParam = Expression.Parameter(typeof(int), "a");  
ParameterExpression bParam = Expression.Parameter(typeof(int), "b"); 
BinaryExpression add = Expression.Add(aParam, bParam); 
Expression<Func<int, int, int>> expression = 
    Expression.Lambda<Func<int, int, int>>(add, new ParameterExpression[] { aParam, bParam });

L’API permet d’implémenter un grand nombre de fonctions avec les fonctions statiques de la classe System.Linq.Expressions.Expression.

Par exemple:

Fonction Permet de définir
Expression.Parameter() Un argument
Expression.Constant() Une constante
Expression.Assign() Affecter une expression à un argument
Expression.Add()
Expression.Multiply()
Expression.Divide()
Expression.Not()
Expression.And()
Expression.Or()
Des opérations sur les arguments
Expression.Label() Un label qui va permettre d’effectuer des sauts dans le code (GOTO)
Expression.Goto() Un saut dans le code (GOTO)
Expression.LessThan()
Expression.GreaterThan()
Expression.LessThanOrEqual()
Expression.GreaterThanOrEqual()
Expression.Equal()
Expression.NotEqual()
Des opérations de comparaison sur les arguments
Expression.Lambda() Permet de construire un delegate
Expression.Block() Permet de construire le corps d’une méthode
Expression.Loop() Permet de définir une boucle de programmation. Par exemple:
Expression.Loop([Expression contenant le code à évaluer], Expression.Break(...))
Expression.Break() Arrête une boucle Loop et effectue un saut à un “Label” particulier.
Expression.IfThen()
Expression.IfThenElse()
Permet de créer une expression conditionnelle
Expression.IsFalse()
Expression.IsTrue()
Evalue une expression
Expression.New() Crée une expression permettant d’effectuer un appel à un constructeur.
Expression.TryCatch()
Expression.TryCatchFinally()
Expression.Finally()
Un bloc try…catch…finally
Etc…

La liste exhaustive des fonctions se trouve sur MSDN.

Convertir un arbre d’expression en delegate (Expression<Func> en Func)

On peut convertir un objet Expression<Func<TResult>> en Func<TResult> avec la méthode Expression.Compile().

Par exemple:

Expression<Func<int, int, int>> expression = (a, b) => a + b; 
Func<int, int, int> funcFromExpression = expression.Compile();

On peut évaluer le delegate avec:

int result = funcFromExpression(3, 9);
Convertir un delegate en arbre d’expressions

Un delegate est une méthode compilée, pour obtenir l’arbre d’expressions correspondant à ce delegate, il faudrait décompiler la méthode et la convertir en arbre d’expressions. Il n’y a pas de méthodes directes pour effectuer ces étapes.

Convertir un delegate en arbre d’expression (Func en Expression<Func>)

On ne peut pas convertir un delegate Func<TResult> en Expression<Func<TResult>> car les éléments définissant l’arbre d’expressions sont perdus lorsqu’on compile le delegate en faisant Expression.Compile().

On peut toutefois construire une expression qui appelle un delegate en faisant:

Func<int, int, int> func = (a, b) => a + b;
Expression<Func<int>> expressionCallingFunc = Expression.Lambda<Func<int, int, int>>(
    Expression.Call(func.Method));

L’expression obtenue ne fait qu’appeler un delegate. Si le delegate a été obtenu à partir d’une expression avec Expression.Compile(), on ne peut pas retrouver de cette façon l’expression d’origine.

Analyser un arbre d’expressions

Comme indiquer un des intérêts des arbres d’expressions est de pouvoir analyser une expression lambda avec des instructions. On peut ainsi décomposer l’expression lambda en utilisant les propriétés de la classe System.Linq.Expressions.LambdaExpression (la classe Expression<TDelegate> dérive de la classe LambdaExpression):

  • LambdaExpression.Body: pour obtenir une expression contenant le corps de l’expression lambda.
  • LambdaExpression.Parameters: pour obtenir les arguments.

Par exemple:

Expression<Func<int, int, int>> expression = (a, b) => a + b; 
BinaryExpression addOperation = (BinaryExpression)expression.Body; 
ParameterExpression aParam = (ParameterExpression)addOperation.Left; 
ParameterExpression bParam = (ParameterExpression)addOperation.Right;

Manipuler un arbre d’expression avec ExpressionVisitor

Il est possible d’explorer et d’analyser un arbre d’expressions plus facilement qu’en utiliser des propriétés avec un objet de type ExpressionVisitor. Cet objet utilise le design pattern Visiteur.
Pour utiliser cette méthode, il faut dériver de la classe abstraite System.Linq.Expressions.ExpressionVisitor et surcharger les fonctions virtuelles correspondant aux parties de l’expression qu’on souhaite analyser.

Par exemple:

  • ExpressionVisitor.VisitBinary(): sera exécutée pour les nœuds de l’arbre qui sont des opérations (i.e. BinaryExpression).
  • ExpressionVisitor.VisitParameter(): sera exécutée pour les arguments d’une expression (i.e. ParameterExpression).
  • ExpressionVisitor.VisitBlock(): sera exécutée pour les nœuds correspondant au corps d’une fonction (i.e. BlockExpression).
  • Etc…

Se reporter à MSDN pour avoir la liste exhaustive de toutes les méthodes virtuelles.

Pour que les méthodes de la classe soient exécutées, il faut appeler la méthode ExpressionVisitor.Visit().

Par exemple si on dérive de ExpressionVisitor:

public class CustomExpressionVisitor : ExpressionVisitor 
{ 
    protected override Expression VisitBinary(BinaryExpression node) 
    { 
        // ... 
        return node; 
    } 
 
    protected override Expression VisitParameter(ParameterExpression node) 
    { 
        // ... 
        return base.VisitParameter(node); 
    } 
}

Les fonctions surchargées seront exécutées en faisant:

Expression<Func<int, int, int>> addExpression = (a, b) => a + b; 
var expressionVisitor = new CustomExpressionVisitor(); 
expressionVisitor.Visit(addExpression.Body);

L’exécution des méthodes se fera dans l’ordre des expressions dans l’arbre d’expressions.

Par exemple, si on définit le “visiteur”:

public class CustomExpressionVisitor : ExpressionVisitor 
{ 
    protected override Expression VisitBinary(BinaryExpression node) 
    { 
        Console.Write("("); 
        this.Visit(node.Left); 
        Console.Write(" {0} ", node.NodeType); 
        this.Visit(node.Right); 
        Console.Write(")"); 
        return node; 
    } 
 
    protected override Expression VisitParameter(ParameterExpression node) 
    { 
        Console.Write("parameter({0})", node.Name); 
        return base.VisitParameter(node); 
    } 
}

A l’exécution, on aura:

((parameter(a) Add (parameter(b))

Arbre d’expressions immutable

Les arbres d’expressions sont immutables c’est-à-dire qu’il n’est pas possible de le modifier après sa création. Toutefois pour profiter de l’intérêt des arbres, on peut utiliser l’API pour créer des arbres différents suivant les besoins ou utiliser un “visiteur” ExpressionVisitor pour dupliquer un arbre et effectuer des modifications pendant cette duplication.

Par exemple, en utilisant l’API on peut créer plusieurs arbres:

ParameterExpression aParam = Expression.Parameter(typeof(int), "a");  
ParameterExpression bParam = Expression.Parameter(typeof(int), "b");  
BinaryExpression add = Expression.Add(aParam, bParam); 
 
ParameterExpression cParam = Expression.Parameter(typeof(int), "c");  
BinaryExpression divide = Expression.Divide(add, cParam); 
 
Expression<Func<int, int, int>> firstExpression = 
    Expression.Lambda<Func<int, int, int>>(add, new ParameterExpression[] { aParam, bParam }); 
Expression<Func<int, int, float>> secundExpression = 
    Expression.Lambda<Func<int, int, float>>(divide, new ParameterExpression[] { aParam, bParam, cParam });

Comparaison entre Expression<Func<TResult>> et Func<TResult>

La plupart du temps, on utilisera plutôt Func<TResult> car il est rarement nécessaire d’analyser une expression lambda. Toutefois dans certains cas, l’utilisation de Func<TResult> peut avoir des conséquences inattendues.

IEnumerable et IQueryable

Ces 2 types d’objet sont des structures de liste qui conviennent dans des cas assez différents. Ainsi dans le cadre de Entity Framework ou LinQ-to-SQL, certaines méthodes d’extension sur le type IEnumerable possèdent des arguments de type Func<TResult>.

Par exemple la signature de la fonction System.Linq.Enumerable.Where():

public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source,  
   Func<TSource, bool> predicate)

A l’opposé, certaines méthodes d’extension sur IQueryable possèdent des arguments de type Expression<Func<TResult>>, si on compare la signature avec la fonction équivalente System.Linq.Queryable.Where():

public static IQueryable<TSource> Where<TSource>(this IQueryable<TSource> source,  
    Expression<Func<TSource, bool>> predicate)

Ainsi les méthodes d’extension de IEnumerable utilisent des delegates alors que les méthodes d’extension de IQueryable utilisent des arbres d’expressions.

Dans le cadre de Entity Framework et LinQ-to-SQL, cette différence peut avoir des conséquences non négligeables puisque la conversion de code C# vers des requêtes SQL ne peut se faire qu’à partir d’objets de type Expression. Ce sont ces objets Expression qui seront analysés pour déterminer la requête à exécuter dans la base SQL.

Si on utilise des delegates pour effectuer ces requêtes, il ne peut pas y avoir d’évaluation de ces delegates en base de données. Donc pour effectuer le traitement, il faut récupérer tout le contenu de la table sur laquelle la requête doit être exécutée. Ensuite, le delegate fournit en argument de la fonction Where() sera executé sur toutes les lignes pour obtenir le résultat. La méthode d’extension utilisant de delegate est donc moins efficace que son équivalent utilisant un arbre d’expression puisqu’on devra récupérer tout le contenu de la table.

Sachant que IQueryable dérive de IEnumerable, suivant le type de l’argument utilisé pour les fonctions, des surchages différentes seront exécutées:

  • Avec Expression<Func<TResult>>, les surcharges IQueryable seront exécutées,
  • Func<TResult>, ça sera plutôt les surcharges IEnumeable qui seront exécutées.

Entity Framework

Comme indiqué plus haut, suivant le type d’argument utilisé entre Expression<Func<TResult>> et Func<TResult>, Entity Framework aura un comportement différent:

  • Avec Expression<Func<TResult>>: Entity Framework va analyser l’expression pour en déduire la requête SQL à exécuté en base.
  • Avec Func<TResult>: Entity Framework récupère tout le contenu de la table ou des tables sur lesquelles porte la requête, le place dans le contexte et ensuite, exécute le delegate pour effectuer le tri.

Ainsi si on considère une table PERSON avec 3 colonnes:

  • FirstName
  • LastName
  • Age

En exécutant:

public IEnumerable<Person> Where(DbEntities dbContext, Func<Person, bool> whereClause) 
{ 
    return dbContext.Persons.Where(where); 
}

La requête suivante sera exécutée en base:

SELECT  
[Extent1].[LastName] AS [LastName],  
[Extent1].[FirstName] AS [FirstName],  
[Extent1].[Age] AS [Age] 
FROM [dbo].[Persons] AS [Extent1]

De même, on exécutant:

public IEnumerable<Person> Where(DbEntities dbContext, Expression<Func<Person, bool>> whereClause) 
{ 
    return dbContext.Persons.Where(where); 
} 
 
var results = Where(dbContext, p => p.Age > 25);

La requête comportera une clause WHERE:

SELECT  
[Extent1].[LastName] AS [LastName],  
[Extent1].[FirstName] AS [FirstName],  
[Extent1].[Age] AS [Age] 
FROM [dbo].[Persons] AS [Extent1] 
WHERE [Extent1].[Age] > 25

Sérialisation

Les delegates ne sont pas sérialisables, ainsi les Func<T, TResult> et Action<T> ne sont pas sérialisables. Ceci s’explique par le fait que les delegates sont compilés en instructions IL pour être exécuté, au runtime il ne s’agit pas d’une structure mais d’une suite d’instructions IL.

A l’opposé, un objet de type Expression<Func<T, Result>> n’est pas compilé en instructions IL au runtime. Cet objet reste une structure qui, par construction, a été implémentée pour être sérialisable. Comme indiqué plus haut, pour qu’un objet de type Expression soit exécutée, il faut exécuter l’instruction Expression.Compile(). Cette instruction transformera la structure en fonction anonyme exécutable.

Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInEmail this to someonePrint this page