“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

Documentation du code C# en 1 min

Il est possible d’ajouter de la documentation dans du code C# et de générer cette documentation vers des fichiers XML à la compilation.
Il est souvent difficile de maintenir une documentation du code à jour car très souvent quand on modifie le code, on oublie de répercuter ces modifications dans la documentation. La documentation peut devenir alors assez vite obsolète et induire en erreur sur le fonctionnement réel du code.
Il existe en C# quelques balises qui permettent de garantir une certaine cohérence entre les éléments de documentation et le code.

Générer la documentation en fichiers XML

Paramétrer Visual Studio

Pour générer la documentation à la compilation en utilisant Visual Studio, il suffit d’activer l’option dans les propriétés d’un projet:

  1. Clique droit sur le projet puis cliquer “Properties” pour afficher les propriétés,
  2. Dans l’onglet “Build”,
  3. Dans la partie “Output”, cocher “XML documentation file”.

Par défaut le fichier contenant la documentation sera généré dans le répertoire de sortie, le plus souvent:

/bin/Debug

Pour que ce fichier contienne des informations, il faut explicitement documenter les objets dans le code C#.

Rendre les commentaires obligatoires

Dans Visual Studio, on peut considérer les “warning” comme des erreurs dans:

  1. Clique droit sur le projet puis cliquer “Properties” pour afficher les propriétés,
  2. Dans l’onglet “Build”,
  3. Dans la partie “Treat warnings as errors”, sélectionner “All”.

Avec ce paramétrage, tous les “warnings” provoqués par de la documentation incomplête ou incohérente à la compilation seront considérés comme des erreurs.

Documenter du code C#

D’une façon générale, pour ajouter de la documentation à un objet (classe, interface, propriété, méthode etc…) il suffit de taper dans Visual “///” au dessus de l’objet à documenter:

/// <summary>  
/// Commentaire pour la classes  
/// </summary>  
public class Class1  
{  
}

Pour une méthode:

/// <summary> 
/// Commentaire d'une méthode 
/// </summary> 
/// <param name="comments">Liste de commentaires</param> 
/// <param name="commentIndex">Index du commentaire</param> 
/// <returns>Commentaire</returns> 
public string GetCommentFromInt(List<string> comments, int commentIndex) 
{ 
}

La documentation est donc introduite à l’aide de:

  • <summary> pour le corps du commentaire,
  • <param> pour les arguments de méthodes,
  • <returns> pour le résultat de la méthode.

Ainsi la classe:

/// <summary> 
/// Commentaire pour la classes 
/// </summary> 
public class Class1 
{ 
    /// <summary> 
    /// Property in a class 
    /// </summary> 
    public int ClassProperty { get; private set; } 
 
    /// <summary> 
    /// Commentaire d'une méthode 
    /// </summary> 
    /// <param name="comments">Liste de commentaires</param> 
    /// <param name="commentIndex">Index du commentaire</param> 
    /// <returns>Commentaire</returns> 
    public string GetCommentFromInt(List<string> comments, int commentIndex) 
    { 
        return string.Empty; 
    } 
}

Générera la documentation:

<?xml version="1.0"?> 
<doc> 
    <assembly> 
        <name>CommentsInCode</name> 
    </assembly> 
    <members> 
        <member name="T:CommentsInCode.Class1"> 
            <summary> 
            Commentaire pour la classes 
            </summary> 
        </member> 
        <member name="M:CommentsInCode.Class1.GetCommentFromInt(System.Collections.Generic.List{System.String},System.Int32)"> 
            <summary> 
            Commentaire d'une méthode 
            </summary> 
            <param name="comments">Liste de commentaires</param> 
            <param name="commentIndex">Index du commentaire</param> 
            <returns>Commentaire</returns> 
        </member> 
        <member name="P:CommentsInCode.Class1.ClassProperty"> 
            <summary> 
            Property in a class 
            </summary> 
        </member> 
    </members> 
</doc>

Enrichir la documentation

Un des intérêts de la documentation de code est de pouvoir l’enrichir en ajoutant des références qui seront vérifiées à la compilation. Dans le cas où les références ne sont plus satisfaites, la compilation générera une erreur de compilation.

Référence vers un membre avec <see />

La balise <see cref="member" /> permet d’effectuer une référence vers un objet. Par exemple si on considère la classe:

public class Class1 
{ 
    public List<string> Comments { get; private set; } 
 
    /// <summary> 
    /// Commentaire d'une méthode 
    /// </summary> 
    /// <param name="commentIndex"></param> 
    /// <returns>Commentaire</returns> 
    public string GetCommentFromInt(int commentIndex) 
    { 
        return string.Empty; 
    } 
}

Dans les commentaires de l’argument commentIndex, on peut rajouter une référence vers le membre Class1.Comments en utilisant:

<see cref="Class1.Comments"/>

Ainsi:

/// <summary> 
/// Commentaire d'une méthode 
/// </summary> 
/// <param name="commentIndex">Index permettant de récupérer un commentaire 
/// dans <see cref="Class1.Comments"/></param> 
/// <returns>Commentaire</returns> 
public string GetCommentFromInt(int commentIndex) 
{ 
    return string.Empty; 
}

Dans le cas où la référence contient une erreur, par exemple:

<see cref="Class1.Coments"/>

On aura un warning ou une erreur de compilation (suivant la configuration des “warnings”):

