Comprendre .NET Standard en 5 min

Les technologies Microsoft adressent un grand nombre de plateformes différentes allant de systèmes d’exploitation comme Windows à des appareils mobiles comme les tablettes. D’autres parts, depuis quelques années, Satya Nadella a impulsé une “ouverture” des technologies Microsoft vers d’autres plateformes que Windows. Cette ouverture a encore augmenté le nombre de plateformes sur lesquelles des technologies pouvaient s’exécuter: Linux, macOS, iOS, Android etc… Toutes ces plateformes ont des spécificités qui nécessitent, pour chacune d’entre elles, une implémentation particulière.

Cet article a pour but d’expliquer l’intérêt du .NET Standard par rapport aux autres approches de Microsoft pour rendre du code Microsoft plus générique et déployable sur plusieurs plateformes.

Explications

Pour Microsoft, un premier challenge a été de tenter d’uniformiser ses bibliothèques pour les rendre moins spécifiques à une plateforme. Ce premier travail a abouti à 3 grandes familles de technologies:

  • .NET Framework déployable sur des machines Windows,
  • .NET Core déployable sur Windows, Linux, macOS mais aussi sur des tablettes, smartphones et Xbox avec Universal Windows Platform (UWP).
  • Xamarin déployable sur Android et iOS.

Cette uniformisation ne permet pas, à elle-seule, d’utiliser un même code pour adresser plusieurs plateformes. Si on développe une bibliothèque, on est obligé d’avoir un projet spécifique pour chaque plateforme et il est nécessaire de compiler tout son code pour chacune des plateformes.

Portable class library (PCL)

Une première tentative pour résoudre ce problème a été d’introduire les Portable Class Library (PCL). Si on sélectionne ce type de projet dans Visual Studio, on peut créer une bibliothèque que l’on pourra déployer sur plusieurs plateformes. En sélectionnant différentes plateform targets (comme .NET Framework, Windows Phone, Xamarin…) dans Visual Studio, on est capable de compiler un même code pour plusieurs plateformes.

Ainsi après compilation, on obtiendra un répertoire différent pour chaque plateforme sélectionnée.

L’approche des PCL rend plus facile le déploiement de bibliothèques sur des plateformes différentes, toutefois elle présente quelques inconvénients:

  • Plus le nombre de plateformes augmente et plus il faudra de compilation spécifiques pour chacune de ces plateformes. Il y aura plus de répertoires de sortie concernant ces plateformes ce qui augmente considérablement la complexité de ce type d’approche car il y a presqu’une dizaine de plateformes possibles (.NET Framework, Silverlight, Windows 8.0, Windows Store Apps, Windows Phone 8.1, Xamarin.iOS, Xamarin.Android).
  • Toutes les API ne sont pas disponibles pour toutes les plateformes. Suivant la plateforme sur laquelle on souhaite déployer, certaines fonctionnalités .NET peuvent ne pas être disponibles.
  • Si on ajoute une nouvelle plateforme, il faudra recompiler la bibliothèque pour qu’elle soit disponible pour cette plateforme.

.NET Standard

Pour éviter de généraliser la démarche des PCL pour un grand nombre de plateformes différentes et pour encapsuler la complexité d’un déploiement sur plusieurs plateformes, la 2e approche de Microsoft a été d’introduire une abstraction supplémentaire avec le .NET Standard.

.NET Standard permet d’introduire une couche supplémentaire entre le code et les plateformes où seront déployées les bibliothèques:

  • Une bibliothèque .NET Standard définit un ensemble d’API qui sont communes à plusieurs plateformes.
  • Une bibliothèque .NET Standard n’est pas liée à une plateforme. Ainsi faire une bibliothèque se baser sur une version de .NET Standard permet d’éviter de la faire se baser sur une plateforme particulière. Il n’y a donc, plus de lien entre la bibliothèque et la plateforme sur laquelle elle sera déployée.
    Cette caractéristique permet d’éviter d’écrire du code pour une plateforme spécifique. Le code de la bibliothèque est écrit pour une version du .NET Standard.
  • La couche supplémentaire qu’est .NET Standard rend plus facile la mise à jour éventuelle d’une plateforme. Au lieu de compiler une bibliothèque pour une version spécifique de la plateforme, on la compile pour une version du .NET Standard.
    Si la nouvelle version d’une plateforme est compatible avec le .NET Standard sur lequel se base des bibliothèques, on peut mettre à jour la plateforme sans craindre une incompatibilité de ces bibliothèques. En effet c’est le .NET Standard qui va garantir la compatibilité.

Différences entre une bibliothèque et un exécutable

L’approche .NET Standard ne convient pas dans tous les cas. Elle permet de faciliter le déploiement de bibliothèques en ajoutant une abstraction de façon à éviter de baser ces bibliothèques directement sur des plateformes. Cette approche est possible car dans la majorité des cas, une bibliothèques de classes n’utilisent pas d’API spécifiques à une plateforme donnée.
En revanche, si une bibliothèque utilise des API trop spécifiques à une plateforme (comme par exemple, WPF qui nécessite un système Windows), elle ne pourra pas se baser sur .NET Standard (.NET Standard ne comporte pas de classes WPF).
De la même façon, un exécutable est spécifique à une plateforme. On ne pourra pas baser une exécutable sur un .NET Standard. Un exécutable est implémenté pour une plateforme précise.

Pour rendre l’approche .NET Standard le plus efficace possible, il faut donc placer un maximum de code dans des bibliothèques qui se basent .NET Standard. Le reste du code, étant plus spécifique, se basera sur une plateforme précise.

Pour résumer, on a donc 2 approches possibles:

  • Une bibliothèque de classes,
  • Un exécutable.

Cas d’une bibliothèque de classes

On peut donc implémenter une bibliothèque de classes .NET Standard en se basant sur:

  • Le .NET Standard qui se base sur des assemblies d’un framework (par exemple .NET Framework ou .NET Core),
  • Eventuellement une autre bibliothèque de classes .NET Standard,
  • Eventuellement un bibliothèque PCL.

Cas d’un exécutable

Un exécutable est plus spécifique à une plateforme, il se base donc directement sur les assemblies d’un framework (par exemple .NET Framework ou .NET Core). Il peut aussi éventuellement avoir des dépendances vers une bibliothèque de classes .NET Standard (qui elle-même se base sur le .NET Standard).

Dépendances à partir de .NET Standard 2.0

A partir de .NET Standard 2.0, une bibliothèque de classes basée sur .NET Standard pourra référencer des assemblies spécifiques au Framework .NET en plus des références vers d’autres bibliothèques .NET Standard. Ces référencements seront possibles à l’aide de compatibility shims. Plus de détails sont disponibles sur .NET Standard Assembly Unification.

Plateforme cible

Comme indiquer plus haut, .NET Standard permet d’indiquer un ensemble d’API utilisables par une bibliothèque de classes. Toutefois, il n’indique pas directement la plateforme cible sur laquelle la bibliothèque sera déployée. Toutes les versions de .NET Standard ne sont pas compatibles avec toutes les plateformes et toutes les versions de plateformes.

Versions de .NET Standard

Ainsi, une plateforme est compatible avec une version précise de .NET Standard. Si on veut déployer une même bibliothèque sur plusieurs plateformes, il faut choisir la version de .NET Standard qui le permet.

On peut résumer la compatibilité entre les frameworks et les versions de .NET Standard dans le tableau suivant:

(1): ces compatiblités sont valables pour le CLI 1.x mais ne seront plus valable quand CLI 2.x sortira.

Quelques remarques sur la version de .NET Standard:

  • Plus la version de .NET Standard augmente et plus des API et des fonctionnalités sont rajoutées au standard. Si une plateforme respecte .NET Standard 1.6 alors elle respecte toutes les versions précédant la 1.6 (de 1.0 à 1.6).
  • Plus la version de .NET est basse et plus de frameworks sont supportés, toutefois plus la version de .NET Standard est basse et moins il y a de fonctionnalités.
Confusions liées à .NET Standard 2.0 (pas de breaking changes)

Il y a eu une grande confusion avec .NET Standard 2.0 car Microsoft a changé son approche entre 2 pre-releases.

1ère approche (valable pour le CLI 1.x):
Dans un premier temps, Microsoft avait envisagé un breaking change entre .NET Standard 1.6 et 2.0. De la version 1.0 à la version 1.6, chaque incrément de version correspond à l’ajout de nouveau élément dans le standard. Pour passer de la version 1.6 à 2.0 de .NET Standard, Microsoft voulait enlever des éléments d’API. La version 2.0 comportait donc moins d’éléments que la version 1.6.

Cette suppression d’éléments entraînait un “breaking change” entre la version 1.6 et 2.0. Ainsi, une bibliothèque de classes compatible avec la version 2.0 n’était pas compatible avec la version 1.6 et 1.5.

De même, le Framework .NET 4.6.1 était compatible de .NET Standard 1.0 à 1.4 puis était compatible avec .NET Standard 2.0. En revanche il n’était pas compatible avec .NET Standard 1.5 et 1.6.
Cette approche est seulement valable pour la version du CLI (Command Line Interface) 1.x.

2e approche (valable pour le CLI 2.x):
Dorénavant, il n’y a pas de breaking changes entre .NET Standard 1.6 et 2.0. La version 2.0 de .NET Standard comporte plus d’éléments d’API que la version 1.6. Ainsi, une bibliothèque de classes compatible avec .NET Standard 2.0 est compatible de 1.0 à 2.0.

De même le Framework .NET 4.6.1 est compatible du .NET Standard 1.0 à 2.0.
A la sortie de la version 2.x du CLI (Command Line Interface), cette approche sera définitive.

Pour avoir la liste plus complête de toutes les plateformes, on peut se reporter à la documentation de .NET Standard sur Github.

Fonctionnalités de .NET Standard

Comme indiqué plus haut, plus la version de .NET Standard augmente et plus le standard comporte de fonctionnalités. Le choix de la version de .NET Standard sur laquelle on va baser une bibliothèque de classes doit se faire suivant les fonctionnalités disponibles.

