Mettez des Add-ins dans votre application avec le Framework 3.5
Date de publication : 09/01/2008 , Date de mise à jour : 09/01/2008
Par
Florian Casabianca (mon site)
(3 commentaires)
Rendre une application extensible passe par la possibilité d'y ajouter des add-ins. Plus facile à dire qu'à faire. De nombreux points sont à prendre en compte et les problèmes à résoudre ne manquent pas. L'arrivée de la version 3.5 du Framework .Net et du nouvel espace de noms System.AddIn va permettre une prise en charge beaucoup plus facile de ce genre de fonctionnalité au sein de nos applications.
Introduction
I. Un peu de théorie
I-1. Vous avez dit Add-in ?
I-2. Les grands principes de développement
I-2-1. Trois types de développeurs
I-3. Aperçu des fonctionnalités
I-3-1. Découverte
I-3-2. Activation
I-3-3. Isolation
I-3-4. Déchargement
I-3-5. Sandboxing
II. L'architecture du pipeline
II-1. Les composants
II-2. L'architecture des répertoires
III. Exemple de création d'un add-in
III-1. Présentation de l'exemple
III-2. Architecture utilisée
III-3. Organisation de la solution
III-4. Le contrat
III-5. Coté Add-in
III-5-1. La vue
III-5-2. L'add-in
III-5-3. L'adaptateur
III-6. Coté Hôte
III-6-1. La vue
III-6-2. L'adaptateur
III-7. L'hôte
III-7-1. L'application
III-7-2. Découverte d'add-ins
III-7-2-1. La mise en cache des informations
III-7-2-2. Recherche d'add-ins
III-7-3. Activation
III-7-4. Utilisation
III-7-5. Déchargement
VI. Compatibilité ancien add-in, nouvel hôte
VI-1. Présentation de la mise à jour
VI-2. Nouveau contrat
VI-3. Modifications coté hôte
VI-3-1. La vue
VI-3-2. L'adaptateur
IV-3-3. L'hôte
VI-4. Création d'un add-in V2
VI-4-1. La vue
VI-4-2. L'add-in
VI-4-3. L'adaptateur
VI-5. Compatibilité descendante avec un ancien add-in
VI-5-1. Etat des lieux
VI-5-2. L'adaptateur
VI-6. Test de l'application
V. Conclusion
VI. Liens
Sources
Remerciements
Contact
Introduction
Rendre une application extensible passe par la possibilité d'y ajouter des add-ins. Plus facile à dire qu'à faire. De nombreux points sont à prendre en compte et les problèmes à résoudre ne manquent pas. L'arrivée de la version 3.5 du Framework .Net et du nouvel espace de noms System.AddIn va permettre une prise en charge beaucoup plus facile de ce genre de fonctionnalité au sein de nos applications.
I. Un peu de théorie
I-1. Vous avez dit Add-in ?
Un add-in est un fragment de code (une assembly) chargé et activé de façon dynamique par une application hôte dans certaines circonstances (démarrage de l'hôte, demande de l'utilisateur, etc). Un add-in est généralement écrit par une personne différente de celle qui a conçue l'application. Cela implique que l'add-in n'a pas forcément été testé de façon poussée. Ainsi, l'application hôte doit pouvoir utiliser un add-in tout en étant protégée de tout problème survenant du coté de l'add-in (instabilité, sécurité, etc.).
Les scénarios d'utilisation d'add-ins peuvent se regrouper dans trois catégories:
-
Fournir un service. Ici l'add-in apporte un service dont l'hôte va se servir. C'est par exemple le cas d'un add-in qui permettrait à un logiciel de traitement de texte d'exporter un fichier dans un nouveau format.
-
L'automation. Ici l'add-in définit la façon dont va se comporter l'hôte. C'est par exemple le cas avec les add-ins Office ou les add-ins Windows Live Messenger dont un article se trouve ici.
-
Le
"
parasitage
". L'add-in se comporte ici un peu comme un parasite et utilise l'hôte simplement comme support. Il ne contrôle pas l'hôte et ne lui offre pas véritablement un service. Il offre des fonctionnalités à l'utilisateur en s'intégrant à l'hôte mais pourrait tout aussi bien fournir les mêmes fonctionnalités s'il été intégré à un hôte différent. C'est le cas par exemple des barres d'outils qui s'installent dans les navigateurs ou dans la barre des tâches.
Dans l'exemple de création d'un add-in que nous verrons un peu plus tard, nous concevrons un add-in se situant dans la première catégorie.
I-2. Les grands principes de développement
Le "versionning" et l'isolation des composants sont des points critiques pour la stabilité à long terme d'une application. Une des conséquences importantes est qu'un type (au sens objet) appartenant à un add-in ne devra jamais être chargé dans le domaine d'application de l'hôte.
L'application hôte et ses add-ins doivent pouvoir évoluer de façon indépendante (aucune référence directe de l'un sur l'autre). Cela signifie par exemple qu'une nouvelle version de l'hôte doit pouvoir (c'est une possibilité, pas une obligation) utiliser des add-ins développés pour l'ancienne version de l'application (compatibilité descendante). La compatibilité ascendante (capacité pour des add-ins écris pour une version V2 d'une application de fonctionner avec une version V1) peut aussi être assurée (mais difficile à obtenir).
"Design for success". La mise en place de l'architecture proposée par Microsoft pour le support d'add-ins va engendrer du travail supplémentaire pour la première version de votre application. Mais le respect de ce modèle permettra d'assurer le succès du développement des futures versions, notamment dans la prise en compte du "versionning" ou de l'isolation des composants. Il est donc important de comprendre qu'une application bien construite dès le départ (ici pour la prise en compte d'add-ins) vous permettra de la faire évoluer sans que la nouvelle version ne casse la compatibilité avec l'ancienne.
I-2-1. Trois types de développeurs
Le modèle de développement proposé permet de découper le travail à effectuer en trois parties (trois types de tâches distinctes). Chaque partie sera prise en charge par un développeur différent (ce n'est pas une obligation, juste une possibilité de fractionner le travail). Vous aurez donc, par ordre de difficulté croissante:
Le développeur d'add-in. Un accent a été mis sur le besoin de faciliter au maximum le travail du développeur de compléments. Son seul soucis doit être de coder les fonctionnalités de son add-in, pas de s'occuper des mécanismes de "versionning", d'isolation etc. Ainsi, la création d'un add-in peut se résumer à ce bout de code:
[AddIn("mon addin", Version = "1.0.0.0",)]
public class monAddin : VueAddIn
|
Deux choses sont à noter. La première est l'ajout d'un tag AddIn (permettant de spécifier quelques méta-données telles qu'une description, un numéro de version, etc.). La deuxième est qu'un add-in doit dériver d'une vue (Interface ou classe abstraite). Cette vue représente la façon dont l'add-in est vu par l'hôte (la façon dont l'hôte va se servir de l'add-in).
Le développeur de l'hôte. Celui-ci n'a pas un travail très difficile non plus. Il lui suffit de quelques lignes de codes pour découvrir, activer puis utiliser un add-in. Voici à peu près le code à utiliser:
IList<AddInToken> tokens = AddInStore.FindAddIns(typeof(AddInType), addinPath);
foreach(AddinToken token in tokens)
{
AddInType addinInstance = token.Activate<AddInType>(AddInSecurityLevel.FullTrust);
}
|
Le développeur du pipeline. C'est ce dernier qui aura la tâche la plus délicate. Il devra développer un modèle permettant à l'hôte et à ses add-ins de communiquer entre eux, indépendamment de la version de l'un ou l'autre. Nous allons dans la prochaine partie détailler les différents composants du pipeline.
I-3. Aperçu des fonctionnalités
Voici un petit récapitulatif des fonctionnalités offertes par cette nouvelle API:
I-3-1. Découverte
- Via des vues/types (au sens objet) et répertoires spécifiques.
- Lecture des métadonnées sans avoir besoin de charger ou activer l'add-in.
- Aucune déclaration (contrairement aux objets COM) n'a besoin d'être faite sur la machine pour ajouter un add-in à un hôte. Un simple copier/coller peut suffire.
- Le pipeline et les add-ins sont physiquement séparés (répertoires différents).
I-3-2. Activation
- Sélection du contexte d'isolation (utilisation d'un domaine d'application personnalisé ou création d'un nouveau / utilisation d'un processus différent).
- Création du pipeline en chargeant les assemblies correspondantes.
- Instanciation de l'add-in.
- L'add-in est retourné à l'hôte sous forme de vue.
I-3-3. Isolation
- Entre l'hôte et l'add-in
- Entre les add-ins
I-3-4. Déchargement
- L'hôte peut à tout moment demander à décharger un add-in.
- Déchargement automatique lorsqu'il n'existe plus aucune référence à l'add-in dans l'hôte.
I-3-5. Sandboxing
- Les add-ins doivent s'exécuter avec des permissions égales ou inférieures à celle de l'hôte.
- Une énumération permet de spécifier le niveau de sécurité accordé à l'add-in (Full Trust, Internet, Intranet).
- Possibilité de spécifier un ensemble personnalisé de permissions.
II. L'architecture du pipeline
II-1. Les composants
Nous allons ici présenter les différents composants du pipeline permettant la communication entre l'hôte et l'add-in. C'est sans doute la partie la plus délicate à comprendre (et à expliquer.).
Voici un schéma présentant l'architecture générale du pipeline:
Deux points sont essentiels à noter.
- Le premier est que chaque composant est en fait matérialisé par une assembly différente.
- Le deuxième est la direction des dépendances statiques. L'hôte et l'add-in ne dépendent seulement que d'une vue. Et cette vue n'a de dépendance sur aucun autre composant.
Les vues hôte et complément (add-in) sont des assemblies ne contenant rien d'autres que des classes abstraites (ou des interfaces) représentants la façon dont l'hôte et l'add-in se voient mutuellement. L'hôte ne connait de l'add-in que ce qui est défini dans la vue hôte, et l'add-in ne connait de l'hôte que ce qui est défini dans la vue complément.
Les contrats sont des interfaces définissants le protocole permettant de communiquer à travers la frontière séparant l'hôte et l'add-in. Les contrats doivent hériter de IContract, doivent être des interfaces (au sens POO) et ne peuvent être "versionnés". Ils n'ont de références sur aucun autre composant. Les types contenus dans l'assembly des contrats sont les seuls à être chargés des deux cotés de la limite d'isolement. Ni l'hôte ni l'add-in ne font directement référence au contrat, on utilise une couche intermédiaire, les adaptateurs.
Les adaptateurs ont un rôle stratégique. Ils ont pour objectif de convertir les vues en contrat et les contrats en vue. C'est dans ces adaptateurs que sera écrit le code permettant d'assurer une compatibilité entre nouvelle et ancienne version de l'hôte et de l'add-in. Pour mieux comprendre ce qu'est un adaptateur, vous êtes invité à lire l'article suivant:
Comprendre le design Pattern Adaptateur.
Comme vous le voyez, l'hôte ne possède jamais de référence au contrat, ni de référence à l'adaptateur lui-même (idem pour l'add-in). Ainsi, nous avons la possibilité d'avoir une nouvelle version du contrat, les adaptateurs s'occupant de maintenir une compatibilité avec les anciens composants.
Le schéma suivant présente ce principe:
Nous avons ici une nouvelle version de l'hôte (V2). Un nouveau contrat à été défini afin de ternir compte de ces nouveautés (le contrat V1 n'existe plus). Comment un add-in développé pour la version 1 de l'hôte va-t-il pouvoir continuer à fonctionner avec la version 2 ? Grâce aux adaptateurs. Un adaptateur (V1-> V2) a été développé et dont le but est de convertir le nouveau contrat (V2) en une vue (V1) compréhensible pour l'add-in (V1). Bien sûr, les add-ins spécialement développés pour l'hôte V2 utilisent une nouvelle vue (V2) et un adaptateur (V2) suivant le schéma traditionnel.
Vous aurez aussi compris que pour qu'une nouvelle version de l'hôte arrête de supporter les anciennes versions d'un add-in il lui suffit de ne pas proposer l'adaptateur adéquat.
Il devient aussi possible de différer le support d'anciens add-ins au sein d'une nouvelle version de l'hôte. Etant donné que les adaptateurs se trouvent dans des assemblies séparés de l'hôte (et des add-ins), il suffit de rajouter (dans le dossier des adaptateurs) l'assembly contenant l'adaptateur adéquat pour assurer le support d'une ancienne version d'un add-in (le tout sans devoir retoucher à l'hôte).
II-2. L'architecture des répertoires
Afin que le Framework puisse découvrir les différents composants du pipeline et activer les add-ins, ceux-ci doivent être placés dans des répertoires ayant une arborescence et des noms spécifiques.
Le schéma suivant montre l'arborescence requise:

|
Répertoire
|
Description
|
| Pipeline root |
C'est le répertoire racine du pipeline qui contiendra tous les autres répertoires listés ci-dessous. Il n'y a pas de restriction concernant son nom ni l'endroit où il se trouve (mais il est généralement dans le même repertoire que l'hôte). Exemple: ..\Pipeline |
| AddIns |
Optionnel. Ce répertoire contient un ou plusieurs autres répertoires. Chacun des ces sous-répertoires contient un add-in.Ce répertoire doit être appelé AddIns. Exemple: ..\Pipeline\AddInsIl est aussi possible d'avoir des add-ins se trouvant ailleurs dans le système. |
| AddInSideAdapters |
Ce répertoire contient tous les adaptateurs coté add-in.Ce répertoire doit être appelé AddInSideAdapters. Exemple: ..\Pipeline\AddInSideAdapters |
| AddInViews |
Ce répertoire contient toutes les vues coté add-in.Ce répertoire doit être appelé AddInViews. Example: ..\Pipeline\AddInViews |
| Contracts |
Ce répertoire contient les contrats.Ce répertoire doit être appelé Contracts. Exemple: ..\Pipeline\Contracts |
| HostSideAdapters |
Ce répertoire contient les adaptateurs coté hôte.Ce répertoire doit être appelé HostSideAdapters. Exemple: ..\Pipeline\HostSideAdapters |
III. Exemple de création d'un add-in
III-1. Présentation de l'exemple
Après la théorie, place à la pratique. Nous allons concevoir ici une petite application permettant d'être étendue via l'ajout d'add-ins. Nous verrons dans un premier temps le développement de l'hôte, du pipeline et d'un add-in, le tout en version 1. Puis nous étudierons les modifications à apporter afin d'assurer une compatibilité entre une nouvelle version de l'hôte et la première version de l'add-in.
Le concept de l'application exemple est assez simple. L'hôte sera représenté par un petit éditeur de texte. La sauvegarde dans un fichier du texte saisi ne sera pas directement prise en compte par l'hôte, mais par un add-in. L'hôte transmettra à l'add-in un objet String contenant le texte saisi par l'utilisateur et l'add-in se chargera de le sauvegarder dans un fichier sous un format spécifique. L'utilisateur aura juste à choisir l'add-in qu'il veut utiliser pour la sauvegarde. Ainsi nous aurons des add-ins permettant d'enregistrer au format texte, XML, PDF, OpenXML, etc.
Voici à quoi ressemblera l'application:
Une liste déroulante sera remplie avec la liste des add-ins disponibles. L'utilisateur choisira un add-in et cliquera sur le bouton Enregistrer afin de sauvegarder le texte saisi dans un format pris en charge par l'add-in.
III-2. Architecture utilisée
Avant de nous jeter dans le code, il nous faut avant tout comprendre comment celui-ci sera structuré. Nous avons vu précédemment des notions de contrat, vue, adaptateur et les références qu'ils avaient les uns sur les autres. Voyons maintenant concrètement comment tout cela va être organisé au niveau code, par rapport à notre exemple de départ.
Le diagramme ci-dessous présente les différentes classes utilisées dans le pipeline et leurs imbrications les unes avec les autres. N'hésitez à revenir y jeter un coup d'oil de temps en temps afin de mieux comprendre la suite de l'article.
Nous retrouvons les différents composants (vue, adaptateur, contrat) présentés précédemment.
III-3. Organisation de la solution
Nous allons tout d'abord créer une nouvelle solution vide dont la structure sera la suivante:
Créez les trois dossiers en faisant un clic droit sur la solution, Add, New Solution Folder.
La création des dossiers n'est pas obligatoire, mais permettra de mieux organiser notre code.
Tout au long du déroulement de l'exemple nous ferons référence aux assemblies System.AddIn et System.AddIn.Contract. Vous les trouverez dans l'onglet .NET de la fenêtre d'ajout de référence:
III-4. Le contrat
Créez un nouveau projet de type Bibliothèque de classes dans le dossier Contracts et nommez-le Contrats.
Dans les propriétés du projet sous l'onglet Build, définissez le chemin de sortie sur ..\output\Pipeline\Contracts. Attention, si vous définissez ce chemin en mode Debug, il faudra le redéfinir en mode Release (idem pour les autres projets).
Le contrat sera défini par une interface héritant de IContract et qui contiendra les opérations que l'hôte fera sur l'add-in. Dans notre cas il n'y aura qu'une seule méthode EnregistrerDoc qui prendra en paramètre l'emplacement où l'on souhaite enregistrer le fichier ainsi que la chaine de caractères à sauvegarder.
N'oubliez pas la référence à System.AddIn.Contract ainsi que l'attribut AddInContract.
| Définition du contrat |
using System.AddIn.Contract;
using System.AddIn.Pipeline;
namespace Contrats
{
[AddInContract]
public interface IContratSauverDoc : IContract
{
bool EnregistrerDoc(String chemin, String texte);
}
}
|
III-5. Coté Add-in
III-5-1. La vue
Créez un nouveau projet de type Bibliothèque de classes dans le dossier Partie_AddIn et nommez-le Addin.Vue.
Dans les propriétés du projet sous l'onglet Build, définissez le chemin de sortie sur ..\output\Pipeline\AddInViews.
La vue sera représentée par une classe abstraite et décorée par l'attribut AddInBase. Ajoutez une référence à System.AddIn.
| Définition de la vue coté add-in |
using System.AddIn.Pipeline;
namespace Addin.Vue
{
[AddInBase]
public abstract class VueAddInSauverDocCotéAddIn
{
public abstract bool EnregistrerDoc(String cheminFichier, String texte);
}
}
|
Cette classe représente la classe de base des add-ins. Toute personne souhaitant développer un add-in pour notre éditeur devra faire dériver son add-in de cette classe. Tout ce que l'add-in connait de l'hôte se trouve dans cette classe. L'add-in sait simplement que l'hôte (via le pipeline) va l'appeler via la fonction EnregistrerDoc.
III-5-2. L'add-in
Chaque add-in dérive de la vue lui correspondant et sera déployé dans son propre répertoire.
Créez un nouveau projet de type Bibliothèque de classes dans le dossier Partie_AddIn\AddIns et nommez-le AddIn.TXT.
Dans les propriétés du projet sous l'onglet Build, définissez le chemin de sortie sur ..\output\Pipeline\AddIns\AddIn.TXT.
Attention à bien séparer les chemins de sortie des Add-ins. Si vous créez un deuxième Add-in (par exemple AddIn.PDF) celui -ci devra avoir pour chemin de sortie ..\output\Pipeline\AddIns\AddIn.PDF.
Ajoutez une référence à la vue (AddIn.Vue) mais veillez à mettre sa propriété Copy Local à FALSE (la dll de la vue ne doit pas être déployée avec l'add-in).
Ajoutez aussi une référence à System.AddIn.
Créez donc une classe SauverEnTXT qui dérive de la classe VueAddInSauverDocCotéAddIn et décorée par l'attribut AddIn. Ce dernier vous permet de spécifier les métadonnées de l'add-in. Il ne reste plus qu'à écrire le code de la méthode EnregistrerDoc.
| Code de l'add-in SauverEnTXT |
using Addin.Vue;
using System.AddIn;
using System.IO;
namespace AddIn.TXT
{
[AddIn("Format texte",
Version = "1.0.0.0",
Description = "AddIn permettant d'enregistrer un document sous format texte",
Publisher = "Badger")]
public class SauverEnTXT : VueAddInSauverDocCotéAddIn
{
public override bool EnregistrerDoc(string cheminFichier, string texte)
{
StreamWriter sw = File.CreateText(cheminFichier);
sw.Write(texte);
sw.Close();
return true;
}
}
}
|
III-5-3. L'adaptateur
L'adaptateur coté add-in permet de convertir la vue de l'add-in pour qu'elle corresponde au Contrat.
Créez un nouveau projet de type Bibliothèque de classes dans le dossier Partie_AddIn et nommez-le AddIn.Adaptateurs. Vous y mettrez toutes les classes servant d'adaptateur pour les différentes versions des vues. Dans notre exemple nous n'aurons besoin que d'un seul adaptateur.
Dans les propriétés du projet sous l'onglet Build, définissez le chemin de sortie sur ..\output\Pipeline\AddInSideAdapters.
Ajoutez une référence à la vue (AddIn.Vue) et au contrat (Contrat) mais veillez à mettre leur propriété Copy Local à FALSE.
Ajoutez aussi des références à System.AddIn et System.AddIn.Contract.
Créez une classe AdaptateurVueAddInVersContratSauverDoc dérivant de ContractBase et implémentant le contrat IContratSauverDoc. N'oubliez pas de décorer la classe avec l'attribut AddInAdapter.
Créez ensuite un constructeur prenant en paramètre la vue qu'il est censé convertir (dans notre cas VueAddInSauverDocCotéAddIn) et gardez une référence à cette vue via un champ privé. Il ne reste plus qu'à écrire le code de la méthode EnregistrerDoc provenant du contrat que nous implémentons.
| AdaptateurVueAddInVersContratSauverDoc |
using System.AddIn.Pipeline;
using Addin.Vue;
using System.AddIn;
namespace AddIn.Adaptateurs
{
[AddInAdapter]
public class AdaptateurVueAddInVersContratSauverDoc : ContractBase, Contrats.IContratSauverDoc
{
private VueAddInSauverDocCotéAddIn _vue;
public AdaptateurVueAddInVersContratSauverDoc(VueAddInSauverDocCotéAddIn vue)
{
this._vue = vue;
}
Bool EnregistrerDoc(string chemin, string texte)
{
return this._vue.EnregistrerDoc(chemin, texte);
}
}
}
|
III-6. Coté Hôte
III-6-1. La vue
La vue coté hôte définit la façon dont l'hôte voit l'add-in. Tout ce que l'hôte connait de l'add-in se trouve dans cette classe. L'hôte ne manipulera au final que des types vue.
Créez un nouveau projet de type Bibliothèque de classes dans le dossier Partie_Hote et nommez-le Hote.Vue.
Dans les propriétés du projet sous l'onglet Build, définissez le chemin de sortie sur ..\output (au même niveau que l'hôte).
La vue que nous appellerons VueAddInSauverDocCotéHote sera représentée par une classe abstraite contenant les méthodes dont se servira l'hôte.
| Définition de la vue coté hôte |
namespace Hote.Vue
{
public abstract class VueAddInSauverDocCotéHote
{
public abstract bool EnregistrerDoc(String cheminFichier, String texte);
}
}
|
III-6-2. L'adaptateur
Il est chargé de convertir le contrat afin qu'il corresponde à ce que voit l'hôte (c'est-à-dire la vue).
Créez un nouveau projet de type Bibliothèque de classes dans le dossier Partie_Hote et nommez-le Hote.Adaptateurs. Vous y mettrez les différents adaptateurs dont vous aurez besoin. Dans notre cas nous n'en créerons qu'un seul.
Dans les propriétés du projet sous l'onglet Build, définissez le chemin de sortie sur ..\output\Pipeline\HostSideAdapters.
Ajoutez une référence à la vue (Hote.Vue) et au contrat (Contrat) mais veillez à mettre leur propriété Copy Local à FALSE (les dll du contrat et de la vue ne doivent pas être déployées avec l'adaptateur).
Ajoutez aussi des références à System.AddIn et System.AddIn.Contract.
Créez une classe AdaptateurContratSauverDocVersVueHote dérivant de la vue VueAddInSauverDocCotéHote. N'oubliez pas de décorer la classe avec l'attribut HostAdapter.
Créez ensuite un constructeur prenant en paramètre le contrat qu'il est censé convertir (dans notre cas IContratSauverDoc) et gardez-en une référence via un champ privé. Il ne reste plus qu'à implémenter la méthode abstraite EnregistrerDoc provenant de la vue dont nous dérivons.
| AdaptateurContratSauverDocVersVueHote |
using Hote.Vue;
using Contrats;
using System.AddIn.Pipeline;
namespace Hote.Adaptateurs
{
[HostAdapter]
public class AdaptateurContratSauverDocVersVueHote : VueAddInSauverDocCotéHote
{
private IContratSauverDoc _contrat;
public AdaptateurContratSauverDocVersVueHote(IContratSauverDoc contrat)
{
this._contrat = contrat;
}
public override bool EnregistrerDoc(string cheminFichier, string texte)
{
return this._contrat.EnregistrerDoc(cheminFichier, texte);
}
}
}
|
III-7. L'hôte
Nous allons faire ici un focus sur l'hôte ainsi que sur les mécanismes permettant la découverte, l'activation et l'utilisation des add-ins.
III-7-1. L'application
Créez un nouveau projet de type Windows Application dans le dossier Partie_Hote et nommez-le Hote.Adaptateurs.
Dans les propriétés du projet sous l'onglet Build, définissez le chemin de sortie sur ..\output. Définissez de plus ce projet en tant que projet de démarrage.
Ajoutez aussi une référence à System.AddIn ainsi qu'à la vue (Hote.Vue). L'hôte n'a besoin d'aucune autre référence (adaptateur ou add-in).
III-7-2. Découverte d'add-ins
L'approche consistant à utiliser des attributs afin de définir un add-in a comme avantage la facilité d'utilisation. Le développeur a juste à créer une classe héritant de la classe abstraite de base, à appliquer l'attribut AddIn sur cette classe et c'est parti ! Cela a cependant l'inconvénient d'alourdir le processus de découverte des add-ins. Il faut en effet examiner chaque assembly afin de déterminer s'il s'agit d'un add-in et auquel cas de quel type d'add-in nous avons à faire. Ce processus est trop lourd pour être exécuté par exemple à chaque démarrage de l'application hôte. Il existe donc un système permettant de mettre en cache les informations concernant les add-ins afin d'y accéder sans devoir ré-inspecter chaque assembly à chaque fois.
III-7-2-1. La mise en cache des informations
La méthode static Update de la classe AddInStore va effectuer tout le travail pour nous. Cette méthode prend en paramètre le chemin du répertoire du pipeline et va l'examiner ainsi que tous ses sous répertoire à la recherche d'add-ins (type ayant l'attribut AddInAttribute) et de composants du pipeline.
Pour chaque add-in trouvé elle va enregistrer les informations le concernant dans un fichier de cache nommé AddIns.store qui sera créé dans le répertoire des add-ins.
Chaque composant du pipeline verra aussi ses informations stockées dans un fichier PipelineSegments.store situé à la racine du répertoire spécifié en argument.
AddInStore.Update(addinRoot);
|
Le processus de découverte s'effectue dans un domaine d'application dédié et les informations concernant chaque add-in sont obtenues en utilisant la réflexion. Cela assure qu'aucun code contenu dans les assemblies étudiées ne sera jamais exécuté (aucun add-in n'est encore actif !) et évite d'encombrer le domaine d'application de l'hôte.
Notez qu'un deuxième appel à cette méthode ne réenclenchera le processus que s'il y a eu des modifications depuis le premier appel (ajout d'un nouvel add-in par exemple).
En plus de la création de ce cache en cours d'exécution de l'hôte, il existe un outil nommé AddInUtil.exe qui peut effectuer ce travail (lors d'une installation personnalisée ou à partir d'un script par exemple).
III-7-2-2. Recherche d'add-ins
Maintenant que les informations concernant les différents add-ins de l'hôte sont en cache, il devient aisé de rechercher un type d'add-in en particulier.
La classe AddInStore propose une méthode static FindAddIns prenant en paramètre le type que le hôte veut utiliser pour communiquer avec les add-ins (la vue) et un chemin et qui retourne une collection d'objets de type AddInTokens représentant les add-ins trouvés. Cette méthode va bien entendu aller chercher la liste des add-ins dans le fichier de cache. Si celui-ci n'existe pas, la collection renvoyée sera vide. Cette méthode peut être utilisée lors du chargement de l'application. Le fichier de cache est gardé en mémoire afin d'éviter les accès disque (il est bien sûr rechargé si besoin).
string addinRoot = Environment.CurrentDirectory;
AddInStore.Update(addinRoot);
Collection<AddInToken> addins = AddInStore.FindAddIns(typeof(VueAddInSauverDocCotéHote), addinRoot);
|
Un objet AddInTokens permet d'activer l'add-in qu'il représente. Il contient de plus des métadonnées sur l'add-in que l'hôte peut utiliser (nom, version, description). Ces informations ont été récupérées lors de la création du cache (méthode Update) et la méthode FindAddIns ne fait que les lire. Cela permet à l'hôte d'obtenir des informations sur les add-ins et choisir lesquels activer sans avoir à exécuter un quelconque code contenu dans les add-ins.
Une fois la liste des add-ins disponibles connue, nous pouvons remplir la combobox (dans notre exemple) qui servira à choisir la méthode de sauvegarde pour notre application:
this.comboBoxFormats.DataSource = addins;
this.comboBoxFormats.DisplayMember = "Name";
|
III-7-3. Activation
Une fois que la liste des add-ins disponibles est chargée et que vous avez sélectionné l'add-in que vous voulez utiliser, il reste une dernière étape avant son utilisation: son activation.
Coté hôte l'activation se fait en une ligne de code et permet de renvoyer un objet de type VueAddInSauverDocCotéHote (qui est la classe représentant la vue de l'add-in par l'hôte):
AddInToken addinToken = this.comboBoxFormats.SelectedItem as AddInToken;
VueAddInSauverDocCotéHote addinInstance = addinToken.Activate<VueAddInSauverDocCotéHote>(AddInSecurityLevel.Internet);
|
Nous utilisons la méthode Activate de l'objet AddInTokens (voir plus haut) représentant l'add-in à activer. Il existe trois versions de la méthode Activate prenant en charge des paramètres différents. Mais dans tous les cas il s'agit de définir les autorisations de sécurité qui seront assignées à l'add-in. Dans l'exemple ci-dessus nous lui passons en paramètre le niveau de sécurité correspondant au jeu d'autorisations Internet. Il existe d'autres jeux d'autorisations prédéfinis que vous pouvez utilisés:
Si vous ne trouvez pas votre bonheur dans cette liste, sachez qu'une autre version de la méthode Activate permet de passer en paramètre votre propre jeu d'autorisations. Une troisième version permet de fournir votre propre AppDomain.
Mais derrière cette relative simplicité (une ligne de code) se cache un mécanisme quelque peu plus complexe. Vous n'y avez peut-être pas prêté attention mais si vous regardez le code que nous avons écrit jusqu'à présent vous remarquerez que nous n'avons pas instancié le moindre objet. Nous n'avons créé ni add-in ni adaptateur et pourtant nous nous retrouvons (grâce à la méthode Activate) avec une instance coté hôte que nous allons pouvoir utiliser. Que s'est-il donc passé lors de l'appel à cette méthode ? Voyons un peu plus en détail les différentes étapes par lesquelles est passé le système. N'hésitez pas à vous référer au schéma représentant l'organisation de l'exemple pour mieux comprendre.
- Un domaine d'application (appDomain) est créé avec les permissions spécifiées lors de l'appel à la méthode Activate.
- L'assembly contenant l'add-in est chargée dans cet appDomain à travers un appel à la méthode Assembly.LoadFrom().
- Le constructeur sans argument de l'add-in est appelé (via réflexion) pour en créer une instance dans cet appDomain. Comme l'add-in dérive d'une classe abstraite (dans notre cas VueAddInSauverDocCotéAddIn) se trouvant dans une autre assembly, cette dernière est aussi chargée dans l'appDomain. A ce point nous avons donc une instance de l'add-in s'exécutant dans son propre appDomain. Il nous faut maintenant la connecter à l'hôte.
- Une instance de l'adaptateur coté add-in (dans notre cas AdaptateurVueAddInVersContratSauverDoc) est construite en lui passant en paramètre de son constructeur l'instance de notre add-in (typée en tant que VueAddInSauverDocCotéAddIn). Cet adaptateur implémente le contrat (IContratSauverDoc) et dérive de MarshalByRefObject par l'intermédiaire de la classe ContractBase. Ainsi, il peut "vivre" à la fois dans l'appDomain de l'hôte et dans celui de l'add-in. Tous les appels provenant de l'appDomain de l'hôte passeront au travers d'un proxy et seront exécutés dans l'appDomain de l'add-in.
- Le code d'activation renvoie à l'appDomain de l'hôte l'adaptateur typé en tant que contrat qu'il implémente (dans notre cas IContratSauverDoc).
- Une instance de l'adaptateur coté hôte (AdaptateurContratSauverDocVersVueHote) est construite dans l'appDomain de l'hôte avec l'adaptateur coté add-in (AdaptateurVueAddInVersContratSauverDoc) passé en paramètre de son constructeur.
- Enfin, le code d'activation retourne à l'hôte l'adaptateur coté hôte (AdaptateurVueAddInVersContratSauverDoc) typé en tant que vue (VueAddInSauverDocCotéHote).
III-7-4. Utilisation
L'activation nous a renvoyé un objet de type VueAddInSauverDocCotéHote (qui est classe représentant la vue de l'add-in par l'hôte). Nous pouvons donc utiliser cet objet et appeler sa méthode qui permet l'enregistrement du texte dans un fichier:
addinInstance.EnregistrerDoc(chemin, richTextBox1.Text);
|
En bout de chaîne le code de l'add-in est exécuté et un fichier texte est créé contenant le texte tapé dans la richTextBox. Du mois, en théorie. Car en pratique l'exécution de ce code va provoquer une jolie exception:
Il s'agit d'une exception de sécurité nous informant que le code de notre add-in ne dispose pas des droits pour pouvoir accéder au système de fichier de l'ordinateur. Pour mieux comprendre, remontons un peu en arrière lors de la phase d'activation. Nous avions en effet activé l'add-in avec le jeu de sécurité correspondant à la zone Internet. L'add-in ne possède ainsi que peu de droits, et n'a notamment pas accès au système de fichier de l'ordinateur. Pour résoudre ce problème nous avons plusieurs solutions. Nous pourrions modifier le jeu de sécurité et en choisir un qui accorde plus de droits à l'add-in:
AddInSecurityLevel.FullTrust
|
Cependant l'add-in se retrouverai ici avec plus de droits que nécessaire.
La meilleure méthode (tout est relatif) est de construire un jeu personnalisé de permissions:
| Jeu personnalisé de permissions |
PermissionSet perms = new PermissionSet(PermissionState.None);
perms.AddPermission(new SecurityPermission(SecurityPermissionFlag.Execution));
perms.AddPermission(new FileIOPermission(FileIOPermissionAccess.AllAccess, chemin));
try
{
VueAddInSauverDocCotéHote addinInstance = addinToken.Activate<VueAddInSauverDocCotéHote>(perms);
addinInstance.EnregistrerDoc(chemin, richTextBox1.Text);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
|
Pour cela nous créons un objet PermissionSet qui contiendra les différentes permissions accordées à l'add-in (plus exactement au domaine d'application de l'add-in). Puis nous y ajoutons deux permissions.
La première est une permission que l'on oublie parfois (et l'on reste ainsi longtemps à se demander pourquoi le code ne fonctionne pas) mais qui est indispensable dans notre cas: c'est la permission donné au code de s'exécuter. Sans cette autorisation, le code managé ne peut pas s'exécuter. Vous pouvez donc donner tous les autorisations que vous voulez, si l'autorisation d'exécution n'est pas présente cela ne servira à rien.
Une fois l'autorisation d'exécution donnée, il faut donner à l'add-in le droit en écriture afin qu'il puisse créer le fichier texte demandé. C'est ce que nous faisons en créant un objet FileIOPermission et en lui passant en paramètre le type d'accès de fichier demandé (dans notre cas, tous les accès) et le fichier sur lequel s'exercent ces autorisations. Nous donnons ainsi ici un accès total seulement à l'emplacement où l'on veut sauvegarder le fichier.
Et cette fois-ci le code s'exécute sans problème. Vous pouvez donc jouer très finement avec les autorisations qui seront accordées aux add-ins de vos applications.
III-7-5. Déchargement
Il est possible à l'hôte de décharger l'add-in après utilisation. Pour cela, une ligne de code suffit:
AddInController.GetAddInController(addinInstance).Shutdown();
|
La méthod ShutDown coupe le pipeline communication entre l'add-in et l'hôte. Elle se charge aussi de décharger le domaine d'application de l'add-in.
VI. Compatibilité ancien add-in, nouvel hôte
Nous venons donc de voir comment mettre en place un mécanisme de gestion d'add-in dans une application. Comme indiqué au début de l'article, la création du pipeline a nécessité un certain travail pour la première version de l'application. Mais le fait d'avoir pris la peine de développer la gestion d'add-in suivant ce modèle va maintenant nous être bénéfique.
Nous allons en effet effectuer une mise à jour de notre éditeur et voir comment rendre les anciens add-ins (développés pour la version 1) compatibles avec cette nouvelle version.
VI-1. Présentation de la mise à jour
La modification apportée sur la version 2 de l'application va être très simple, il s'agit ici simplement de montrer le mécanisme permettant la compatibilité descendante avec les add-ins.
Dans la première version de l'application, nous voulions que les add-ins permettent l'enregistrement d'une chaîne de caractères dans un fichier. C'était là le seul service que l'hôte demandait à ses add-ins. Dans la version 2 de l'application nous souhaitons que les add-ins puissent en plus renvoyer sous forme de chaîne de caractères l'extension correspondante au format du fichier créé. Par exemple, un add-in permettant de sauvegarder sous format PDF devra renvoyer "PDF".
Vous devez surement vous douter du problème que nous allons devoir traiter. Effet, l'add-in de sauvegarde au format texte que nous avons développé n'offre absolument pas cette fonctionnalité. Nous allons donc étudier comment continuer tout de même à utiliser cet add-in (sans le modifier) avec la version 2 de l'application.
VI-2. Nouveau contrat
Le contrat ne devant pas être "versionné", vous ne pouvez pas le modifier. Vous êtes obligés d'en créer un nouveau et de supprimer l'ancien.
Dans le projet Contrats supprimez le contrat initial (IContratSauverDoc) et créez-en un nouveau (nouvelle classe) que vous appellerez IContratSauverDocV2:
| ContratV2 |
using System.AddIn.Contract;
using System.AddIn.Pipeline;
namespace Contrats
{
[AddInContract]
public interface IContratSauverDocV2 : IContract
{
bool EnregistrerDoc(String chemin, String texte);
String GetExtentionDoc();
}
}
|
N'oubliez pas de définir son chemin de sortie sur ..\output\Pipeline\Contracts\.
VI-3. Modifications coté hôte
VI-3-1. La vue
L'hôte attend une nouvelle fonctionnalité de l'add-in. Il faut donc refléter ce changement dans la façon dont l'hôte voit l'add-in, c'est-à-dire dans la vue VueAddInSauverDocCotéHote.
Pour cela, nous allons modifier la vue et lui rajouter la méthode GetExtentionDoc qui permettra à l'hôte d'utiliser cette nouvelle fonctionnalité.
namespace Hote.Vue
{
<summary>
</summary>
public abstract class VueAddInSauverDocCotéHote
{
public abstract bool EnregistrerDoc(String cheminFichier, String texte);
public abstract String GetExtentionDoc();
}
}
|
VI-3-2. L'adaptateur
Nous modifions aussi l'adaptateur coté hôte.
using Contrats;
using System.AddIn.Pipeline;
using Hote.Vue;
namespace Hote.Adaptateurs
{
<summary>
</summary>
[HostAdapter]
public class AdaptateurContratSauverDocVersVueHote : VueAddInSauverDocCotéHote
{
private IContratSauverDocV2 _contrat;
<summary>
</summary>
<param name="contrat"></param>
public AdaptateurContratSauverDocVersVueHote(IContratSauverDocV2 contrat)
{
this._contrat = contrat;
}
public override bool EnregistrerDoc(string cheminFichier, string texte)
{
return this._contrat.EnregistrerDoc(cheminFichier, texte);
}
public override string GetExtentionDoc()
{
return this._contrat.GetExtentionDoc();
}
}
}
|
IV-3-3. L'hôte
Au sein de l'hôte nous n'effectuerons que très peu de changements. Après l'appel à la méthode EnregistrerDoc nous affichons une boite de dialogue indiquant le format d'enregistrement:
MessageBox.Show("Texte sauvegardé sous format: "+addinInstance.GetExtentionDoc());
|
Comme vous le voyez, les changements dans le pipeline coté hôte se résument dans notre cas à la modification des classes afin de prendre en compte la nouvelle fonctionnalité.
VI-4. Création d'un add-in V2
Nous allons voir ici comment créer un add-in spécialement conçu pour la version 2 de l'application. Les étapes seront pratiquement les mêmes que celles vues en début d'article. La partie suivante sera consacrée à la compatibilité entre la nouvelle version de l'application et une ancienne d'un add-in.
VI-4-1. La vue
Dans le dossier Partie_AddIn créez un nouveau projet de type bibliothèque de classes et nommez-le AddIn.VueV2.
Créez une classe VueAddInSauverDocCotéAddInV2:
using System.AddIn.Pipeline;
namespace Addin.Vue
{
<summary>
</summary>
[AddInBase]
public abstract class VueAddInSauverDocCotéAddInV2
{
public abstract bool EnregistrerDoc(String cheminFichier, String texte);
public abstract String GetExtentionDoc();
}
}
|
Cette classe définit la façon dont l'add-in sera appelée par l'application version 2.