XML comment on ... has cref attribute cref 'Class1.Coments' that could
    not be resolved

Quelques exemples pour ajouter ces références:

  • Référence vers une méthode: <see cref="Class1.GetCommentFromInt" />
  • Référence vers une méthode possédant plusieurs surcharges: il faut ajouter les arguments explicitement: <see cref="Class1.GetCommentFromInt(string,int)" />
  • Référence vers une fonction du framework: <see cref="String.Equals(string, string)" />.
  • Dans le cas où les namespaces ne sont pas indiqués, il faut préciser explicitement les namespaces de la méthode: <see cref="System.String.Equals(string, string)" />.

Cas particulier des génériques

Les génériques sont déclarés avec <T> ce qui peut poser problème car les caractères “<” et “>” sont interprétés en XML. La première solution consiste à remplacer le caractère “<” par &lt;.

Par exemple si on déclare la méthode:

public string GetCommentFromInt(List<string> comments, int commentIndex)

Une référence vers cette méthode s’écrira:

<see cref="Class1.GetCommentFromInt(List&lt;string>, int)" />

Une autre méthode consiste à remplacer les caractères “<” et “>” par respectivement “{” et “}”. En prenant le même exemple que précédemment, on aura:

<see cref="Class1.GetCommentFromInt(List{string}, int)" />

Balise <seealso />

Cette balise permet de rajouter une autre référence vers un objet quand une référence avec <see /> existe déjà. Elle s’utilise de la même façon que <see />.

Par exemple:

/// <summary> 
/// On peut obtenir une valeur avec <see cref="Class1.GetCommentFromInt(List{string}, int)" /> 
/// On peut aussi s'aider de <seealso cref="Class1.GetCommentFromInt(string, int)"/> 
/// </summary>

Balise <para />

Permet de structurer un texte en paragraphe lorsque ce texte se trouve entre des balises <summary/>, <remarks /> ou <returns/>.

Balise <exception />

Permet de documenter une exception. Par exemple, pour indiquer qu’une exception de type System.ArgumentNullException est levée si l’argument comments est nul:

/// <summary> 
///  
/// </summary> 
/// <param name="comments"></param> 
/// <param name="commentIndex"></param> 
/// <returns></returns> 
/// <exception cref="System.ArgumentNullException">Exception levée 
/// si <paramref name="comments"/> est nul.</exception> 
public string GetCommentFromInt(List<string> comments, int commentIndex)

Balises <c/> et <code />

Ces balises permettent de renvoyer du texte à interpréter sous forme de code:

  • <c/> doit être utilisé pour un élément sur une seule ligne et
  • <code /> doit être utilisé lorsque le code doit être affiché sur plusieurs lignes.

Par exemple pour <c/>:

/// <summary> 
/// Cette méthode permet de renvoyer <c>string.Empty</c>. 
/// </summary> 
/// <param name="commentFormat"></param> 
/// <param name="commentIndex"></param> 
/// <returns></returns> 
public string GetCommentFromInt(string commentFormat, int commentIndex) 
{ 
    return string.Empty; 
}

De même pour <code />:

/// <summary> 
/// Cette méthode permet de renvoyer <c>string.Empty</c>. Le corps de cette méthode est: 
/// <code>public string GetCommentFromInt(string commentFormat, int commentIndex) 
/// { 
///     return string.Empty; 
/// }</code>
/// </summary> 
/// <param name="commentFormat"></param> 
/// <param name="commentIndex"></param> 
/// <returns></returns> 
public string GetCommentFromInt(string commentFormat, int commentIndex) 
{ 
    return string.Empty; 
}

Balise <paramref />

Permet de faire référence au paramètre d’une méthode:

<paramref name="name"/>

Par exemple:

/// <summary> 
/// Le paramètre <paramref name="comments"/> est obligatoire. 
/// </summary> 
/// <param name="comments"></param> 
/// <param name="commentIndex"></param> 
/// <returns></returns> 
public string GetCommentFromInt(List<string> comments, int commentIndex)

Balise <remarks />

Ajoute des indications en plus de ce qui se trouve à l’intérieur d’une balise <summary />, par exemple:

/// <summary> 
/// Commentaire de base 
/// </summary> 
/// <param name="comments"></param> 
/// <param name="commentIndex"></param> 
/// <returns></returns> 
/// <remarks>Autre commentaire sur cette méthode</remarks>
public string GetCommentFromInt(List<string> comments, int commentIndex)

Balise <value />

S’utilise pour documenter des valeurs d’une propriété. Cette balise n’est pas rajoutée automatiquement.

Par exemple:

/// <summary> 
/// Renvoie l'index de la structure. 
/// </summary> 
/// <value>La valeur doit être un entier</value>
public int Index 
{ 
    get { return index; } 
    set { index = value; } 
}

Balise <typeparam />

Cette balise permet de documenter le type d’un générique. Elle est automatique ajoutée si la déclaration d’un objet contient des génériques.

Par exemple:

/// <summary> 
///  
/// </summary> 
/// <typeparam name="TReturn"></typeparam>
/// <typeparam name="TIndex"></typeparam>
/// <param name="commentIndex"></param> 
/// <returns></returns> 
public TReturn GetTypedComment<TReturn, TIndex>(TIndex commentIndex) 
{ 
    return default(TReturn); 
}