Les fonctionnalités présentent dans .NET Standard sont les suivantes:

  • .NET Standard 1.0:
    • types primitifs,
    • Reflection dans System.Reflection,
    • Task Parallel Library (TPL) dans System.Threading.Tasks,
    • Collections dans System.Collections,
    • Linq dans System.Linq.
  • .NET Standard 1.1:
    • Les éléments du .NET Standard 1.0,
    • Les structures de collections concurrentes dans System.Collections.Concurrent,
    • Les services d’interopérabilité COM et platform invoke dans System.Runtime.InteropServices,
    • HttpClient dans System.Net.Client.
  • .NET Standard 1.2:
    • Les éléments du .NET Standard 1.1,
    • La classe Timer dans System.Threading,
    • Ajout du paramétrage LargeObjectHeapCompactMode pour le garbage collector dans System.Runtime.
  • .NET Standard 1.3:
    • Les éléments du .NET Standard 1.2,
    • Les classes pour s’interfacer avec le système de fichiers comme File ou Directory dans System.IO,
    • S’interfacer avec une Console avec la classe Console dans System,
    • Gestion des sockets dans System.Net,
    • Ajout d’éléments pour s’interfacer avec l’environnement avec la classe Envionment dans System,
    • Ajout de la classe AsyncLocal dans System.Threading,
    • La classe StringBuilder dans System.Text.
  • .NET Standard 1.4:
    • Les éléments du .NET Standard 1.3,
    • Ajout de l’algorithme ECDSA dans la classe ECDsa dans System.Security.Cryptography.
  • .NET Standard 1.5:
    • Les éléments du .NET Standard 1.4,
    • Des éléments liés à la Reflection dans la classe Activator dans System et ajout de la classe Assembly, Module dans System.Reflection.
    • Ajout de la classe BufferedStream dans System.IO,
  • .NET Standard 1.6:
    • Les éléments du .NET Standard 1.5,
    • Possibilité de compiler les Expression Trees dans la classe Expression dans System.Linq.Expressions.
    • Ajout de la classe ECCurve dans System.Security.Cryptography,
    • Ajout de plus de fonctionnalités dans la classe Regex dans System.Text.RegularExpressions.

Pour .NET Standard 2.0, on peut avoir une vue d’ensemble des fonctionnalités et des API qui sont prises en compte:

On peut avoir une liste exhaustive des fonctionnalités par version de .NET Standard en allant sur: https://github.com/dotnet/standard/tree/master/docs/versions.

.NET API Browser

La disponibilité de classes dépends de la version et de la plateforme cible. Etant donné la multitude de plateformes cible, on peut avoir des difficultés à savoir si une classe existe pour la version de Framework utilisée et pour la plateforme sur laquelle on va déployer.

Pour rendre plus facile cette vérification, Microsoft a mis en place le .NET API Browser.

On peut avoir une documentation précise des classes utilisables en fonction de la plateforme sur le .NET API Browser.

Support des fonctionnalités suivant la plateforme

On peut penser que si une classe est présente pour une version donnée de .NET Standard alors toutes les plateformes supporteront l’implémentation de cette classe. C’est vrai la plupart du temps mais pas dans tous les cas.

Certaines classes sont spécifiques à une plateforme. Ainsi pour assurer une homogénéité des classes définies dans .NET Standard, Microsoft peut les avoir inclus alors qu’elles ne s’appliquent pas pour certaines plateformes. Plusieurs comportements sont possibles, suivants les cas, Microsoft implémente 2 approches:

  • Lancer une exception de type PlatformNotSupportedException à l’exécution quand une classe est incompatible avec la plateforme sur laquelle elle est exécutée.
  • Emuler le comportement de la classe même si la plateforme n’est pas adaptée: par exemple il est possible d’émuler l’accès à la base de registres sur d’autres plateformes que Windows.
Il faut tester les fonctionnalités suivant toutes les plateformes

Certaines classes de .NET Standard peuvent être spécifiques pour certaines plateformes. Ainsi si on base une bibliothèque sur une version de .NET Standard comprenant ces classes, la compilation peut réussir alors qu’à l’exécution, l’utilisation de ces classes peut mener à une exception de type PlatformNotSupportedException. On est donc pas sûr, après compilation, d’avoir un code exécutable sur toutes les plateformes.

Il faut donc appliquer des tests qui s’exécuteront pour les plateformes sur lesquelles ont veut déployer de façon à être sûr de ne pas déclencher des exceptions à l’exécution de certaines fonctionnalités.

Comprendre le fonctionnement de .NET Standard avec NuGet

Comme indiquer plus haut, .NET Standard permet d’encapsuler la complexité de la gestion de plusieurs plateformes en ajoutant une abstraction. Des bibliothèques de classes peuvent se baser sur un .NET Standard et éviter ainsi de se baser sur une plateforme spécifique.

On a l’impression que les mêmes fichiers seront utilisés pour toutes les plateformes. A vrai dire c’est impossible puisque les plateformes sont trop hétérogènes:

  • Pour une bibliothèque de classes .NET Standard lors du développement: les projets de ce type contiennent des références vers une ou plusieurs assemblies contenues dans des packages NuGet de .NET Standard. Ces packages ne contiennent pas vraiment des assemblies avec une implémentation. Ce sont, soit des métapackages (qui référencent d’autres packages NuGet), soit des packages contenant des assemblies avec des fonctions sans implémentation. Le but de ces assemblies est de simplement permettre la compilation. Elles ne seront pas utilisées à l’exécution.
  • Pour un exécutable consommant une bibliothèque de classes .NET Standard: l’exécutable doit s’exécuter sur une plateforme spécifique. Ainsi, lors de l’exécution de tests ou en débug ce sont des assemblies correspondant à la plateforme de développement qui sont utilisées. De même, à l’exécution de l’exécutable sur une plateforme spécifique, ce sont les assemblies contenant du code adaptés à la plateforme qui seront utilisées.

Il y a donc une logique qui associe certaines assemblies suivant la plateforme sur laquelle sera déployée une bibliothèque de classes .NET Standard. Cette logique est gérée par le compilateur et la plateforme de développement quel que soit le framework utilisé au déploiement (Framework .NET, .NET Core etc…).

Le fonctionnement de .NET Standard est différent entre les versions 1.x et 2.x:

  • Pour les versions 1.x: .NET Standard est consommé sous forme d’un “meta-package” NuGet permettant de télécharger d’autres packages plus spécifiques à une plateforme.
  • Pour les versions 2.x: le contenu du package est différent est différent, ils contiennent une assembly monolitique netstandard.dll.

Pour les versions 1.x

Quand on base une bibliothèque de classes sur .NET Standard 1.x, on consomme un package NuGet appelé NETStandard.Library.

Ce package est un “meta-package” qui ne contient aucune assembly. Il permet seulement de télécharger d’autres packages.

Si on regarde le contenu du package .NETStandard.Library 1.6.1:

  1. Télécharger .NETStandard.Library 1.6.1 sur NuGet.org:
  2. Renommer le fichier de .nupkg à .zip
  3. Décompresser le package pour en voir le contenu

En regardant le contenu du répertoire du package, on se rends compte qu’il n’y aucune assembly. Si on lit le fichier NETStandard.Library.nuspec: on trouve des informations relatives au .NET Standard 1.0, .NET Standard 1.1, .NET Standard 1.2 et .NET Standard 1.3:

<?xml version="1.0" encoding="utf-8"?> 
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd"> 
  <metadata minClientVersion="2.12"> 
    <id>NETStandard.Library</id> 
    <version>1.6.1</version> 
    <title>NETStandard.Library</title> 
    <!-- ... --> 
    <dependencies> 
      <group targetFramework=".NETStandard1.0"> 
        <!-- ... --> 
      </group> 
      <group targetFramework=".NETStandard1.1"> 
        <!-- ... --> 
      </group> 
      <group targetFramework=".NETStandard1.2"> 
        <!-- ... --> 
      </group> 
      <group targetFramework=".NETStandard1.3"> 
        <!-- ... --> 
      </group> 
    </dependencies> 
  </metadata> 
</package>

Il n’y a pas d’informations relatives aux .NET Standard suivants. Ceci doit s’expliquer par le fait que Microsoft a changé d’approche pour le .NET Standard 2.0 et est revenu sur les packages livrés pour .NET Standard 1.5 et .NET Standard 1.6.

Si on regarde le nœud pour le .NET Standard 1.3:

<group targetFramework=".NETStandard1.3"> 
  <dependency id="Microsoft.NETCore.Platforms" version="1.1.0" /> 
  <dependency id="Microsoft.Win32.Primitives" version="4.3.0" /> 
  <dependency id="System.AppContext" version="4.3.0" /> 
  <dependency id="System.Collections" version="4.3.0" /> 
  <dependency id="System.Collections.Concurrent" version="4.3.0" /> 
  <!-- ... --> 
  <dependency id="System.Security.Cryptography.Algorithms" version="4.3.0" /> 
  <dependency id="System.Security.Cryptography.Encoding" version="4.3.0" /> 
  <dependency id="System.Security.Cryptography.Primitives" version="4.3.0" /> 
  <dependency id="System.Security.Cryptography.X509Certificates" version="4.3.0" /> 
</group>
2 types de dépendances existent pour les frameworks:
  • Des dépendances non spécifiques vers des bibliothèques d’un framework: par exemple pour .NET Core, une partie de ces bibliothèques est fournie sous forme d’assemblies à l’installation de .NET Core. Les assemblies fournies de cette façon sont communes à plusieurs plateformes. Par exemple, les assemblies System.Collections et System.Linq ne sont pas spécifiques à une plateforme.
  • Des dépendances vers des bibliothèques d’un framework spécifiques à une plateforme: par exemple, les assemblies System.IO et System.Security.Cryptography.Algotithms sont spécifiques à une plateforme.

    Dans le cas du Framework .NET, toutes les bibliothèques sont dépendantes de la plateforme puisque le Framework .NET n’est déployable que sur un système Windows. D’autre part, toutes les bibliothèques du Framework .NET sont fournies sous la forme d’assemblies qui sont livrées avec le Framework .NET à son installation.

Pour plus de détails sur .NET Core, voir la page suivante: https://docs.microsoft.com/en-us/dotnet/articles/core/.

Dans le fichier NETStandard.Library.nuspec, on considère les dépendances aux packages suivants:

  • Microsoft.NETCore.Platforms,
  • System.Collections et
  • System.Security.Cryptography.Algorithms

Le package NETStandard.Library ne contient pas d’assemblies.

Microsoft.NETCore.Platforms

Si on télécharge le package Microsoft.NETCore.Platforms en version 1.1.0:

En regardant le contenu, on s’aperçoit qu’il n’y a pas non plus d’assemblies. Par contre 3 fichiers sont importants:

  • Microsoft.NETCore.Platforms.nuspec: il contient la description du package.
  • Dans lib\netstandard1.0\_._: ce fichier est vide.
  • Runtime.json: ce fichier contient des Runtime IDentifiers (RID) qui permettent d’identifier les systèmes d’exploitation cibles sur lesquels une application sera exécutée. Par exemple, pour le système Windows 10 en 64 bits, on peut lire la ligne suivante:
            "win10-x64": { 
                "#import": [ "win10", "win81-x64" ]
    

    Ce fichier indique que NuGet peut restaurer les packages ayant besoin de win10 et win81-x64. Par suite, pour win10, NuGet peut restaurer des packages ayant besoin de win81 et ainsi de suite.

    L’arbre de dépendances est:

    • win10-x64 => win10 => win81 => win8 => win7 => win => any => base et
    • Win10-x64 => win81-x64 => win8-x64 => win7-x64 => win-x64 => any => base.

Pour plus de détails sur le RID: Catalogue d’identificateurs de runtime (RID) .NET Core.

Il permet d’indiquer quelles sont les RID à utiliser pour une plateforme cible donnée.

System.Collections

Si on regarde le package System.Collections en version 4.3.0, il contient:

  • System.Collections.nuspec: ce fichier indique d’autres packages suivant la plateforme cible choisie:
    <?xml version="1.0" encoding="utf-8"?> 
    <package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd"> 
      <metadata minClientVersion="2.12"> 
        <id>System.Collections</id> 
        <version>4.3.0</version> 
        <title>System.Collections</title> 
        <! -- ... --> 
        <dependencies> 
          <! -- ... --> 
          <group targetFramework=".NETStandard1.3"> 
            <dependency id="Microsoft.NETCore.Platforms" version="1.1.0" /> 
            <dependency id="Microsoft.NETCore.Targets" version="1.1.0" /> 
            <dependency id="System.Runtime" version="4.3.0" /> 
          </group> 
          <! -- ... --> 
      </metadata> 
    </package>
    
  • Dans le répertoire lib: on trouve d’autres répertoires suivant les plateformes cibles. Mais ces répertoires contiennent des fichiers vides nommés _._.
  • Dans le répertoire ref: se trouve quelques assemblies suivant les plateformes cibles mais l’implémentation de ces assemblies est vide.
    • Si on prends le répertoire ref\netstandard1.0: on trouve l’assembly System.Collections.dll, toutefois en regardant avec DotPeek, il n’y a pas d’implémentation dans le corps des fonctions.
    • Si on prends le répertoire ref\net45: on trouve encore des fichiers vides nommés _._.

Il n’y pas réellement d’assemblies avec une implémentation dans ce package. Ceci s’explique par le fait que System.Collections n’est pas spécifique à une plateforme. L’implémentation correspondant à ce namespace est fournie par les frameworks. Il n’est donc pas nécessaire de fournir une implémentation particulière.

System.Security.Cryptography.Algorithms

Si on regarde le contenu de System.Security.Cryptography.Algorithms en version 4.3.0:

  • System.Security.Cryptography.Algorithms.nuspec contient d’autres indications sur des dépendances de packages NuGet.
  • Les répertoires lib et ref contiennent d’autres répertoires pour les plateformes cibles. Ces répertoires contiennent une assembly quand le framework ne fournit pas lui-même une implémentation.
  • Le répertoire runtime contient des répertoires désignant un contenu spécifique à un runtime indiqué par le package Microsoft.NETCore.Platforms. Par chaque runtime, il y a aussi une assembly spécifique.

Contrairement à System.Collection, le package System.Security.Cryptography.Algorithms contient des assemblies car il fournit une implémentation spécifique pour chaque plateforme.

Si on prends la référence de package Microsoft.NETCore.Targets dans le fichier System.Collections.nuspec en continuant l’exploration.

En résumé

Pour les versions 1.x, le .NET Standard est composé d’une multitude de packages NuGet contenant:

  • D’autres dépendances vers des packages NuGet,
  • Des fichiers vides lorsque l’implémentation n’est pas nécessaire parce-qu’elle est fournie par un framework (Framework .NET, .NET Core etc…).
  • Des assemblies spécifiques à une plateforme lorsque c’est nécessaire.

Ainsi, si une bibliothèque de classes se base sur .NET Standard, elle n’a pas de dépendances directes vers des packages et par suite des assemblies. Ces dépendances sont indiquées par le package .NET Standard suivant la ou les plateformes cibles.

Lien entre .NET Standard et .NET Core

Un des inconvénients de cette approche est le lien entre les packages NuGet de .NET Core et .NET Standard. En fait pour une bibliothèque de classes basée sur .NET Standard, les packages non spécifiques à une plateforme utilisés proviennent de .NET Core. Le package NuGet .NETStandard.Library permet d’implémenter ce lien entre la bibliothèque de classes et les packages .NET Core.

C’est en partie pour casser ce lien qu’une approche différente a été utilisée pour les versions 2.x de .NET Standard.

Pour les versions 2.x

Il n’y a pas encore de version stable de .NET Standard 2.0 et Microsoft a plusieurs fois changé son approche pour les packages correspondant à cette version.

Microsoft a voulu casser la complexité de la gestion de packages pour les versions 1.x en fournissant un package contenant tout le .NET Standard sans avoir une multitude de packages NuGet comme pour les versions 1.x.

L’approche actuelle utilise plusieurs redirections de type (i.e. type forwarding).

Si on regarde le contenu du package NETStandard.Library 2.0:

  • Une assembly nommée netstandard.dll qui contient tous les types du .NET Standard. Ce fichier se trouve dans build\netstandard2.0\ref. L’assembly netstandard.dll ne contient aucune implémentation mais seulement avec des redirections de types vers des assemblies des frameworks.
  • Des assemblies qui se trouvent dans build\netstandard2.0\ref qui correspondent à des types utilisés par le .NET Standard sans implémentation contenant des redirections de type vers l’assembly netstandard.dll.

Les assemblies qui contiennent ces redirections de types sont des assemblies système comme mscorlib.dll, System.Runtime.x et d’autres assemblies non spécifiques à une plateforme.

Par exemple, en regardant le contenu de mscorlib.dll (qui se trouve dans le package) avec DotPeek, on voit qu’elle ne contient que des redirections vers netstandard.dll:

Redirection de type (Type forwarding)

Ce mécanisme fonctionne en utilisant l’attribut TypeForwardedTo (System.Runtime.CompilerServices.TypeForwardedToAttribute) qui permet de mapper un type dans une assembly source vers un type dans une autre assembly.

Ce mécanisme permet de créer des assemblies avec seulement des types contenant des type forward vers d’autres assemblies, rendant ainsi transparent l’utilisation d’un type en fonction d’une plateforme. Ce sont les outils de développement et le compilateur qui prennent en compte les attributs TypeForwardedTo pour savoir quelle assembly utiliser.

Pour comprendre l’utilisation de la redirection de type avec l’assembly netstandard.dll, il y a plusieurs cas de figure:

  • Une bibliothèque de classes utilisant d’autres bibliothèques .NET Standard
  • Un exécutable qui consomme des bibliothèques de classes .NET Standard.

Une bibliothèque de classes .NET Standard

Dans ce cas, la bibliothèque de classes .NET Standard a une dépendance vers:

  • L’assembly netstandard.dll qui se trouve dans le package NuGet NETStandard.Library
  • Eventuellement des assemblies qui ont elles-mêmes des dépendances vers les assemblies d’un framework (Framework .NET, .NET Core, etc…).

Comme indiqué plus haut, le package NETStandard.Library contient des assemblies avec les mêmes types que le framework mais sans implémentation. Ces assemblies font de la redirection de type vers l’assembly netstandard.dll.

Un exécutable consommant une bibliothèque .NET Standard

Dans le cas d’un exécutable qui consomme une bibliothèque .NET Standard, les dépendances sont:

  • Les assemblies du framework puisque comme indiqué plus haut, un exécutable est spécifique à une plateforme et ne peut pas uniquement se baser sur le .NET Standard.
  • La bibliothèque de classes .NET Standard qui elle-même a une dépendance vers l’assembly netstandard.dll (le cas de cette bibliothèque correspond au cas précédent).
  • Eventuellement des assemblies ayant des dépendances vers les assemblies du framework.

Par suite l’assembly netstandard.dll effectue des redirections de types vers les assemblies spécifiques d’un framework.

Compatibilité entre la version 2.x et les versions 1.x

Dans le cas où on se base sur le .NET Standard 2.0 et qu’on utilise des packages.NET Standard 1.x, on peut se retrouver avec plusieurs assemblies avec le même nom: des assemblies provenant du .NET Standard 1.x et les assemblies contenant les type forward. Pour résoudre les éventuels problèmes de conflits, une adaptation a été nécessaire dans MSBuild pour résoudre les conflits.

Il est possible que Microsoft change encore son approche pour la version définitive de .NET Standard 2.0. Pour davantage d’informations sur ce sujet: Packaging for .NET Standard 2.0.

En résumé

Dans la même façon que pour les versions 2.x du .NET Standard, la redirection de type permet d’avoir des bibliothèques de classes qui se basent seulement sur le .NET Standard et n’ont aucune dépendance directe vers des assemblies spécifiques d’un framework.
D’autres part, par rapport aux versions 1.x, le gros avantage de la redirection de type est qu’elle permet d’éviter d’avoir beaucoup de packages NuGet quasiment vides pour la plupart des types non spécifiques à une plateforme. L’utilisation du .NET Standard devient plus directe puisqu’elle se base, en grande partie, sur une seule assembly netstandard.dll.

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

NuGet en 5 min

NuGet est un outil facilitant le téléchargement de dépendances externes. Ces dépendances peuvent être téléchargées et ajoutées à un projet Visual Studio ou indépendamment de l’IDE, directement dans un répertoire.
NuGet peut aussi créer et uploader des packages sur un repository.

La documentation de NuGet est plutôt bien faite:

Le but de cet article n’est pas de paraphraser la documentation mais d’être un aide-mémoire sur les principales fonctionnalités de NuGet.

Installation de Nuget

Dans Visual Studio

Pour les versions précédant Visual Studio 2012, il est nécessaire d’installer NuGet après avoir installé Visual Studio: Install Nuget.

A partir de Visual Studio 2012, NuGet est livré avec l’IDE. On peut vérifier la version en cliquant sur le menu Help puis About Microsoft Visual Studio. Il faut ensuite regarder la version de NuGet Package Manager.

On peut toutefois mettre à jour la version livrée en faisant:

  1. Cliquer dans le menu sur Tools puis sur Extensions and Updates
  2. Dans la boite de dialogue, sélectionner l’onglet Tab
  3. Sélectionner Visual Studio Gallery
  4. Sélectionner NuGet Package Manager for Visual Studio
  5. Cliquer sur Update.

Voici les versions du NuGet livrées avec Visual Studio:

Visual Studio 2012 (11.0) NuGet 2.0
Visual Studio 2013 (12.0) NuGet 2.7
Visual Studio 2015 (14.0) NuGet 2.8.6
Visual Studio 2017 (15.0) NuGet 4.0.0
NuGet 4.1.0

La version inclue de NuGet dans Visual Studio n’est accessible qu’à partir du Package Manager ou du Package Manager Console.

Téléchargement direct de nuget.exe

Nuget.exe peut être directement téléchargé sur nuget.org/downloads.

NuGet à la ligne de commandes

Il est possible d’utiliser NuGet à la ligne de commandes sans Visual Studio. On peut le télécharger directement sur: NuGet CLI

Le fichier téléchargé possède une extension .nupkg. NuGet.exe se trouve dans ce package:

  1. Modifier l’extension .nupkg en .zip
  2. Décompresser l’archive zip.
  3. NuGet.exe se trouve dans [Nom du package]/tools/NuGet.exe

En ajoutant le répertoire où se trouve NuGet.exe dans la variable d’environnement PATH, on peut utiliser facilement NuGet.exe dans une invite de commandes.

Avec Chocolatey

Il est possible d’installer l’exécutable NuGet.exe en utilisant Chocolatey. L’intérêt de cette méthode est d’avoir une procédure facilement scriptable dans un script de déploiement (avec Puppet, Ansible ou Chef).

Après avoir installé Chocolatey (https://chocolatey.org/install), il suffit de taper la commande suivante pour installer Nuget.CommandLine:

choco install nuget.commandline

Configuration

Emplacement

La configuration NuGet se trouve dans un ficher XML NuGet.config. Il peut exister plusieurs versions de ce fichier suivant les différentes versions de NuGet et suivant la portée du paramétrage:

  • Dans le répertoire d’un projet, le paramétrage s’appliquera seulement dans le cadre du projet.
  • L’emplacement par défaut est: %APPDATA%\NuGet\NuGet.Config. La portée de ce fichier est globale.
  • A l’échelle de la machine, on peut utiliser un fichier dont la portée sera la machine ou une version particulière de Visual Studio. Ce fichier se trouve dans %ProgramData%\NuGet\Config.

    Suivant la version de Visual Studio le répertoire peut être:

    • %ProgramData%\NuGet\Config\[IDE]\[Version]\[SKU]\*.config
    • %ProgramData%\NuGet\Config\[IDE]\[Version]\*.config
    • %ProgramData%\NuGet\Config\[IDE]\*.config
    • %ProgramData%\NuGet\Config\*.config

    [IDE] correspond à VisualStudio; [Version] correspond à celle de Visual Studio (la version de Visual Studio 2015 est 14.0); [SKU] correspond à Community, Pro ou Enterprise.

Plus de détails sur l’emplacement du fichier de configuration dans Configuring NuGet behavior.

Contenu

La prise en compte de la configuration se fait en commençant par le fichier le plus global (i.e. fichier dans %ProgramData%) et en allant vers le fichier le plus spécifique (par exemple dans le répertoire d’un projet).

Les éléments de configuration sont surchargés par addition, ainsi si on indique des sources dans un fichier global et d’autres sources dans un fichier plus spécifique, les sources seront prises en compte par addition. Pour ne pas avoir ce comportement, il faut utiliser le nœud <clear />.

Par exemple un fichier NuGet.config peut contenir les nœuds suivants:

<?xml version="1.0" encoding="UTF-8"?> 
<configuration> 
    <config> 
        <add key="DefaultPushSource" value="https://example.com/packages/" /> 
    </config> 
    <packageSources> 
        <add key="Example Package Source" value="https://example.com/packages/" /> 
        <add key="nuget.org" value="https://www.nuget.org/api/v2/" /> 
    </packageSources> 
    <disabledPackageSources> 
        <add key="nuget.org" value="true" /> 
    </disabledPackageSources> 
</configuration>

Le détail de la configuration est:

  • Le nœud config permet d’indiquer les repositories vers lesquels on peut uploader des packages (cf. Nuget push).
  • Le nœud packageSources indique les sources à partir desquels les packages peuvent être téléchargés.
  • Le nœud disabledPackageSources contient les sources que l’on souhaite désactiver. Le nom de la source est indiquée avec le clé. Quand le booléen est à true la source est désactivée (c’est-à-dire que les packages ne pourront plus être téléchargés à partir de cette source).

Clear

Dans le nœud packageSources, il est possible d’indiquer <clear />:

<packageSources> 
    <clear />  
    <add key="otherSource" value="https://other/nuget/source" /> 
</packageSources>

<clear /> permet d’indiquer que les sources renseignées dans des fichiers plus haut dans la hiérarchie de prise en compte de la configuration ne seront pas ajoutées. Seulement la source otherSource sera prise en compte.

Configuration dans Visual Studio

Le plus souvent, il n’est pas nécessaire d’éditer les fichiers de configuration de NuGet à la main, il suffit d’aller dans Visual Studio dans:

  1. Cliquer sur le menu Tools puis Options
  2. Etendre le nœud Nuget Package Manager
  3. Sélectionner Package Sources
  4. On peut rajouter des sources en indiquant leur adresse. Il est aussi possible de désactiver des sources particulières.

Télécharger un package

Plusieurs méthodes sont possibles pour télécharger un package avec NuGet. La méthode la plus courante est de le faire avec Visual Studio en utilisant le Package Manager. Toujours dans Visual, si on souhaite ajouter des options particulières, on peut utiliser le Package Manager Console. On peut aussi utiliser NuGet en dehors de l’IDE à la ligne de commandes, avec des outils comme Package Explorer ou directement sur nuget.org.

Informations liées à un package

Pour un package donné, plusieurs informations sont disponibles et visibles à partir du Package Manager, avec Package Explorer ou sur le site nuget.org:

  • Le nom du package
  • La version du package MAJOR.MINOR.PATCH (cf. semantic versioning) suivi éventuellement d’une mention -alpha1 ou -beta1 pour les packages “prerelease”.
    La version d’un package NuGet et la version des assemblies ne correspondent pas forcément

    La version d’un package NuGet peut ne pas correspondre avec la version de ou des assemblies qui s’y trouvent. Il n’y a pas d’obligations que les versions correspondent. Ces différences peuvent mener à des confusions.

  • Les dépendances du package : ce sont les packages nécessaires au package courant. Généralement NuGet télécharge les dépendances quand il télécharge un package.

Dans Visual Studio

Package Manager

Dans Visual Studio, l’intérêt du Package Manager est d’avoir une interface graphique facile  à utiliser. Pour y accéder:

  1. Faire un clique droit sur le projet pour lequel on peut télécharger des packages (il est aussi possible de télécharger des packages à l’échelle de la solution en effectuant un clique droit sur la solution).
  2. Cliquer sur Manage NuGet Packages… (pour une solution, il faut cliquer sur Manager NuGet Packages for Solution…).
  3. L’interface permet de chercher facilement le package qu’on souhaite télécharger

L’interface permet de:

  • Renseigner le nom du package,
  • Indiquer si on souhaite effectuer un nouveau téléchargement (onglet Browse), lister les packages déjà téléchargés (onglet Installed) ou effectuer des mises à jour de packages déjà installés (onglet Updates): en sélectionnant directement le bon onglet, on peut effectuer les étapes plus rapidement.
  • Sélectioner la source à partir de laquelle on veut télécharger le package: si on ne préciser rien, toutes les sources seront prises en compte. Il est parfois plus efficace d’indiquer directement la source quand on la connaît pour que la recherche soit plus rapide.
  • Renseigner la version voulue.
  • Indiquer si on veut une version “prerelease”: dans le cas où cette case n’est pas cochée, les versions “prerelease” n’apparaîtront pas ce qui ce qui peut mener à quelques confusions.
La liste des packages affichés prend en compte la version du framework renseignée dans les paramètres du projet

Si le package n’est compatible qu’avec une version plus récente du framework que celle du projet, il ne sera pas affiché. Ce comportement peut mener à des confusions car on ne verra pas un package qui se trouve bien dans le repository de la source.
On peut s’aider d’outils comme le Package Explorer pour voir la liste des packages sans filtres.

Installation de package au niveau d’une solution

Seules les versions 2.x supportent cette fonctionnalité. Pour les versions ultérieures, il n’est pas possible d’installer un package au niveau d’une solution.

Package Manager Console

Le Package Manager Console permet aussi de télécharger des packages NuGet en utilisant des commandes Powershell. Pour ouvrir le Package Manager Console dans Visual Studio, il faut cliquer sur Tools, sur NuGet Package Manager puis sur Package Manager Console.

Pour que les commandes s’exécutent sur un projet spécifique et pour une source spécifique, il faut le préciser dans l’entête de la console:

Commandes principales de la Package Manager Console

Les commandes les plus courantes sont:
Pour chercher un package (find-package):
A partir de NuGet 3.0, on peut utiliser la commande find-package:

find-package [nom du package]

Pour avoir une version particulière d’un package:

find-package [nom du package] -version [version du package]

Parfois il y a trop de packages qui correspondent pour une recherche, pour restreindre la recherche exactement au nom du package, on peut utiliser l’option -ExactMatch:

find-package [nom du package] -ExactMatch

Pour installer un package (install-package):

Install-package [nom du package] {-Version [numéro de version]}

Pour une version prérelease:

Install-package [nom du package] {-Version [numéro de version]} -Pre

Pour lister des packages (get-package):
Lister les packages dans la solution:

get-package {[nom du package]}

Lister les packages disponibles dans les repositories:

get-package {[nom du package]} -ListAvailable

Lister les version d’un package disponible dans les repositories:

get-package [nom du package] -ListAvailable -AllVersions

Pour supprimer un package (uninstall-package):

Uninstall-package [nom du package]

Pour mettre à jour un package (update-package):
Met à jour le package et ses dépendances dans tous les projets:

update-package [nom du package]

Met à jour un package dans un projet particulier:

update-package -ProjectName [nom du projet]

Met à jour tous les packages dans tous les projets:

update-package

Réinstalle un package dans tous les projets:

update-package [nom du package] -reinstall

Examine toutes les assemblies d’un projet et ajoute des binding redirects si nécessaire:

Add-BindingRedirect

Etapes effectuées à l’installation d’un package

Lorsqu’un package est installé dans Visual Studio, il y un certain nombre d’opérations qui sont effectuées sur le projet:

  1. NuGet crée un répertoire packages dans le répertoire de la solution.
  2. Il télécharge le package ainsi que ses dépendances dans le cache de NuGet (cf. emplacement du cache NuGet).
  3. Il décompresse le package ainsi que les dépendances dans le répertoire packages dans le répertoire de la solution. Ce répertoire contiendra les éléments suivants:
    • Dans packages\[{nom du package espacé avec "."}.{version du package}]: se trouve le package .nupkg compressé et le fichier .nuspec contenant les métadonnées du package.
    • Dans packages\[{nom du package espacé avec "."}.{version du package}]\lib: se trouve toutes les assemblies suivant leur target framework. Par exemple pour le framework .NET 4.6.2, le target framework sera net462.
    • Dans packages\[{nom du package espacé avec "."}.{version du package}]\lib\[target framework]: se trouve les assemblies correspondant au target framework.
  4. Il ajoute le fichier packages.config s’il n’existe pas dans le ou les projets où les assemblies provenant du package sont rajoutées. Si packages.config existe, NuGet rajoute les lignes correspondant aux packages rajoutés.
    Ce fichier contient le nom des packages NuGet, leur version et le target framework correspondant. Par exemple, un fichier packages.config peut contenir les informations suivantes:

    <?xml version="1.0" encoding="utf-8"?>
    <packages>
      <package id="MongoDB.Bson" version="2.0.0" targetFramework="net45" />
      <package id="MongoDB.Driver" version="2.0.0" targetFramework="net45" />
      <package id="MongoDB.Driver.Core" version="2.0.0" targetFramework="net45" />
    </packages>
    

Précisions sur le target framework:

  • Pour le framework .NET, target framework sera du type net462, net46, net45, net40 etc…
  • Pour .NET Core: netcore, netcore451 etc…
  • Pour Universal Windows Platform: uap, uap10 etc…
  • Pour .NET Standard: netstandard1.0, netstandard1.6 etc…

On peut trouver une liste exhaustive de ces versions sur : Target Frameworks:

  1. NuGet rajoute les dépendances dans les références du projet (cf. ces références sont visibles dans le répertoire References sur projet dans l’explorateur de solution dans Visual.
  2. NuGet peut exécuter un script Powershell provenant du package s’il y en a un (cette fonctionnalité n’est plus disponible à partir de la version 3.x).
  3. NuGet peut modifier des fichiers de code source, des fichiers de configuration app.config ou web.config (cf. transformations appliquées par NuGet sur des fichiers) par exemple pour rajouter un élément de configuration lié au binding redirect (cf. BindingRedirect en 5min).

Activer la suggestion de packages dans Visual Studio 2017

Cette fonctionnalité est désactivée par défaut et elle n’est disponible que sur Visual Studio 2017. Elle permet de suggérer un package NuGet lorsqu’on écrit un namespace et les objets de ce namespace sont définis dans un package qui n’est pas installé:

Pour activer la fonctionnalité, il faut cliquer dans:

  1. Options du menu Tools
  2. Dans la partie Text Editor -> C# et Advanced
  3. Cocher Suggest using for types in NuGet packages et Suggest usings for types in reference assemblies.

La suggestion de package est accessible au moment de l’édition en plaçant le curseur sur le type inconnue et en tapant Alt + Maj + F10.

Avec Package Explorer

Package Explorer est outil qui permet de gérer des packages NuGet. Le gros intérêt de Package Explorer est qu’il est indépendant de Visual Studio et de la configuration de NuGet dans les fichiers NuGet.config. Il propose une interface simple pour:

  • Parcourir les packages disponibles dans le repository d’une source.
  • Avec accès aux détails d’un package (en particulier les target framework ou les dépendances).
  • Télécharger un package sans tirer toutes les dépendances.
  • Voir le contenu d’un package.
  • Construire un packages.

Package Explorer se trouve sur GitHub et sur CodePlex.

A partir de la ligne de commandes

Pour installer un package dans le répertoire courant:

nuget install [nom du package]

Cette commande ne modifie pas le fichier packages.config ou le fichier du projet.

Quelques options utiles:

  • -PreRelease : autoriser de télécharger une version “pre-release”.
  • -Version [numéro de version]: télécharge une version spécifique.
  • -NoCache: effectue le téléchargement sans utiliser le contenu du répertoire du cache NuGet.
  • -Source [URL de la source]: télécharge le package à partir d’une source précise.

Emplacement du cache

Quand NuGet télécharge des packages, il les place dans un répertoire de cache. Ce cache permet d’éviter d’effectuer des téléchargements du même package quand celui-ci a déjà été installé pour un projet.

Le gros inconvénient de ce cache est qu’il peut mener à des comportements inattendus dans certains cas:

  • Si un package se trouve dans le cache et si il a été supprimé de la source: le package s’installera normalement sur le poste où il est présent dans le cache. En revanche sur une machine où le package n’est pas présent dans le cache, NuGet ne pourra pas l’installer. Il y aura donc, 2 comportements différents sur les 2 machines pour un même package.
  • Si un package a été modifié dans une source mais que la version n’a pas changé: si ce package est déjà présent dans la cache, NuGet utilisera toujours la version qui se trouve dans le cache et non la nouvelle version se trouvant sur la source. Ce comportement entraîne l’utilisation de l’ancien package.

Pour éviter ces comportements, on peut utiliser les options -NoCache lorsqu’on utilise NuGet à la ligne de commande.

On peut vérifier le contenu du cache en exécutant (à partir de la version 3.3):

nuget locals all -list

On peut vider le cache en exécutant (à partir de la version 3.3):

nuget locals all -clear

Pour les versions antérieures à la version 3.3, on peut supprimer le cache directement à partir des répertoires:

  • %LocalAppData%\NuGet\Cache
  • %UserProfile%\.nuget\packages

Creation de package

La création de package se fait au moyen d’un fichier .nuspec qui est un fichier texte indiquant comment générer le package .nupkg et indique des informations sur le package.

Les fichiers ajoutés à un package:

  • peuvent se baser sur d’autres package NuGet, ce sont des dépendances. Ces dépendances seront ajoutées au projet de la même façon que le package lui-même.
  • peuvent être autonomes c’est-à-dire que l’exécution de ces assemblies ne nécessite pas d’autres assemblies ou fichiers.

S’il y a des dépendances sous forme de packages NuGet, il est préférable d’indiquer ces dépendances dans le fichier .nuspec plutôt que d’inclure les assemblies des dépendances directement dans le package que l’on souhaite créer.

Fichier .nuspec

Ce fichier sert à générer un package NuGet, il comprends les fichiers à inclure dans le package et la description du package.

Exemple de fichier .nuspec:

<?xml version="1.0"?>
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
  <metadata>
    <id>MyPackage</id>
    <version>2.5.1.23</version>
    <authors>Robert Mitchoum</authors>
    <owners>Robert Mitchoum</owners>
    <licenseUrl>http://opensource.org/licenses/MS-PL</licenseUrl>
    <projectUrl>http://github.com/MyProject</projectUrl>
    <iconUrl>http://github.com/MyProject/nuget_icon.png</iconUrl>
    <requireLicenseAcceptance>false</requireLicenseAcceptance>
    <releaseNotes>Bug fixes and performance improvements</releaseNotes>
    <description>Description of MyPackage</description>
    <copyright>Copyright  ©2016 Mitchoum Corp.</copyright>
    <tags>web utility</tags>
    <dependencies>
        <dependency id="Newtonsoft.Json" version="9.0" />
    </dependencies>
  </metadata>
  <files>
    <file src="readme.txt" target="" />
    <file src="c:\docs\bin\*.xml" target="lib" /> 
    <file src="bin\Debug\*.dll" target="lib" /> 
    <file src="bin\Debug\*.pdb" target="lib" /> 
    <file src="tools\**\*.*" />
    </files>
</package>

Ce fichier comprends les informations suivantes:

  • dans le nœud package\metadata: on indique les informations du package: nom, version, auteurs, description…
  • dans le nœud files\file: on indique où se trouve les fichiers à inclure dans le package. Pour chaque fichier, l’attribut src indique le chemin du fichier à inclure et l’attribut target indique où le fichier sera rangé dans le package NuGet. Le répertoire du fichier dans le package NuGet indiquera quelle sera la fonction du fichier.
  • dans le nœud files\dependencies: on indique quels sont les dépendances NuGet du package à créer. Les dépendances sont reconnues avec le nom du package et des conditions sur sa version.

    Par exemple, on peut indiquer des dépendances de cette façon:

    <dependencies>
      <dependency id="NomPackage1" version="3.0.0" />
      <dependency id="NomPackage2"/>
    </dependencies>
    

    Un syntaxe particulière peut être utilisée pour indiquer les conditions appliquées sur la version des dépendances (cf. Indication de la version des dépendances).

  • dans le nœud frameworkAssemblies\frameworkAssembly: on peut indiquer les assemblies du framework qui sont nécessaires à l’exécution des assemblies du package. Les assemblies du framework ne seront pas inclues dans le package. Ces précisions permettent d’indiquer, par exemple, à quelle version du framework s’applique le package.
    Par exemple:

    <frameworkAssemblies>
      <frameworkAssembly assemblyName="System.ComponentModel.DataAnnotations" targetFramework="net40" />
      <frameworkAssembly assemblyName="System.ServiceModel" targetFramework="net40" />
    </frameworkAssemblies>
    

Nom d’un package

Pour éviter les confusions, il est préférable d’utiliser un nom de package qui n’est pas déjà utilisé même si le package sera utilisé seulement dans un repository privé. Utiliser des noms de package spécifique permettra d’éviter le téléchargement par NuGet de mauvais packages.

Il est préférable d’utiliser des noms de package similaires à ceux des namespaces c’est-à-dire utiliser des “.” plutôt que des “-” ou des “_”.

Version du package

Il est préférable d’utiliser la même version entre celle du package et celle de l’assembly principale du package. Il n’y a pas d’obligation que ces versions soient similaires toutefois, utiliser un numéro de version couplé permet d’éviter des confusions quand le package sera téléchargé et installé.

Indication de la version des dépendances

Les contraintes sur les versions peuvent s’indiquer de cette façon:

1.0 1.0 ≤ x Version 1.0 ou supérieure
(1.0,) 1.0 < x Version strictement supérieure à 1.0
[1.0] x == 1.0 Exactement la version 1.0
(,1.0] x ≤ 1.0 Version 1.0 ou antérieure
(,1.0) x < 1.0 Version strictement antérieure à 1.0
[1.0,2.0] 1.0 ≤ x ≤ 2.0 Version entre la 1.0 et 2.0
(1.0) indication non valide

Pour plus de précisions : Specifying dependency versions for NuGet Packages.

Organisation d’un fichier .nupkg

Précisions sur la fonction d’un fichier suivant son chemin dans un package NuGet:

  • dans le répertoire tools: contient des scripts powershell et des programmes exécutables dans la Package Manager Console.
  • dans le répertoire lib se trouvent les assemblies à inclure au projet, la documentation liée aux assemblies (fichier .xml) et les fichiers symboles (.pdb).
  • dans le répertoire content: ces fichiers seront copiés à la racine du projet Visual dans lequel sera installé le package.
  • dans le répertoire build se trouvent des fichiers de targets ou de propriétés de MSBuild. Ces fichiers sont automatiquement rajoutés au projet Visual.

Comment utiliser un même package pour plusieurs versions de framework ?

A partir du framework .NET 4.0, il est possible d’appliquer plusieurs targets pour un projet de façon à ce qu’il soit déployé sur plusieurs plateformes (par exemple framework .NET 4.0 ou 4.5 ou suivant le .NET Standard 1.6 etc…).

Pour adresser les différents frameworks avec un même package, il suffit de placer les différentes assemblies dans des répertoires suivant une structure avec la version du framework.

Par exemple, si on veut supporter le framework .NET 4.6.2 et .NET Standard 1.6, il faut placer les assemblies suivant la structure:

\lib
    \net462
        \Assembly.dll
    \netstandard1.6
        \Assembly.dll

Si une même assembly est utilisée pour 2 target platforms, il faut obligatoirement copier cette assembly dans les 2 répertoires correspondant à ces target platforms.

Le nom des répertoires des target platforms (par exemple net462 et netstandard1.6) doit respecter une convention permettant d’indiquer précisément une plateforme.

Les plateformes les plus courantes sont :

Framework Abréviation Target framework moniker
.NET framework net net11
net40
net45
net451
net452
.NET Core App netcoreapp netcoreapp1.0
netcoreapp1.1
.NET Core netcore netcore [netcore45]
netcore45 [win, win8]
netcore451 [win81]
netcore50
Universal Windows Platform uap uap [uap10.0]
uap10.0
.NET Standard netstandard netstandard1.0
netstandard1.3
netstandard1.6

On peut trouver une liste exhaustive des target platforms sur Target frameworks.

Il ne faut pas confondre .NET Core App et .NET Core

NET Core App correspond à la première version du .NET Core qui a été renommé en janvier 2016 pour passer de .NET Core 5 à .NET Core 1.0 (voir Introducing .NET Core 1.0).
Pour les versions actuelles de .NET Core, il faut utiliser les abréviations netcoreapp.

Générer un fichier .nuspec

On peut créer un fichier .nuspec directement à la main avec un éditeur de texte mais il est aussi possible de le générer.

Pour générer un fichier générique avec les nœuds XML:

nuget spec [nom du package]

Pour générer un .nuspec à partir d’une assembly:

nuget spec [chemin de l'assembly]

Dans ce cas les informations du fichier seront complétées en fonction des informations de l’assembly.

Pour générer un .nuspec à partir de Visual Studio, il faut se placer dans le répertoire du projet (i.e. du fichier .csproj) et exécuter:

nuget spec

Le fichier généré sera un template contenant des tokens qui seront remplacés par les valeurs à la création du package. Pour que les tokens soient remplacés par les valeurs, il faut que le fichier .nupkg soit généré avec nuget pack [chemin du fichier .csproj].

Ajouter un fichier README

Le fichier sera affiché dans Visual Studio à l’installation du package.
Il suffit d’indiquer le chemin du fichier dans le nœud package\files, le nom du fichier doit être readme.txt:

<?xml version="1.0"?>
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
    <!-- ... -->
    <files>
      <file src="readme.txt" target="" />
    </files>
</package>

Ajouter des propriétés et des targets MSBuild

Ajouter des targets ou des propriétés MSBuild dans un package permet de rajouter ces fichiers directement dans un répertoire \build d’un projet. Ces fichiers doivent s’appeler:

  • [nom du package].targets pour les targets,
  • [nom du package].props pour les propriétés.

Si ces fichiers se trouvent dans le répertoire build, ils seront ajoutés pour tous les projets pour lesquels on installe la package quelque soit la version du framework cible. Si on souhaite que les fichiers soient installés pour des frameworks cible spécifiques, il faut ranger les fichiers dans un répertoire utilisant le nom de la plateforme cible.

Par exemple, si les fichiers doivent être installés pour des projets ayant pour plateforme cible le framework .NET 4.5, il faudra placer les fichiers suivant la hiérarchie:

  • build\net45\[nom du package].props et
  • build\net45\[nom du package].targets.

Il faut inclure ensuite des indications sur ces fichiers dans le fichier .nuspec au moment de la création du package.

Suivants les versions de NuGet, le comportement sera différent à l’installation d’un package lorsque celui-ci comprends des targets ou des propriétés MSBuild:

  • pour les versions Nuget 2.x: NuGet rajoute un nœud <Import> dans le fichier .csproj pour inclure les targets et propriétés.
  • pour les versions Nuget 3.x: NuGet ne rajoute pas les “targets” dans le fichier .csproj.

Créer un package .nupkg

Pour créer un package avec le fichier .nuspec:

nuget pack [fichier .nuspec]

Pour créer un package sur un projet Visual Studio:

nuget pack [nom du projet .csproj]

Quelques options peuvent être utiles:

  • -IncludeReferencedProjects: si un projet dépend d’autres projets, cette option permettra d’inclure dans le package, les fichiers générés par les autres projets. Cette option s’utilise en exécutant:
    nuget pack [chemin du fichier .csproj] -IncludeReferencedProjects
    
  • -properties: permet d’utiliser les fichiers provenant d’une configuration de build particulière. Par défaut, nuget pack [chemin du .csproj] utilise la configuration de build par défaut, on peut préciser une configuration spécifique en écrivant:
    nuget pack [chemin du fichier .csproj] -properties Configuration=[nom de la configuration de build]
    

    La configuration de build peut être, par exemple, Release ou Debug.

  • -symbols: cette option permet d’inclure dans le package généré les fichiers .pdb. Elle s’utilise en exécutant:
    nuget pack [nom du projet .csproj] -symbols
    

Nuget push

Permet d’uploader un package sur une source:

nuget push [chemin du fichier .nupkg] -Source [URL de la source]

On peut omettre l’option -Source à partir de NuGet 3.4.2 si on précise une valeur DefaultPushSource dans NuGet.config.

L’authentification auprès de la source se fait en utilisant une Api Key. Cette Api Key est fournie par la source. Avec l’Api Key:

nuget push [chemin du fichier .nupkg] [ApiKey de la source] -Source [URL de la source]

Modifier des fichiers du projet à l’installation du package

A partir de NuGet 3.x, on ne peut plus inclure des scripts d’installation Powershell dans un package NuGet. Il existe, en revanche, des méthodes pour créer ou modifier des fichiers dans un projet Visual.

Les modifications à appliquer sur les fichiers seront effectuées à l’installation du package et quand on désinstallera le package, certaines de ces modifications peuvent être annulées.

Modifier un fichier de code source

Les modifications peuvent être appliquées au projet à l’installation mais elles ne seront pas annulées à la désinstallation.

Il faut effectuer les étapes suivantes:

  • Pour ajouter un fichier dans un répertoire: par exemple pour ajouter le fichier dans le répertoire Controller, il faut placer le fichier dans le répertoire content\Controller du package NuGet.
  • Le nom du fichier doit comporter le suffixe .pp pour indiquer que le fichier doit être modifié à l’installation.

    Par exemple si on place un fichier ProductController.cs.pp dans le répertoire content\Controller du package NuGet, le fichier Controller\ProductController.cs sera ajouté dans le projet Visual.

  • Le contenu du fichier avec l’extension .pp peut comporter des tokens indiqués sous la forme $[nom du "token"]$ qui seront remplacés par leur valeur à l’installation.
    Par exemple : si on place la ligne : namespace $RootNamespace$.Controllers dans le fichier avec l’extension .pp et le RootNamespace est ProductApi dans le projet, le résultat après installation sera:

    namespace ProductApi.Controllers
    

    On trouver une la liste exhaustive des tokens utilisables dans ProjectProperties.

Modifier un fichier de configuration

Les modifications apportées à ces fichiers peuvent être annulées à la désinstallation.
2 méthodes sont possibles:

  • Appliquer une transformation XML,
  • Utiliser un fichier XDT.

Appliquer une transformation XML
Pour modifier un fichier de configuration App.config ou Web.config, il faut:

  • Ajouter le suffixe .transform au nom du fichier à modifier.
    Par exemple : pour un fichier App.config, il faut renommer le fichier en App.config.transform.
  • Placer ce fichier dans le répertoire content du package NuGet.
  • Ajouter des nœuds XML <add ... /> pour ajouter les éléments dans le fichier final.
    Par exemple : si on écrit dans le fichier Web.config.transform:

    <configuration>
      <system.webServer>
        <nodeToAdd value="false" />
        <modules>
          <add name="module1" type="Example.module1, Example" />
          <add name="module2" type="Example.module2, Example" />
        </modules>
      </system.webServer>
      <customNode>
        <!-- Custom text -->
        <innerCustomNode allowRemoteAccess="false" />
      </customNode>
    </configuration>
    

Dans le fichier Web.config:

  • Le nœud configuration/system.webServer/nodeToAdd sera rajouté.
  • Les nœuds configuration/system.webServer/nodeToAdd/modules/<add name="module1"/> et <add name="module2" />
  • Les commentaires <!-- Custom text --> seront ajoutés en suivant la hiérarchie XML.
  • etc…

A la désintallation du package, ces modifications sont annulées seulement si les lignes ajoutées n’ont pas été modifiées après l’installation.

Appliquer une modification par fichier XDT
Cette fonctionnalité est disponible à partir de NuGet 2.6.
On peut appliquer des transformations à un fichier XML en utilisant la syntae XDT. Pour plus de détails, voir Web.config Transformation Syntax for Web Application Project Deployment Using Visual Studio.

Pour appliquer ces modifications, il faut:

  • Ajouter le suffixe .install.xdt au nom du fichier à modifier.

    Par exemple : pour un fichier App.config, il faut renommer le fichier en App.config.install.xdt.

  • Placer ce fichier dans le répertoire content du package NuGet.
  • On peut utiliser des tokens indiqués sous la forme $[nom du "token"]$ qui seront remplacés par leur valeur à l’installation (voir ProjectProperties pour la liste des tokens).

Par exemple, si on utilise le fichier Web.config.install.xdt:

<?xml version="1.0"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <system.webServer>
    <nodeToAdd value="false" xdt:Transform="Insert"/>
    <modules>
      <add name="module1" type="Example.module1, Example" xdt:Transform="Insert"/>
      <add name="module2" type="Example.module2, Example" xdt:Transform="Insert"/>
    </modules>
  </system.webServer>
  <customNode>
    <innerCustomNode allowRemoteAccess="false" xdt:Transform="Insert"/>
  </customNode>
</configuration>

Les mêmes modifications seront appliquées au fichier Web.config d’origine que l’exemple précédent.

Pour que les modifications soient annulées à la désintallation, il faut ajouter au package NuGet un fichier avec l’extension .uninstall.xdt.

Par exemple, pour annuler les modifications précédentes, on peut ajouter un fichier Web.config.uninstall.xdt avec le contenu:

<?xml version="1.0"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <system.webServer>
    <nodeToAdd value="false" xdt:Transform="Remove"/>
    <modules>
      <add name="module1" type="Example.module1, Example" xdt:Transform="Remove"/>
      <add name="module2" type="Example.module2, Example" xdt:Transform="Remove"/>
    </modules>
  </system.webServer>
  <customNode>
    <innerCustomNode allowRemoteAccess="false" xdt:Transform="Remove"/>
  </customNode>
</configuration>

Appliquer un script d’installation Powershell

Cette fonctionnalité n’est plus disponible à partir de NuGet 3.x. Elle permet d’exécuter un script Powershell à l’installation et la désinstallation d’un package NuGet.

Pour que ces scripts soient exécutés, il faut qu’ils soient nommés suivant la convention suivante:

  • Init.ps1: il est exécuté à la 1ère installation du package,
  • Install.ps1: ce script est exécuté à l’installation du package,
  • Uninstall.ps1: il est exécuté à la désinstallation du package.

Il faut placer ses scripts dans le répertoire tools du package NuGet.

Le script Powershell doit commencer avec la ligne:

param($installPath, $toolsPath, $package, $project)

avec:

  • $installPath: path où le projet se trouve.
  • $toolsPath: path où le contenu du répertoire tools est décompressé à l’installation.
  • $package: informations sur le package qui est installé.
  • $project: informations sur le projet dans lequel l’installation est effectuée. Ces informations sont de type EnvDTE.

Nuget restore

Nuget restore est une fonctionnalité qui permet d’ajouter les assemblies provenant de packages NuGet dans les projets d’une solution juste avant la compilation. Si ces packages n’ont pas été téléchargés. NuGet les télécharge, les décompresse et les ajoute aux projets en fonction du fichier packages.config de chaque projet.

Nuget restore peut aussi restaurer dans le projet des packages si certains sont manquants.

L’intérêt de cette fonctionnalité est l’ajout automatique des packages sans que le développeur ne s’en préoccupe.
3 méthodes existent pour activer la fonctionnalité restore:

  • au niveau de Visual Studio: à partir de Nuget 2.7.
  • au niveau de MSBuild: pour les versions antérieures à Nuget 2.6.
  • en appelant une commande à la ligne de commandes.

Activer la fonctionnalité

Avec Visual Studio

Pour activer Nuget restore dans Visual studio: Tools -> Options -> Nuget package Manager -> General

Il faut activer Allow Nuget to download missing package.

L’option Automatically check for missing packages during build in Visual Studio permet d’effectuer le vérification des packages à chaque build.

Avec les fichiers de configuration

Au niveau global:
Il faut modifier %AppData%\Nuget\Nuget.config:

<configuration>
  <packageRestore>
    <add key="enabled" value="False" />
  </packageRestore>
</configuration>

Avant Nuget 2.6, il faut effectuer la modification dans .nuget\nuget.config dans chaque projet.

Pour vérifier la présence des packages à chaque build, il faut modifier le fichier: %AppData%\Nuget\Nuget.config:

<configuration>
  <packageRestore>
    <add key="automatic" value="False" />
  </packageRestore>
</configuration>

L’activation de ces options peuvent être globale en modifiant les fichiers dans:

%ProgramData%\Nuget\Config\[IDE]\[Version]\[SKU]

On peut aussi utiliser la variable d’environnement EnableNuGetPackageRestore en affectant la valeur true ou false pour surcharger les paramètres indiqués dans les fichiers de configuration. La valeur de la variable d’environnement sera utilisée par Visual Studio à condition qu’elle soit affectée avant le démarrage.

La priorité des fichiers de configuration se fait dans l’ordre indiqué précédemment.

Activer Nuget restore au niveau de MSBuild

Nuget restore existe sous cette forme pour les versions précédant Nuget 2.6. Les versions suivant Nuget 2.7 sont compatibles avec ce paramétrage toutefois il est déconseillé de l’utiliser.

L’activation se fait dans Visual Studio en effectuant un clique droit sur la solution et en sélectionnant Enable Nuget Package Restore.

A l’activation de cette option:

  • NuGet crée un répertoire .nuget au niveau de la solution et y ajoute Nuget.exe, un fichier nuget.config et un fichier nuget.targets.
  • NuGet met à jour tous les projets en ajoutant un nœud <RestorePackages>true<RestorePackages> et importe les targets se trouvant dans nuget.targets.
Breaking changes entre NuGet 2.6 et 2.7

Le paramétrage de Nuget restore au niveau de MSBuild pour les versions précédant Nuget 2.6 est incompatible avec les versions 2.7 et suivantes et peut mener à des comportements inattendus.

Quand Nuget restore est activée au niveau de MSBuild, Nuget 2.6 et précédents crée un répertoire .nuget avec un exécutable de Nuget.exe, un fichier Nuget.config et un fichier Nuget.targets. Avec Nuget 2.7 et suivant, la présence du répertoire .nuget peut causer ndes erreurs car la version de Nuget.exe correspondra à une ancienne version.

D’autre part si il existe un fichier .nuget\Nuget.targets dans le répertoire d’un projet, la restauration automatique (activée au niveau de Visual studio) sera ignorée pour ces projets.

Pour passer de Nuget restore au niveau de MSBuild à la restauration automatique, il faut d’abord d’abords supprimer tous les répertoires .nuget se trouvant dans les répertoires des projets et de la solution.

Il faut ensuite éditer les fichiers .csproj en supprimant les nœuds <RestorePackages> et supprimer toutes les références aux fichiers .nuget.targets.

Étapes effectuées par NuGet lorsque la restauration automatique est activée

Si NuGet restore est activée au niveau de Visual Studio:

  • un fichier .nuget\nuget.config est créé,
  • avant la compilation proprement dite, NuGet parcours tous les fichiers packages.config pour télécharger les packages à partir du cache ou à partir de la source.

Restauration automatique à partir de la ligne de commandes

Pour les versions 2.7 et suivantes, on peut exécuter Nuget restore au niveau d’une solution en exécutant dans le répertoire de la solution:

nuget restore

Ou

nuget restore [fichier .sln]
Références:
  • Repository nuget: https://www.nuget.org/
  • Extension NuGet Package Manager pour Visual Studio: https://marketplace.visualstudio.com/items?itemName=NuGetTeam.NuGetPackageManager
  • Consume NuGet packages in Visual Studio: https://www.visualstudio.com/en-us/docs/package/nuget/consume
  • Configuring NuGet behavior: https://docs.microsoft.com/fr-fr/nuget/consume-packages/configuring-nuget-behavior
  • NuGet File Locations: http://lastexitcode.com/projects/NuGet/FileLocations/
  • Target frameworks: https://docs.microsoft.com/en-us/nuget/schema/target-frameworks
  • NuGet Package Manager UI: https://docs.microsoft.com/en-us/nuget/tools/package-manager-ui
  • Installing NuGet: https://docs.microsoft.com/fr-fr/nuget/guides/install-nuget
  • Release Notes NuGet: https://docs.microsoft.com/en-us/nuget/release-notes/index
  • Creating NuGet packages: https://docs.microsoft.com/en-us/nuget/create-packages/creating-a-package
  • Installing and reinstalling packages with package restore: https://docs.microsoft.com/en-us/nuget/consume-packages/package-restore
  • Transforming source code and configuration files: https://docs.microsoft.com/en-us/nuget/create-packages/source-and-config-file-transformations
  • Supporting multiple .NET framework versions: https://docs.microsoft.com/en-us/nuget/create-packages/supporting-multiple-target-frameworks
  • NuGet CLI reference: https://docs.microsoft.com/fr-fr/nuget/tools/nuget-exe-cli-reference
  • NuGet.VisualStudio 4.0.0 : https://www.nuget.org/packages/NuGet.VisualStudio
  • Visual Studio 2017 can automatically recommend NuGet packages for unknown types: https://www.hanselman.com/blog/VisualStudio2017CanAutomaticallyRecommendNuGetPackagesForUnknownTypes.aspx
  • ASP.NET 5 is dead – Introducing ASP.NET Core 1.0 and .NET Core 1.0: https://www.hanselman.com/blog/ASPNET5IsDeadIntroducingASPNETCore10AndNETCore10.aspx
  • Installing Chocolatey: https://chocolatey.org/install
  • Nuget.CommandLine 3.5.0 with Chocolatey: https://chocolatey.org/packages/NuGet.CommandLine
  • Running PowerShell scripts during NuGet package installation and removal: https://everydaylifein.net/netframework/running-powershell-scripts-during-nuget-package-installation-and-removal.html
  • NuGet Package explorer sur Github: https://github.com/NuGetPackageExplorer/NuGetPackageExplorer
  • Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInEmail this to someonePrint this page

    Routed commands en WPF en 2 min

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

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

    Caractéristiques des commandes routées

    Interface ICommand

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

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

    Ainsi:

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

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

    Par exemple:

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

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

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

    Aspect routé

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

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

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

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

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

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

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

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

    Routage des commandes

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

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

    Implémentations des commandes routées

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

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

    Commandes prédéfinies

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

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

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

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

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

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

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

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

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

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

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

    Définir une commande

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

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

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

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

    RoutedUICommand

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

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

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

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

    Effectuer le binding dans le code behind

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

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

    CommandTarget

    Lors du déclenchement de la commande:

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

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

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

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

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

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

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

    CommandParameter

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

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

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

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

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

    KeyBinding

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

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

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

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

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

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

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

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

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

    Git en 5 min

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

    Comparaison avec les autres gestionnaires de code source

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

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

    Les principales caractéristiques de Git sont:

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

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

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

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

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

    Installation

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

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

    Premières étapes

    Renseigner quelques paramètres

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

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

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

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

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

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

    $ git config --list
    

    Pour indiquer la valeur d’un paramètre:

    $ git config user.email 
    

    Initialiser le répertoire de travail (git init)

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

    Pour initialiser le répertoire de travail:

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

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

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

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

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

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

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

    Par exemple:

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

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

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

    Pour afficher l’état, on écrit:

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

    Effectuer des commits

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

    Workflow pour effectuer un commit

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

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

    Ajouter les fichiers à commiter (git add)

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

    git add [chemin du fichier à ajouter] 
    

    Enlever un fichier des fichiers à commiter

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

    git reset HEAD [chemin du fichier] 
    

    Commiter des fichiers “stagés”

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

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

    Supprimer des fichiers versionnés

    Il faut d’abord supprimer le fichier de Git:

    git rm [chemin du fichier à supprimer] 
    

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

    git rm -f [chemin du fichier à supprimer] 
    

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

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

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

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

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

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

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

    git diff [chemin du fichier]

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

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

    git diff HEAD [chemin du fichier]

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

    git diff --cache [chemin du fichier]

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

    git diff

    Afficher l’historique (git log)

    On peut afficher l’historique en utilisant git log:

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

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

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

    Après 2 commits, le graphe devient:

    On peut afficher les 5 derniers commits avec:

    $ git log -5
    

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

    $ git log --graph --oneline 
    

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

    $ git log --graph –oneline --all 
    

    Envoyer ses modifications vers le dépôt distant

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

    $ git push origin master
    

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

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

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

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

    git remote add origin [nom du serveur]  
    

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

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

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

    git push 
    

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

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

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

    git push origin master
    

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

    Branche

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

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

    Créer une branche et passer directement dessus:

    git checkout -b [nom de la branche]
    

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

    git push origin [nom de la branche]  
    

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

    git checkout [nom de la branche]  
    

    Créer une branche sans passer dessus:

    git branch [nom de la branche]  
    

    Lister les branches locales:

    git branch --list  
    

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

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

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

    Supprimer une branche

    D’abord supprimer la branche localement en faisant:

    git branch --delete [nom de la branche]  
    

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

    git push origin :[nom de la branche]  
    

    Renommer une branche

    D’abord localement:

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

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

    git push origin :[ancien nom de la branche]  
    

    Effectuer des merges

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

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

    git pull
    
    PULL = FETCH + MERGE

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

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

    git fetch
    

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

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

    Merger 2 branches (git merge)

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

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

    git merge [nom de la branche à merger]
    

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

    git add [nom du fichier]  
    

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

    Tags

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

    Il existe 2 types de tag:

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

    Tags légers

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

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

    Pour tagguer le dernier commit:

    git tag [nom du tag] -1
    

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

    git push origin –tags
    

    Supprimer un tag local:

    git tag –d [nom du tag] 
    

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

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

    Liste les tags existants:

    git tag --list 
    

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

    git checkout tags/[nom du tag] 
    

    Récupérer un tag et créer une branche (locale):

    git checkout tags/[nom du tag] -b [nom de la branche]
    

    Tags annotés

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

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

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

    git push origin [nom du tag] 
    

    Voir le détail d’un tag:

    git show [nom du tag] 
    

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

    git checkout tags/[nom du tag]
    

    Rebase

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

    • Modifier des commits
    • Réunir 2 branches

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

    Modifier des commits

    Modifier des commits localement

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

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

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

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

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

    $ git rebase -i HEAD~2 
    

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

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

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

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

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

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

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

    Un écran suivant propose de modifier le commentaire puis:

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

    Si on affiche l’historique:

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

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

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

    $ git push -f
    

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

    Réunir 2 branches

    Branche ayant un nœud commun avec le master

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

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

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

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

    $ git checkout featureBranch 
    $ git rebase master
    

    Le graphe devient:

    Ensuite, on se place dans la branche master:

    $ git checkout master
    

    Le graphe est alors:

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

    $ git merge featureBranch 
    

    Le graphe devient:

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

    Branche séparée du master

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

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

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

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

     $ git rebase –onto master featureBranch otherBranch 
    

    Le graphe devient:

    Il suffit ensuite de merger la branche “otherBranch”:

    $ git checkout master
    

    Le graphe est:

    Enfin après le merge:

    $ git merge otherBranch
    

    Le graphe après le merge:

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

    $ git branch –d featureBranch
    

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

    Annuler des modifications

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

    Avant d’effectuer un commit

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

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

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

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

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

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

    git clean -f
    

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

    git fetch origin 
    git reset --hard origin/master 
    

    Modifier le dernier commit

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

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

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

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

    git push origin master –f 
    

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

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

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

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

    Annuler un commit

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

    git reset [label ou SHA-1 du commit]
    

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

    git push –f 
    

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

    Cherry-pick

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

    Pour utiliser git cherry-pick, il suffit de:

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

    On exécute ensuite:

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

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

    Stash

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

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

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

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

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

    Le répertoire ne contient plus de modifications:

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

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

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

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

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

    $ git stash apply
    

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

    $ git stash apply [nom du stash] 
    

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

    $ git stash pop 
    

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

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

    Pour s’exercer:

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

    “Mocker” une dépendance statique

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

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

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

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

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

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

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

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

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

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

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

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

    Injecter une dépendance

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

    L’implémentation de AgeCalculator devient:

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

    Avec:

    public interface ICurrentDateHandler 
    { 
        DateTime GetCurrentDate(); 
    }
    

    Et:

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

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

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

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

    Le test devient:

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

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

    Utiliser une Factory

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

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

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

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

    Avec:

    public interface IDateHandlerFactory 
    { 
        ICurrentDateHandler GetCurrentDateHandler(); 
    }
    

    urrentDateHandler devient:

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

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

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

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

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

    Utiliser une classe statique Proxy

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

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

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

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

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

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

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

    Ainsi si on considère la classe suivante:

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

    On peut l’utiliser directement dans AgeCalculator sans injection:

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

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

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

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

    Smocks

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

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

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

    Le code de AgeCalculator n’est pas modifié:

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

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

    Microsoft Fakes

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

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

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

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

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

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

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

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

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

    Les options sont:

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

    D’autres options peuvent être utiles comme:

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

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

    VisualWget

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

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

    Trouver les processus utilisant un fichier ou un répertoire

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

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

    ou

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

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

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

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

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

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

    Fusion

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

    Activer les logs de “Fusion”

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

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

    Visionner les logs

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

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

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

    The operation failed.
    Bind result: hr = 0x80070002. The system cannot find the file specified.
    
    Assembly manager loaded from:  C:\WINDOWS\Microsoft.NET\Framework\v2.0.60425\mscorwks.dll
    Running under executable  C:\Program Files\Microsoft Visual Studio 8\Team Tools\Performance Tools\XXXXviewer.exe
    — A detailed error log follows.
    
    === Pre-bind state information ===
    LOG: User = REDMOND\XXXX
    LOG: DisplayName = XXXXvisualization, Version=8.0.0.0, Culture=neutral, PublicKeyToken=XXXXXXXXXXXX
     (Fully-specified)
    LOG: Appbase = file:///C:/Program Files/Microsoft Visual Studio 8/Team Tools/Performance Tools/
    LOG: Initial PrivatePath = NULL
    LOG: Dynamic Base = NULL
    LOG: Cache Base = NULL
    LOG: AppName = XXXXviewer.exe
    Calling assembly : XXXXviewer, Version=8.0.0.0, Culture=neutral, PublicKeyToken=XXXXXXXXXXXX
    ===
    LOG: This bind starts in default load context.
    LOG: No application configuration file found.
    LOG: Using machine configuration file from C:\WINDOWS\Microsoft.NET\Framework\v2.0.60425\config\machine.config.
    LOG: Post-policy reference: XXXXvisualization, Version=8.0.0.0, Culture=neutral, PublicKeyToken=XXXXXXXXXX
    LOG: GAC Lookup was unsuccessful.
    LOG: Attempting download of new URL file:///C:/Program Files/Microsoft Visual Studio 8/Team Tools/Performance Tools/XXXXvisualization.DLL.
    LOG: Attempting download of new URL file:///C:/Program Files/Microsoft Visual Studio 8/Team Tools/Performance Tools/XXXXvisualization/XXXXvisualization.DLL.
    LOG: Attempting download of new URL file:///C:/Program Files/Microsoft Visual Studio 8/Team Tools/Performance Tools/XXXXvisualization.EXE
    LOG: Attempting download of new URL file:///C:/Program Files/Microsoft Visual Studio 8/Team Tools/Performance Tools/XXXXvisualization/XXXXvisualization.EXE.
    LOG: All probing URLs attempted and failed.
    

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

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

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

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

    DependancyWalker

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

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

    Identifier les dépendances par programmation

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

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

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

    Chargement d’un assembly

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

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

    Pour afficher la version du framework

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

    Assembly assembly = Assembly.LoadFrom(assemblyPath);
    object[] attributes = assembly.GetCustomAttributes(true);
    Type targetFrameworkAttributeType = 
        typeof(System.Runtime.Versioning.TargetFrameworkAttribute);
    System.Runtime.Versioning.TargetFrameworkAttribute fwkAttribute = 
        (System.Runtime.Versioning.TargetFrameworkAttribute)attributes
            .FirstOrDefault(p => p.GetType() == targetFrameworkAttributeType);
    
    // Pour afficher la version sous la forme ".NETFramework,Version=vX.X"
    Console.WriteLine(fwkAttribute.FrameworkName);
    
    // Pour afficher la version sous la forme ".NET Framework X.X"
    Console.WriteLine(fwkAttribute.FrameworkDisplayName);
    

    Pour afficher la version du CLR d’une assembly

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

    Routed events en WPF en 3 min

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

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

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

    Caractéristiques des évènements routés

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

    On distingue 2 types d’arbres:

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

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

    Stratégie de propagation

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

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

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

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

    Convention de nommage des évènements routés

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

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

    Evènements routés définis par pair

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

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

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

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

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

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

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

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

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

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

    Par exemple, si on écrit:

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

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

    • Window,
    • StackPanel,
    • Button

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

    • Button,
    • StackPanel,
    • Window.

    Types d’évènements routés

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

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

    Evènements du cycle de vie

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

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

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

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

    Evènements du clavier

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

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

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

    Evènements de la souris

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

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

    Implémentation des évènements routés

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

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

    Ajouter une callback à un évènement

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

    Handler défini dans UIElement

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

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

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

    Ainsi on peut écrire:

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

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

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

    Evènement défini dans une autre classe

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

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

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

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

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

    Le code behind reste identique.

    Ajouter un handler dans le code behind

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

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

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

    Ainsi si le code Xaml est:

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

    On peut écrire:

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

    On peut aussi utiliser une notation plus concise:

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

    Utiliser UIElement.AddHandler()

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

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

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

    Avec:

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

    Utiliser EventManager.RegisterClassHandler()

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

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

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

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

    Interrompre la propagation d’un évènement

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

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

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

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

    Par exemple:

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

    Définir un évènement routé

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

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

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

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

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

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

    Avec:

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

    Partager un évènement routé entre plusieurs classes

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

    System.Windows.RoutedEvent.AddOwner(Type ownerType)

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

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

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

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

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

    Et:

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

    Déclencher un évènement routé

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

    UIElement.RaiseEvent(RoutedEventArgs e)

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

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

    Arbre logique et arbre visuel WPF en 2 min

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

    Hiérarchie des classes d’objets

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

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

    La hiérarchie des classes des objets est:

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

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

    Arbre logique

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

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

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

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

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

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

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

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

    Arbre visuel

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

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

    Par exemple quand on écrit:

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

    L’arbre visuel est:

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

    Ainsi l’arbre visuel sert à:

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

    LogicalTreeHelper et VisualTreeHelper

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

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

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

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

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

    Chercher un type particulier parmi les enfants

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

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

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

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

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

    Chercher tous les enfants ayant un type particulier

    Pour chercher tous les descendants ayant un type particulier:

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

    Débugguer l’arbre visuel

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

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