Balise <typeparamref />

Permet de faire une référence vers un type d’un générique.

Par exemple:

/// <summary> 
/// Le type <typeparamref name="TReturn"/> est le type de retour et  
/// <typeparamref name="TIndex"/> est le type de l'index. 
/// </summary> 
/// <typeparam name="TReturn"></typeparam> 
/// <typeparam name="TIndex"></typeparam> 
/// <param name="commentIndex"></param> 
/// <returns></returns> 
public TReturn GetTypedComment<TReturn, TIndex>(TIndex commentIndex)

Balise <list />

Permet d’indiquer une liste d’éléments. Le type de la liste doit être:

  • “bullet” pour une liste simple
  • “number” pour une liste numérotée
  • “table” pour un tableau.

Pour indiquer une liste, il faut utiliser la syntaxe:

<list type="bullet"> 
    <listheader> 
        <term>term</term> 
        <description>description</description> 
    </listheader> 
    <item> 
        <term>term</term> 
        <description>description</description> 
    </item> 
</list>

Les éléments sont utilisés de cette façon:

  • <listheader/> est utilisé dans le cas d’un tableau pour l’entête. Il peut être omis dans les autres cas. S’il y a plusieurs colonnes dans le tableau, il doit y avoir une succession de plusieurs éléments <listheader/>.
  • <term /> indique le nom d’un élément
  • <description /> indique la description d’une élément.
  • <item/> contient un élément à lister.

Autres balises

Pour avoir une liste complête des balises: Recommended Tags for Documentation Comments.

Outils générant la documentation à partir d’un fichier XML

Sur MSDN, on indique qu’il est possible d’utiliser NDoc ou SandCastle (sur GitHub ou CodePlex.

Pour avoir plus de détails pour utiliser SandCastle, on peut se référer à cette page (pour télécharger SandCastle, il faut utiliser le page CodePlex): Génération de documentation avec Sandcastle Help File Builder

D’autres solutions existent:

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

Principe de développement DRY (Don’t Repeat Yourself)

DRY pour “Don’t Repeat Yourself” est un principe de programmation visant à exercer les développeurs à reconnaître des duplications puis de trouver le moyen de les supprimer. Ces duplications peuvent se produire évidemment dans le code mais aussi à tout niveau de l’application comme par exemple dans son architecture, dans les tests unitaires ou dans les processus de déploiement. DRY est donc un principe beaucoup général que simplement de la duplication de code.

Andy Hunt et Dave Thomas qui ont été les premiers (dans The Pragmatic Programmer) à énoncer ce principe, partent de l’idée que chaque ligne de code d’une application doit être maintenue. D’autre part, le plupart des interventions sur du code ont pour but d’effectuer de la maintenance plutôt que l’écriture de code nouveau. Ainsi pour corriger une logique répétée, il faut:

  • d’abord, intervenir sur les points de l’application pour chercher où se trouve ces répétitions,
  • ensute, intervenir sur chacune de ces répétitions pour corriger la logique implémentée.

Toutes ces étapes sont d’autant plus couteuses si elles sont réalisées par un développeur qui n’a pas réalisé ces duplications ou qui ne connaît pas assez l’application pour trouver où se trouve ces points de duplications.

Ainsi, d’après Andy Hunt et Dave Thomas, DRY nécessite que:

"Dans un système, toute connaissance doit avoir une représentation unique, 
    non-ambiguë, faisant autorité."

Ne pas dupliquer du code

La première source de duplication de logique provient de la duplication directe de code en copiant-collant des parties de code.

Acquérir certains réflexes permet d’abord d’éviter de dupliquer du code soi-même et ensuite de s’exercer à identifier des duplications quand on lit du code. A part les cas évident, ce n’est pas toujours trivial de remarquer que du code est dupliqué.

Magic string

Les magic strings peuvent être des chaines de caractères qui sont des constantes utilisées directement dans le code:

Par exemple:

public string GetDivisionName(int floor) 
{ 
    if (floor == 0) 
    { 
        return "Accueil"; 
    } 
    else if (floor == 1) 
    { 
        return "Comptabilité"; 
    } 
    else if (floor == 2) 
    { 
        return "Marketing"; 
    } 
    else if (floor == 3) 
    { 
        return "Conception"; 
    } 
    else if (floor == 4) 
    { 
        return "Direction"; 
    } 
    else 
    { 
        throw new InvalidOperationException(string.Format( 
            "L'étage '{0}' n'existe pas.", floor); 
    } 
} 

Les chaînes de caractères “Accueil”, “Comptabilité”, “Marketing”, “Conception” et “Direction” sont des magic strings car elles correspondent à des constantes. Utiliser ce type de constantes dans du code poussent à la duplication.

Par exemple, si on veut faire la fonction symétrique de celle-ci, on peut écrire la fonction suivante en dupliquant les chaînes de caractères:

public int GetFloor(string divisionName) 
{ 
    if (divisionName == "Accueil") 
    { 
        return 0; 
    } 
    else if (divisionName == "Comptabilité") 
    { 
        return 1; 
    } 
    else if (divisionName == "Marketing") 
    { 
        return 2; 
    } 
    else if (divisionName == "Conception") 
    { 
        return 3; 
    } 
    else if (divisionName == "Direction") 
    { 
        return 4; 
    } 
    else 
    { 
        throw new InvalidOperationException(string.Format( 
        "La service '{0}' ne correspond à aucun étage", service); 
    } 
}

Pour éviter cette duplication, on peut, par exemple, ranger les chaines des variables statiques dédiées dans la même classe, dans une autre classe ou même dans un fichier séparé suivant leur portée:

private const string receptionDivisionName = "Accueil"; 
private const string accountingDivisionName = "Comptabilité"; 
private const string marketingDivisionName = "Marketing"; 
private const string conceptionDivisionName = "Conception"; 
private const string directionDivisionName = "Direction"; 
 
public string GetDivisionName(int floor) 
{ 
    if (floor == 0) 
    { 
        return receptionDivisionName; 
    } 
    else if (floor == 1) 
    { 
        return accountingDivisionName; 
    } 
    else if (floor == 2) 
    { 
        return marketingDivisionName; 
    } 
    else if (floor == 3) 
    { 
        return conceptionDivisionName; 
    } 
    else if (floor == 4) 
    { 
        return directionDivisionName; 
    } 
    else 
    { 
        throw new InvalidOperationException(string.Format( 
            "L'étage '{0}' n'existe pas.", floor); 
    } 
} 
 
public int GetFloor(string divisionName) 
{ 
    if (divisionName == receptionDivisionName) 
    { 
        return 0; 
    } 
    else if (divisionName == accountingDivisionName) 
    { 
        return 1; 
    } 
    else if (divisionName == marketingDivisionName) 
    { 
        return 2; 
    } 
    else if (divisionName == conceptionDivisionName) 
    { 
        return 3; 
    } 
    else if (divisionName == directionDivisionName) 
    { 
        return 4; 
    } 
    else 
    { 
        throw new InvalidOperationException(string.Format( 
            "La service '{0}' ne correspond à aucun étage", service); 
    } 
}

Magic number

Les magic numbers sont des constantes qui ont un sens particulier mais ce sens reste caché parce que le code ne le dévoile pas assez facilement.
Dans les fonctions précédentes, les entiers correspondants aux étages correspondent à des magic numbers. On pourrait de la même façon les ranger dans des variables dédiés.

En utilisant un exemple différent:

public string GetMobilePhonePriceRange(decimal price) 
{ 
    if (price < 50.0m) 
    { 
        return "Bon marché"; 
    } 
    else if (price >= 50.0m && price < 130.0m) 
    { 
        return "Moyen de gamme"; 
    } 
    else if (price >= 130.0m && price < 300.0m) 
    { 
        return "Haut de gamme"; 
    } 
    else if (price >= 300.0m) 
    { 
        return "Très haut de gamme"; 
    } 
}

Cette fonction devient:

private const decimal cheapPriceRangeLimit = 50.0m; 
private const decimal midPriceRangeLimit = 130.0m; 
private const decimal highPriceRangeLimit = 300.0m; 
 
public string GetMobilePhonePriceRange(decimal price) 
{ 
    if (price < cheapPriceRangeLimit) 
    { 
        return "Bon marché"; 
    } 
    else if (price >= cheapPriceRangeLimit && price < midPriceRangeLimit) 
    { 
        return "Moyen de gamme"; 
    } 
    else if (price >= midPriceRangeLimit && price < highPriceRangeLimit) 
    { 
        return "Haut de gamme"; 
    } 
    else if (price >= highPriceRangeLimit) 
    { 
        return "Très haut de gamme"; 
    } 
} 

On peut aller plus loin en considérant des gammes de prix:

private const decimal cheapPriceRangeLimit = 50.0m; 
private const decimal midPriceRangeLimit = 130.0m; 
private const decimal highPriceRangeLimit = 300.0m; 
 
private const string cheapPriceLabel = "Bon marché"; 
private const string midPriceLabel = "Moyen de gamme"; 
private const string highPriceLabel = "Haut de gamme"; 
private const string veryHighPriceLabel = "Très haut de gamme"; 
 
private readonly Dictionary<string, Tuple<decimal?, decimal?>> priceRanges = 
    new Dictionary<string, Tuple<decimal?, decimal?>> 
    { 
        {  cheapPriceLabel, new Tuple<decimal?, decimal?>(null, cheapPriceRangeLimit) }, 
        {  midPriceLabel, new Tuple<decimal?, decimal?>(cheapPriceRangeLimit, midPriceRangeLimit) }, 
        {  highPriceLabel, new Tuple<decimal?, decimal?>(midPriceRangeLimit, highPriceRangeLimit) }, 
        {  veryHighPriceLabel, new Tuple<decimal?, decimal?>(highPriceRangeLimit, null) } 
    }; 
 
public string GetMobilePhonePriceRange(decimal price) 
{ 
  return priceRanges.First(r =>  
    { 
      var lowerLimit = r.Value.Item1; 
      var upperLimit = r.Value.Item2; 
      return (!lowerLimit.HasValue && upperLimit.HasValue && price < upperLimit.Value) ¦¦ 
        (lowerLimit.HasValue && upperLimit.HasValue && price >= lowerLimit.Value && price >= upperLimit.Value) ¦¦ 
        (lowerLimit.HasValue && !upperLimit.HasValue && price >= upperLimit.Value); 
    }) 
    .Key; 
}

Duplication de clauses “if”

D’une façon générale la répétition de if est révélateur d’une implémentation qui pourrait être améliorée.

Dans l’exemple utilisé précédemment:

private const string receptionDivisionName = "Accueil"; 
private const string accountingDivisionName = "Comptabilité"; 
private const string marketingDivisionName = "Marketing"; 
private const string conceptionDivisionName = "Conception"; 
private const string directionDivisionName = "Direction"; 
 
private const int mainFloor = 0; 
private const int firstFloor = 1; 
private const int secondFloor = 2; 
private const int thirdFloor = 3; 
private const int fourthFloor = 4; 
 
public string GetDivisionName(int floor) 
{ 
    if (floor == mainFloor) 
    { 
        return receptionDivisionName; 
    } 
    else if (floor == firstFloor) 
    { 
        return accountingDivisionName; 
    } 
    else if (floor == secondFloor) 
    { 
        return marketingDivisionName; 
    } 
    else if (floor == thirdFloor) 
    { 
        return conceptionDivisionName; 
    } 
    else if (floor == fourthFloor) 
    { 
        return directionDivisionName; 
    } 
    else 
    { 
        throw new InvalidOperationException(string.Format( 
            "L'étage '{0}' n'existe pas.", floor); 
    } 
}

Cette fonction comporte beaucoup de répétitions dues aux clauses if. Il est possible de les supprimer facilement:

private readonly Dictionary<int, string> floors = new Dictionary<int, string> 
  { 
    {  mainFloor, receptionDivisionName },  
    {  firstFloor, accountingDivisionName },  
    {  secondFloor, marketingDivisionName },  
    {  thirdFloor, conceptionDivisionName },  
    {  fourthFloor, directionDivisionName }  
  }; 
 
public string GetDivisionName(int floor) 
{ 
    var foundFloor = floors.FirstOrDefault(f => f.Key.Equals(floor)); 
    if (foundFloor == null) 
    { 
        throw new InvalidOperationException(string.Format( 
            "L'étage '{0}' n'existe pas.", floor); 
    } 
 
    return foundFloor.Value; 
}

Duplication de logique

On peut sans s’en rendre compte dupliquer de la logique dans le code.

Par exemple, si on considère les objets suivants:

public class Building 
{ 
  private string Name { get private set; } 
 
  public Building() 
  {} 
} 
 
public class Floor 
{ 
  public string Name { get private set; } 
  public int FloorNumber { get private set; } 
 
  public Floor() 
  {} 
} 
 
public class Branch 
{ 
  public string Name { get private set; } 
 
  public Branch() 
  {} 
} 
 
public class Division 
{ 
  public Building building; 
  public Floor floor; 
  public Branch branch; 
 
  public Division() 
  {} 
 
  public string GetDivisionAddress() 
  { ... } 
}

Pour implémenter la fonction Division.GetDivisionAddress() on peut écrire:

public string GetAddress() 
{ 
  string address = string.Empty; 
  if (this.branch != null) 
  { 
    address += string.Format("Branch: {0}", this.branch.Name); 
  } 
  if (this.building != null) 
  { 
    address += string.Format("Building: {0}", this.building.Name); 
  } 
  if (this.floor != null) 
  { 
    address += string.Format("Floor {0}: {1}", this.floor.FloorNumber, 
      this.floor.Name); 
  } 
  return address; 
}

Si on doit créer un nouvel objet représentant un bureau et qu’on souhaite rajouter une adresse dans cet objet, on devra dupliquer certains éléments du code notamment dans la fonction Desk.GetDeskAddress():

public class Desk 
{ 
  public Building building; 
  public Floor floor; 
  public Branch branch; 
  public string area; 
 
  public Desk() 
  {} 
 
  public string GetDeskAddress() 
  { 
    string address = string.Empty; 
    if (this.branch != null) 
    { 
      address += string.Format("Branch: {0}", this.branch.Name); 
    } 
    if (this.building != null) 
    { 
      address += string.Format("Building: {0}", this.building.Name); 
    } 
    if (this.floor != null) 
    { 
      address += string.Format("Floor {0}: {1}", this.floor.FloorNumber,  
        this.floor.Name); 
    } 
    if (!string.IsNullOrEmpty(this.area)) 
    { 
      address += string.Format("Area: {0}", this.area); 
    } 
 
    return address; 
  } 
}

Ranger du code dans des classes statiques

Dans l’exemple précédent, les fonctions Division.GetAddress() et Desk.GetDivision() sont dupliquées. Une méthode pour éviter cette duplication, consiste à ranger le code dupliqué dans des classes statiques Helper. Pour cette exemple, on pourrait isoler le code en créent la fonction statique suivante:

public static class Helper 
{ 
  public static string GetAddress(Branch branch, Building building, Floor floor) 
  { 
    string address = string.Empty; 
    if (branch != null) 
    { 
      address += string.Format("Branch: {0}", branch.Name); 
    } 
    if (building != null) 
    { 
      address += string.Format("Building: {0}", building.Name); 
    } 
    if (floor != null) 
    { 
      address += string.Format("Floor {0}: {1}", floor.FloorNumber, 
        this.floor.Name); 
    } 
 
    return address; 
  } 
}

Les appels se font dans les classes Division et Desk, de cette façon:

public class Division 
{ 
  // ... 
 
 
  public string GetDivisionAddress() 
  { 
    return Helper.GetAddress(this.branch, this.building, this.floor); 
  } 
} 
 
public class Desk 
{ 
  // ... 
 
  public string GetDeskAddress() 
  { 
    string address = Helper.GetAddress(this.branch, this.building, this.floor); 
    if (!string.IsNullOrEmpty(this.area)) 
    { 
      address += string.Format("Area: {0}", this.area); 
    } 
 
    return address; 
  } 
}

Cette méthode présent l’inconvénient de coupler la classe statique Helper aux classes Division et Desk. D’autre part, les objets métier Branch, Building et Floor n’ont pas la faculté de savoir comment ils affichent leur propre emplacement. Cette faculté se trouve dans des objets différents comme Division et Desk. Ainsi cette implémentation a plusieurs inconvénients:

  • Couplage fort avec un autre objet Helper.
  • Elle donne trop de responsabilité aux objets Division et Desk puisqu’ils ont la faculté d’afficher des adresses concernant d’autres objets, ce qui casse le principe “Single Responsibility” de SOLID.
  • En cas de modification des propriétés des objets Branch, Building ou Floor, le code des classes Division et Desk devra être modifié, ce qui casse le principe “ouvert/fermé” de SOLID: ouvert aux extensions et fermé aux modifications.

Rétablir les responsabilités

Pour rétablir les responsabilités concernant l’affichage de l’adresse, on peut charger un autre objet d’avoir cette responsabilité. Ce nouvel objet sera uniquement chargé d’afficher une adresse:

public class AddressDisplayer 
{ 
  public string GetAddress(Branch branch, Building building, Floor floor,  
    string area = "") 
  { 
    string address = string.Empty; 
    if (branch != null) 
    { 
      address += string.Format("Branch: {0}", branch.Name); 
    } 
    if (building != null) 
    { 
      address += string.Format("Building: {0}", building.Name); 
    } 
    if (floor != null) 
    { 
      address += string.Format("Floor {0}: {1}", floor.FloorNumber, 
        this.floor.Name); 
    } 
    if (!string.IsNullOrEmpty(this.area)) 
    { 
      address += string.Format("Area: {0}", this.area); 
    } 
 
    return address; 
  } 
}

En injectant cette classe dans les classes Division et Desk, on supprime une partie de la responsabilité de l’affichage de l’adresse:

public class Division 
{ 
  public Building building; 
  public Floor floor; 
  public Branch branch; 
 
  public AddressDisplayer addressDisplayer; 
 
  public Division(AddressDisplayer addressDisplayer, Building building, Floor floor, 
    Branch branch) 
  { 
    this.addressDisplayer = addressDisplayer; 
 
    this.building = building; 
    this.branch = branch; 
    this.floor = floor; 
  } 
 
  public string GetDivisionAddress() 
  { 
    return this.addressDisplayer.GetAddress(this.branch, this.building, this.floor); 
  } 
}

De même pour la classe Desk:

public class Desk 
{ 
  public Building building; 
  public Floor floor; 
  public Branch branch; 
  public string area; 
 
  public AddressDisplayer addressDisplayer; 
 
  public Desk(AddressDisplayer addressDisplayer, Building building, Floor floor,
    Branch branch, 
    string area) 
  { 
    this.addressDisplayer = addressDisplayer; 
 
    this.building = building; 
    this.branch = branch; 
    this.floor = floor; 
    this.area = area; 
  } 
 
  public string GetDeskAddress() 
  { 
    return this.addressDisplayer.GetAddress(this.branch, this.building, this.floor,
      this.area); 
  } 
}

Le couplage avec la classe statique Helper est supprimée en effectuant une injection de dépendances dans les classes Division et Desk.

Eviter les couplages forts

Pour diminuer davantage le nouveau couplage avec la classe AddressDisplayer, on peut introduire l’interface IAddressDisplayer. De cette façon la dépendance sera vers une interface plutôt qu’une classe:

public interface IAddressDisplayer 
{ 
  string GetAddress(Branch branch, Building building, Floor floor, string area = ""); 
}

On redéfinit alors AddressDisplayer de cette façon:

public class AddressDisplayer : IAddressDisplayer 
{ 
  public string GetAddress(Branch branch, Building building, Floor floor,
    string area = "") 
  { 
    // ... 
  } 
}

Maintenant il suffit d’injecter l’interface IAddressDisplayer plutôt que la classe AddressDisplayer:

public class Division 
{ 
  public Building building; 
  public Floor floor; 
  public Branch branch; 
 
  public IAddressDisplayer addressDisplayer; 
 
  public Division(IAddressDisplayer addressDisplayer, Building building, Floor floor,
    Branch branch) 
  { 
    this.addressDisplayer = addressDisplayer; 
 
    // ... 
  } 
 
  // ... 
}

Et:

public class Desk 
{ 
  public Building building; 
  public Floor floor; 
  public Branch branch; 
  public string area; 
 
  public IAddressDisplayer addressDisplayer; 
 
  public Desk(IAddressDisplayer addressDisplayer, Building building, Floor floor,
    Branch branch, string area) 
  { 
    this.addressDisplayer = addressDisplayer; 
 
    // ... 
  } 
 
  // ... 
}

Pour avoir plus de détails sur l’injection de dépendances: Injection de dépendances en utilisant Unity en 10 min.

L’injection de dépendances facilite les tests unitaires

Le gros intérêt d’introduire un couplage vers une interface plutôt que la classe, est de faciliter les tests unitaires des classes Division et Desk. Il sera ainsi plus facile d’utiliser un “Mock” pour la classe AddressDisplayer (un “Mock” peut être facilement appliqué sur une interface, sur une classe abstraite ou sur une classe dont les fonctions à “moquer” sont virtuelles).

Introduire une abstraction

Les classes Division et Desk restent très couplées aux objets Building, Branch et Floor. On peut encore diminuer ce couplage en introduisant la classe abstraite Location:

public abstract class Location 
{ 
  public string Name { get private set; } 
  public int AddressRank { get private set; } 
 
  public Location(string name, int addressRank) 
  { 
    this.Name = name; 
    this.AddressRank = addressRank; 
  } 
 
  public abstract string GetAddress(); 
}

On redéfinit les classes Building, Branch et Floor:

public class Building : Location 
{ 
  public Building(string name, 2) 
    : base(name) 
  {} 
 
  public override string GetAddress() 
  { 
    if (!string.IsNullOrEmpty(this.Name)) 
    { 
      return string.Format("Building: {0}", this.Name); 
    } 
 
    return string.Empty; 
  } 
} 
 
public class Floor : Location 
{ 
  public int FloorNumber { get private set; } 
 
  public Floor(string name, int floorNumber) 
    : base(name, 3) 
  { 
    this.FloorNumber = floorNumber; 
  } 
 
  public override string GetAddress() 
  { 
    if (!string.IsNullOrEmpty(this.Name)) 
    { 
      return string.Format("Floor {0}: {1}", this.FloorNumber, 
        this.Name); 
    } 
 
    return string.Empty; 
  } 
} 
 
public class Branch : Location 
{ 
  public Branch(string name) 
    : base(name, 1) 
  {} 
 
  if (!string.IsNullOrEmpty(this.Name)) 
  { 
    return string.Format("Branch: {0}", this.Name); 
  } 
}

On peut ensuite facilement introduire la classe Area:

public class Area : Location 
{ 
  public Area(string name, 4) 
    : base(name) 
  {} 
 
  public override string GetAddress() 
  { 
    if (!string.IsNullOrEmpty(this.Name)) 
    { 
      return string.Format("Desk area: {0}", this.Name); 
    } 
 
    return string.Empty; 
  } 
}

L’héritage permet de supprimer les dépendances des classes Division et Desk:

public class Desk 
{ 
  public readonly List<Location> locations; 
  public readonly IAddressDisplayer addressDisplayer; 
 
  public Desk(List<Location> locations, IAddressDisplayer addressDisplayer) 
  { 
    this.locations = locations; 
    this.addressDisplayer = addressDisplayer; 
  } 
 
  public string GetDeskAddress() 
  { 
    return this.addressDisplayer.GetAddress(this.locations); 
  } 
} 
 
public class Division 
{ 
  public readonly List<Location> locations; 
  public readonly IAddressDisplayer addressDisplayer; 
 
  public Division(List<Location> locations, IAddressDisplayer addressDisplayer) 
  { 
    this.locations = locations; 
    this.addressDisplayer = addressDisplayer; 
  } 
 
  public string GetDivisionAddress() 
  { 
    return this.addressDisplayer.GetAddress(this.locations); 
  } 
}

On rédifinit la classe AddressDisplayer et l’interface IAddressDisplayer:

public interface IAddressDisplayer 
{ 
  string GetAddress(IEnumerable<Location> locations); 
} 
 
public class AddressDisplayer : IAddressDisplayer 
{ 
  public string GetAddress(IEnumerable<Location> locations) 
  { 
    string address = string.Empty; 
    foreach (var location in locations.OrderBy(l => l.AddressRank) 
    { 
      string locationAddress = location.GetAddress(); 
      if (!string.IsNullOrEmpty(locationAddress)) 
      { 
        if (!string.IsNullOrEmpty(address)) 
        { 
          address += " "; 
        } 
        address += locationAddress; 
      } 
    } 
 
    return address; 
  } 
}

Le couplage entre les classes est donc fortement réduit. D’autre part cette implémentation permet de mieux respecter les principes “Single responsability” et “Open/Close” de SOLID (http://cdiese.fr/principe-de-developpement-oriente-objet-solid/).

Démarche Software Craftmanship

La démarche Software Craftmanship et la réalisation de Katas permet de s’exercer pour acquérir des réflexes visant à limiter des duplications du code ou à indiquer des méthodes de “refactoring” pour supprimer du code dupliqué.

Utiliser des “design patterns”

L’utilisation de “design patterns” permet aussi d’éviter les duplications de logique dans plusieurs objets.

Par exemple dans l’exemple précédent, si souhaite instancier des objets de type Division et Desk, il faudra écrire le code:

IAddressDisplayer addressDisplayer = new AddressDisplayer(); 
 
var itDivisionLocations = new List<Location>  
{ 
  new Branch("Paris"), new Building("La Défense"), new Floor(3, "IT") 
} 
 
Division division = new Division(itDivisionLocations, addressDisplayer); 
 
var ceoDeskLocations = new List<Location> 
{ 
  new Branch("Paris"), new Building("La Défense"), new Floor(36, "Direction"), 
  new Area("Chief headquarter"), 
} 
 
Desk ceoDesk = new Desk(ceoDeskLocations, addressDisplayer);

Ce qui signifie qu’à chaque instanciation de Division ou de Desk, il faudra aussi instancier certaines classes avec certaines informations. Pour éviter de dupliquer cette logique, on peut s’aider des “design patterns” Factory ou Abstract Factory.

Dans le cas de Factory, il suffit de déporter cette logique dans une classe, par exemple:

public class PlaceFactory 
{ 
  private readonly IAddressDisplayer addressDisplayer; 
 
  public PlaceFactory() 
  { 
    this.addressDisplayer = new AddressDisplayer(); 
  } 
 
  public Division CreateDivision(string branchName, string building,  
    int floorNumber, string floorName) 
  { 
    var divisionLocations = new List<Location>  
    { 
      new Branch(branchName), new Building(building),  
      new Floor(floorNumber, floorName) 
    } 
    return new Division(divisionLocations, this.addressDisplayer); 
  } 
 
  public Desk CreateDesk(string branchName, string building,  
    int floorNumber, string floorName, string deskArea) 
  { 
    var deskLocations = new List<Location>  
    { 
      new Branch(branchName), new Building(building),  
      new Floor(floorNumber, floorName), new Area(deskArea) 
    } 
    return new Desk(deskLocations, this.addressDisplayer); 
  } 
}

De même pour afficher les adresses d’une division ou d’un desk, il faut faire des appels différents suivants le type de classe:

Division division = ... 
string divisionAddress = division.GetDivisionAddress(); 
 
Desk desk = ... 
string deskAddress = desk.GetDeskAddress();

On peut utiliser le pattern Strategy pour obtenir les adresses sans se préoccuper de l’appel de la méthode suivant le type de la classe.

Par exemple, si on définit les classes suivantes:

public interface IPlaceAddressDisplayerStrategy 
{ 
  string GetPlaceAddress(); 
} 
 
public class DivisionAddressDisplayerStrategy : IPlaceAddressDisplayerStrategy 
{ 
  private readonly Division division; 
 
 
  public DivisionAddressDisplayerStrategy(Division division) 
  { 
    this.division = division; 
  } 
 
  public string GetPlaceAddress() 
  { 
    return this.division.GetDivisionAddress(); 
  } 
} 
 
public class DeskAddressDisplayerStrategy : IPlaceAddressDisplayerStrategy 
{ 
  private readonly Desk desk; 
 
 
  public DeskAddressDisplayerStrategy(Desk desk) 
  { 
    this.desk = desk; 
  } 
 
  public string GetPlaceAddress() 
  { 
    return this.desk.GetDeskAddress(); 
  } 
}

Si une classe doit afficher des adresses mais ne doit pas se préoccuper du type des objets qui détiennent une adresse, il suffit qu’elle consomme une classe stratégie, dans l’exemple il s’agira d’une classe satisfaisant IPlaceAddressDisplayer.

Par exemple, si on considère la classe AddressShower:

public class AddressShower 
{ 
  public AddressShower() 
  {} 
 
  public void ShowAddress(IPlaceAddressDisplayerStrategy addressDisplayer) 
  { 
    Console.WriteLine(addressDisplayer.GetPlaceAddress()); 
  } 
}

Cette classe ignore le type des objets Division et Desk et n’a pas besoin de les connaître pour afficher une adresse.
AddressShower peut afficher une adresse de cette façon:

AddressShower addressShower = new AddressShower(); 
 
Division division = ... 
var divisionAddressDisplayerStrategy = new DivisionAddressDisplayerStrategy(division); 
addressShower.ShowAddress(divisionAddressDisplayerStrategy); 
 
Desk desk = ... 
var deskAddressDisplayerStrategy = new DeskAddressDisplayerStrategy(desk); 
addressShower.ShowAddress(deskAddressDisplayerStrategy);

Ainsi le pattern Strategy sert aussi éviter des duplications dans le code puisqu’il va permettre d’implémenter la logique d’appel à la bonne méthode suivant le type de classe dans un seul type d’objets.

Automatiser des processus

Le principe DRY ne vise pas seulement à éviter du dupliquer du code, il concerne aussi des processus au sens large. Ainsi ces processus comprennent le fait d’éviter d’effectuer des tâches répétitives.

Test unitaires

Les tests unitaires participent fortement à réduire les tâches répétitives puisqu’ils vont permettre d’appliquer des tests de façon automatique et éviter de les effectuer manuellement.
Des outils comme MsTest, NUnit, moq ou NSubstitute aident à l’exécution de test unitaires.

Intégration continue

Executer des tests unitaires, des tests d’intégration de façon périodique et notifier les développeurs des éventuelles erreurs liées à ces tests permet aussi de réduire les tâches répétitives.
Des outils comme Jenkins ou Teamcity permet d’apporter une automatisation de toutes ces étapes.

Générateurs de code

On peut aussi utiliser des générateurs de code pour s’éviter de dupliquer des objets à la main. Par exemple on peut utiliser la génération T4 de Visual Studio (T4 pour Text Template Transformation Toolkit).

S’aider d’outils pour éviter les duplications

Enfin il existe des outils pour indiquer quelles sont les parties du code qui sont dupliquées. Ce type d’outils permet de traquer les répétitions dans le code, elles seront moins efficaces pour trouver les répétitions dans la logique.
Des outils comme SonarQube permettent d’analyser le code et de donner des indicateurs quant aux duplications du code.

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