Création d'un Addin pour Visual Studio .Net

MORAND Louis-Guillaume

Tutoriel pas à pas expliquant le principe, le développement et l'intégration d'un addin dans Visual Studio .Net.

N'hésitez pas à commenter cet article ! Commentez ♪

- Titre : Création d'un Addin pour Visual Studio .Net

- Auteur : MORAND Louis-Guillaume

- Parution : 25 octobre 2004

- Mise à jour : 20 mars 2005

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2004 MORAND Louis-Guillaume. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- Avant-propos

- I. Création du projet

- I-A. Introduction

- I-B. Le projet de complément

- II. Fonctionnement d'un add-in

- II-A. Structure d'un add-in

- II-B. Intégration dans Visual Studio .Net

- II-C. Interaction avec Visual Studio .Net

- III. Add-in : Project Manager

- III-A. Description

- III-B. Développement de l'interface de l'add-in

- III-C. Développement du code fonctionnel

- IV. Add-in : Code Improver

- IV-A. Description

- IV-B. Intégration de l'add-in

- IV-C. Développement du code fonctionnel

- V. Interactions possibles avec Visual Studio .Net

- V-A. Ajouter un fichier au projet

- V-B. Travailler avec le dossier racine de l'addin

- VI. Déploiement de l'add-in

- VI-A. Préparation du setup

- VI-B. Configuration du setup

- Conclusion

- Téléchargements

Avant-propos

Visual Studio .Net est connu pour être le logiciel le plus complet du marché du développement. Non content de permettre le développement d'un grand nombre de langages, il permet également l'utilisation des dernières technologies et fournit une interface intuitive et productive, facilitant le développement de toutes sortes de projets. Que vous développiez un utilitaire, un logiciel de gestion, un progiciel, une interface avec une base de données, ou autre, Visual Studio .Net vous facilite les choses de nombreuses manières.
Néanmoins, malgré une évolution importante et constante (voir la prochaine version : Whidbey), Visual Studio .Net ne contient peut-être pas la fonction de vos rêves : celle qui vous permettrait de vous simplifier le travail et/ou réduire le temps de développement. Pourquoi alors ne pas prendre quelques heures pour développer son propre add-in pour, par la suite, gagner énormément de temps ?
C'est ce que je vous propose ici de faire et nous allons donc voir comment développer son add-in pour Visual Studio .Net.

Notez que nous créerons un « add-in » et non pas un « add-on », car il sera totalement intégré à Visual Studio .Net et interagira directement avec lui. Dans ce tutoriel, j'appellerai également notre add-in un complément, car c'est ainsi que l'on nomme un add-in qui ne fait pas partie des « assistants » de Visual Studio .Net (templates de projet, wizard, etc.).

I - Création du projet

I-A - Introduction

Un add-in est une partie complémentaire d'un logiciel de base, il peut être sous la forme d'un menu principal, un menu contextuel ou même une fonction d'arrière-plan. Ici, nous verrons le fonctionnement et la création de compléments, puis nous verrons le développement d'un add-in permettant de fournir un rapport détaillé du projet en cours (arborescence, statistiques). Puis, dans un deuxième temps, nous verrons l'interaction d'un add-in avec les fichiers code via un menu contextuel.
Vous aurez, à la fin de ce cours, vu plusieurs des différentes manières de s'intégrer dans Visual Studio et interagir avec lui.

I-B - Le projet de complément

Lancez Visual Studio .Net puis cliquez sur le menu Fichier > Nouveau > Projet.
Dans la fenêtre qui s'ouvre, choisissez alors Autres Projets > Projets d'Extensibilité puis un complément de Visual Studio

Donnez alors un nom à votre projet. Puis cliquez sur OK. Un nouveau panneau s'affiche alors.

Cliquez sur suivant.

Ici, nous développerons un add-in en C#, choisissez donc:
Créer un complément à l'aide de Visual C# (libre à vous de le faire dans un autre langage par la suite)

Choisissez maintenant, où vous souhaitez que votre plugin s'intègre. Simplement dans l'ide Microsoft Visual .Net, dans l'éditeur de Macro ou dans les deux. L'éditeur de macros est un deuxième IDE servant à l'édition de macros principalement en VBA

Choisissez le nom de votre add-in (le nom sous lequel il apparaitra dans la liste des add-ins) ainsi qu'une rapide description.

Vous pouvez alors personnaliser votre add-in.
Choisissez d'abord si vous souhaitez que votre add-in soit accessible via le menu outils. (note: vous pouvez ne pas la cocher et par la suite, je montrerai comment rajouter des menus. Mais la cocher autogénère le code qu'il est plus facile de modifier)
Choisissez si votre add-in pourra avoir une interface graphique ou non, puis choisissez s'il se charge directement au lancement de Visual Studio .Net ou non.
Enfin, cochez la dernière case si vous souhaitez qu'il soit intégré à Visual Studio .Net pour n'importe quelle personne utilisant l'ordinateur sur lequel il sera installé.

Ces informations apparaitront dans les détails de l'add-in dans la fenêtre « A propos » de l'application hôte (ici Visual Studio .Net)

Le panneau récapitulatif. Cliquez alors sur Terminer et Visual Studio .Net se charge se générer automatiquement tout le squelette de votre add-in.

Vous pouvez alors noter que toute la structure de votre add-in est déjà générée et commentée. Cette structure ne doit pas être modifiée, car elle doit suivre certaines restrictions pour s'adapter à Visual Studio et possède certaines méthodes qui lui sont nécessaires pour fonctionner. Voyons maintenant le fonctionnement d'un add-in.

II - Fonctionnement d'un add-in

Avant d'aller plus loin dans le développement de notre add-in, il est important de comprendre sa structure, tant au niveau de l'arborescence des fichiers et de la nomenclature de ceux-ci, que du code structuré et lui aussi nomenclaturé.

II-A - Structure d'un add-in

Regardons tout d'abord la structure du projet :

Notons tout d'abord le fichier assembly.cs qui contient l'assembly de notre projet et qu'il nous faudra remplir (nom, description, version) par la suite. Notons ensuite le très important connect.cs. Ce fichier est obligatoire, il sert en sorte de super constructeur: c'est la classe qui est appelée en premier lieu lors du chargement de l'add-in. Il est indispensable de ne pas le renommer et c'est dans ce dernier que ce retrouvera une grosse partie du code fonctionnel à moins que vous préfériez (en fonction de la taille de l'add-in) séparer les fonctions dans diverses classes.
Notez également que Visual Studio Projet a automatiquement généré un projet de déploiement (setup) pour pouvoir redistribuer facilement l'add-in.

Ouvrons maintenant le fichier connect.cs.
Tout d'abord, le namespace. Même s'il est créé automatiquement il ne faut pas le modifier sous peine de voir arriver des problèmes avec les autres classes du projet.

namespace NarfAddin

Puis l'utilisation d'autres namespaces permettant d'appeler des fonctions.

using System;
using Microsoft.Office.Core;
using Extensibility;
using System.Runtime.InteropServices;
using EnvDTE;

Puis vient un minireadme qui explique que pour de nombreux cas, l'intégration de l'add-in peut ne plus se faire(bug reconnu :D). Le premier réflexe serait de le supprimer, néanmoins je vous conseille de le garder, au moins pour vous rappeler qu'il vous suffit d'exécuter le fichier .reg qui se trouve dans le dossier du projet pour « réparer » l'add-in.

#region Read me for Add-in installation and setup information.
// When run, the Add-in wizard prepared the registry for the Add-in.
// At a later time, if the Add-in becomes unavailable for reasons such as:
// 1) You moved this project to a computer other than which is was originally created on.
// 2) You chose 'Yes' when presented with a message asking if you wish to remove the Add-in.
// 3) Registry corruption.
// you will need to re-register the Add-in by building the MyAddin21Setup project
// by right clicking the project in the Solution Explorer, then choosing install.
#endregion

Voici ensuite notre classe principale intégrant des interfaces spécifiques à l'interaction : Extensibility.IDTExtensibility2, IDTCommandTarget. Notez aussi l'attribution automatique d'un GUID (unique) à notre add-in.

[GuidAttribute("A8989C04-130B-4F30-913C-7ED1BF6107B8"), ProgId("NarfAddin.Connect")]
public class Connect : Object, Extensibility.IDTExtensibility2, IDTCommandTarget

Nous avons ensuite le constructeur. C'est dans celui-ci que nous pouvons mettre notre code d'initialisation (que nous verrons par la suite)

constructeur

public Connect()
{
}

Celui-ci est vide, mais comme tout constructeur il est forcément appelé et sert à mettre votre code d'initialisation, comme toute application normale.
Nous allons maintenant voir ce qu'il lui permet de s'intégrer dans Visual Studio .Net.

Un add-in utilise l'interface IDTExtensibility2 qui fournit cinq événements de base et qui correspondent aux événements courants d'un add-in. Ces méthodes permettent à un programme de jouer le rôle de complément en répondant à des conditions de démarrage et d'arrêt de l'environnement, des événements de connexion de complément, etc.

L'événement OnConnection est déclenché lorsque l'add-in est chargé (connecté). Un add-in se charge dans différents cas :
- l'utilisateur charge l'add-in via la boite de dialogue des Add-ins ;
- l'add-in est chargé en même temps que l'appli, lorsqu'il a été configuré pour démarrer avec celle-ci ;
- la propriété Connect d'un objet COMAddIn correspondant est réglé sur : True. L'objet COMAddIn représente un add-in COM dans l'application Office

méthode OnConnection

public void OnConnection(object application, Extensibility.ext_ConnectMode connectMode, object addInInst,
ref System.Array custom)
{
}

L'événement OnDisconnection est déclenché lorsque l'add-in est déchargé. Il peut l'être de deux manières :
- l'utilisateur le décharge via la fenêtre des Add-ins ;
- l'application hôte est fermée, donc l'add-in aussi par la même occasion.
Astuce : si vous souhaitez que les actions faites par l'add-in dans l'application soient annulées pour le prochain démarrage (recherche, autre), vous pouvez mettre le code de nettoyage dans cette méthode.

méthode OnDisconnection

public void OnDisconnection(Extensibility.ext_DisconnectMode disconnectMode, ref System.Array custom)
{
}

L'événement OnAddinsUpdate est déclenché lorsque la liste du Gestionnaire de compléments est modifiée ; lorsqu'un add-in est chargé ou déchargé.

méthode OnAddInsUpdate

public void OnAddInsUpdate(ref System.Array custom)
{
}

L'événement OnStartupComplete est déclenché lorsque l'add-in et l'application hôte ont fini de charger toutes leurs routines de démarrage. Si l'application est chargée complètement, mais si l'add-in n'est pas chargé au démarrage alors cet événement ne s'effectue pas, même si par la suite, l'add-in est chargé via la boite de dialogue des add-ins. Vous pouvez utiliser l'événement OnStartupComplete pour interagir avec l'application. Par exemple, proposez des choix à l'utilisateur au démarrage de visual studio pour créer différents documents.

méthode OnStartupComplete

public void OnStartupComplete(ref System.Array custom)
{
}

L'événement OnBeginShutdown est déclenché lorsque l'application hôte lance sa routine d'extinction. Cet événement est utile pour agir avant que l'application ne se ferme (autosave par exemple).

méthode OnBeginShutdown

public void OnBeginShutdown(ref System.Array custom)
{
}

Un add-in contient également deux méthodes de base :
- la première méthode : QueryStatus() est appelée par l'IDE pour vérifier si une commande est appropriée ou non dans l'état courant.

méthode QueryStatus

public void QueryStatus(string commandName, EnvDTE.vsCommandStatusTextWanted neededText,
ref EnvDTE.vsCommandStatus status, ref object commandText)
{
}

- la méthode Exec est elle, appelée par l'IDE pour effectuer la commande courante. (en fonction du menu choisi par exemple)

méthode Exec

public void Exec(string commandName, EnvDTE.vsCommandExecOption executeOption, ref object varIn,
ref object varOut, ref bool handled)
{

}

Plus clairement, ces deux méthodes sont utilisées pour l'une pour interagir avant l'exécution d'une commande, pour voir si le menu est valide dans un cas précis, l'autre pour lancer les commandes en fonction du menu sélectionné.

II-B - Intégration dans Visual Studio .Net

Un add-in est un petit programme intégré dans un second programme (ici dans Visual Studio) et peut s'intégrer de deux manières :
- la première, invisible, peut-être un utilitaire qui se lance en arrière-plan à chaque démarrage de Visual Studio ;
- la deuxième se vérifie par la présence d'un menu ou plusieurs menus.
Pour un add-in qui tourne en arrière plan, il suffit d'ajouter le code fonctionnel dans la méthode OnConnection et ainsi l'add-in sera lancé (selon vos options) avec Visual Studio .Net et débutera directement son fonctionnement.
Mais pour des raisons fonctionnelles et comme nous verrons les cas des deux chapitres suivants, il peut être intéressant ou tout simplement nécessaire d'ajouter des menus à l'environnement de Visual Studio afin de récupérer les choix de l'utilisateur.
L'ajout d'un menu se fait toujours de la même manière: on crée un objet Command (qui représente le menu) et que l'on ajoute dans une CommandBar (menu déjà existant).

création d'un menu dans la méthode OnConnection

applicationObject = (_DTE)application;
addInInstance = (AddIn)addInInst;

if(connectMode == Extensibility.ext_ConnectMode.ext_cm_UISetup)
{
object []contextGUIDS = new object[] { };
Commands commands = applicationObject.Commands;
_CommandBars commandBars = applicationObject.CommandBars;
try
{
CommandBar commandBar = (CommandBar)commandBars["Code Window"];
Command command = commands.AddNamedCommand(addInInstance,
"NomUniqueDeMonMenu",
"Texte du menu ",
"Tooltip de mon menu ",
true,
1,
ref contextGUIDS,
(int)vsCommandStatus.vsCommandStatusSupported+
(int)vsCommandStatus.vsCommandStatusEnabled);
CommandBarControl commandBarControl = command.AddControl(commandBar, 1); // j'ajoute mon menu à la barre d'outils
}
catch
{
}
}

Reprenons la structure de l'ajout d'un menu via la méthode AddNamedCommand( ) :

paramètres de création d'un menu

commands.AddNamedCommand(
addInInstance,    // Instance de notre Add-in (autogénéré pas VS et se trouvant au début de OnConnection( )
"NomMenu",        // Nom unique du menu. Donnera : Addin.progid.<le nom choisi>
"Texte du menu",  // Texte du menu (le texte affiché :D)
"Tooltip",        // Tooltip pour donner plus d'explications
true,             // si le menu est un MSOButton ou non (bouton utilisant une image office)
1,                // id d'un bitmap dans les ressources Office (car true). Image dessinée devant le menu
ref contextGUIDS, // contextes d'environnement (mode débogage, mode design, etc.) activent la commande
(int)blabla....   // flags d'activation ou non de la commande
)

Si vous souhaitez lancer telle ou telle action en fonction du menu sélectionné (dans le cas de plusieurs menus pour le même add-in) il vous faudra le faire dans la méthode Exec( ) où le menu sera représenté par NomAddin.Connect.NomMenu.

exemple

public void Exec(string commandName, EnvDTE.vsCommandExecOption executeOption, ref object varIn,
ref object varOut, ref bool handled)
{
if(commandName == "MonAddin.Connect.Menu1")
{
// je fais l'action du Menu1
}
if(commandName == "MonAddin.Connect.Menu2")
{
// je fais l'action du Menu2
}
}

Dernière chose : selon certains cas, pour mieux contrôler votre add-in, vous devrez désactiver un ou plusieurs menus.
Cela se fait généralement dans la méthode QueryStatus qui retourne (ou définit) le status d'un menu avant qu'il ne soit sélectionné (apparition du menu contextuel par exemple). status y représente l'état du menu en question :

// activation
status = (vsCommandStatus)vsCommandStatus.vsCommandStatusSupported | vsCommandStatus.vsCommandStatusEnabled;
// désactivation
status = (vsCommandStatus)vsCommandStatus.vsCommandStatusUnsupported;

II-C - Interaction avec Visual Studio .Net

Visual Studio .Net contient une interface acceptant des compléments et propose également des méthodes pour agir avec lui. Il propose également certaines variables permettant d'accéder à un certain nombre d'informations de Visual Studio ou des projets qu'il contient. Voici quelques exemples de ces variables.

Nous avons tout d'abord, la variable de type _DTE qui représente Visual Studio en lui même. Elle est toujours déclarée ainsi

private _DTE applicationObject;

et se définit de la sorte :

applicationObject = (_DTE)application;

ApplicationObject vous fournira alors plusieurs méthodes permettant soit de récupérer des informations sur Visual Studio (projets chargés, options de ligne de commande) soit de modifier son environnement (afficher/cacher des barres, désactiver les tooltip, etc.).

Il existe également la variable de type Addin

private AddIn addInInstance;
addInInstance = (AddIn)addInInst;

Vous pourrez ainsi récupérer quelques informations sur l'addin en lui même par exemple, le chemin de sa dll (son chemin d'installation).
Ces deux objets vous permettent d'agir totalement sur Visual Studio, vous pouvez aller de la modification de son interface à son utilisation. par exemple, il est possible d'utiliser la fenêtre de sortie pour y afficher du texte ou utiliser la progressbar de la barre de status pour y décrire l'avancement d'une action de votre complément :

applicationObject.StatusBar.Progress(parametres);

III - Add-in : Project Manager

III-A - Description

Un article étant plus parlant avec un exemple concret, nous allons donc voir le code complet d'un petit add-in. Dans ce chapitre, nous ferons un add-in avec une interface graphique permettant de faire des exports des informations du projet (arborescence, statistiques,etc.).

III-B - Développement de l'interface de l'add-in

Pour cet add-in, nous aurons besoin d'une interface utilisateur bien plus développée que le simple menu dans outils. Créons donc une nouvelle fenêtre.
Dans l'explorateur de solution, faites bouton droit > ajouter > ajouter un formulaire windows.
Vous pouvez alors donner l'interface graphique que vous souhaitez. De mon côté, j'ai fait celle-ci pour qu'elle soit la plus simple et fonctionnelle possible, mais vous pouvez faire autrement, il vous faudra juste quelque peu éditer le code.

Vous pouvez remarquer les futures options à implémenter: un export dans un dossier choisi contenant :
- un fichier de rapport (arborescence, infos, détails) ;
- une copie du projet ;
- tout en gérant plusieurs langages(C#, VB.Net,etc.) de projets.

III-C - Développement du code fonctionnel

Il ne m'est pas possible ici de montrer entièrement le code de l'add-in et je ne montrerai que les parties du code que j'estime intéressantes pour le fonctionnement de l'add-in ou pour leur « difficulté » relative. Néanmoins, je fournis les sources complètes et commentées à la fin de ce chapitre

La première chose dans notre add-in pour qu'il puisse fonctionner est de récupérer la liste de tous les projets chargés.

Récupération de la liste des projets chargés

System.Array theProjects = (System.Array)applicationObject.ActiveSolutionProjects;

Puis, pour ne travailler que sur un projet (dans mon cas d'exemple), je crée un objet représentant mon projet.

EnvDTE.Project theProject = null;
// On vérifie que la solution contient au moins un projet
if (theProjects.Length > 0)
{
theProject = (EnvDTE.Project)(theProjects.GetValue(0)); // je récupère uniquement le premier projet
// voir la source pour la gestion de plusieurs projets
}

Pour la suite, il nous faudra créer notre fichier d'export puis un streamWriter dessus qui, au long des différentes méthodes, permettra de le compléter.

FileInfo f= new FileInfo(theProject.FullName);
StreamWriter strW = new StreamWriter(TB_Chemin.Text+"\\[export] "+ f.Name.Substring(0,f.Name.Length-7) +".htm");

J'ai ici choisi délibérément de travailler sur le fichier du projet, puis formater son nom après, car le FileInfo nous servira dans de nombreux cas par la suite. Dans d'autres types de projets, pour avoir le nom du projet, il est plus simple d'utiliser :

applicationObject.Solution.FileName

Nous allons maintenant récupérer la liste des objets du projet SANS passer par un listing du dossier afin d'avoir un listing propre ne contenant pas les fichiers « extérieurs », présents dans le dossier.

foreach (ProjectItem p in theProject.ProjectItems)
{
try
{
Compteur=Compteur+1; // compteur de fichier

FileInfo Fichier=new FileInfo(f.DirectoryName+"\\"+p.Name);
string taille=(CKB_Det.Checked)?" ("+ FormatSize(Fichier.Length)+")":""; // formattage (ko,Mo,etc.)
strW.WriteLine("<FONT class=content>- "+p.Name+ taille+"</FONT><br>"); // j'écris le nom du fichier et sa taille

// On vérifie que les fichiers correspondent au type d'export souhaité
if (Fichier.Extension == sFiltre) // le filtre étant récupéré dans un switch en fonction de la combobox du langage
{
SpecCompteur = SpecCompteur+1; // compteur de fichier code
TotLiCompteur=TotLiCompteur+CompteLigne(f.DirectoryName+"\\"+p.Name); // appel de la fonction CompteLigne( )
}
}
catch{}
}

Voici le code de la fonction CompteLigne( ) retournant pour le fichier passé en paramètre, le nombre de lignes de ce fichier. On ne peut malheureusement pas « deviner » le nombre de linges d'un fichier sans compter les lignes une par une. Heureusement, ce traitement reste très rapide.

#region Fonction CompteLigne
/// <summary>
/// Fonction retournant le nombre de lignes du fichier passé en paramètre.
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
private int CompteLigne(string path)
{
StreamReader srFile = new StreamReader(path);    // création d'un streamreader sur le fichier de code
string strLine=string.Empty;                                    // variable contenant la ligne courante
int LiCompteur=0;                                                     // compteur de lignes
while ((strLine = srFile.ReadLine()) != null)
{
LiCompteur++;
}
srFile.Close();                                                            // je ferme le streamreader
return LiCompteur;                                                 // je retourne le nombre de lignes de ce fichier
}
#endregion

Voici maintenant une petite fonction qui va redessiner un arbre avec images représentant l'arborescence complète du projet. La récursivité étant nécessaire, il fallait ici représenter les différents niveaux de l'arborescence, de travailler sur les fichiers et les dossiers, et le tout en un minimum de ligne.

#region Fonction Arbo
/// <summary>
/// Génère un listing hiérachique à partir d'un dossier root
/// </summary>
/// <param name="path">string</param>
/// <param name="strW">StreamWriter</param>
/// <param name="count">int</param>
private void Arbo(string path,StreamWriter strW,int count)
{
DirectoryInfo di = new DirectoryInfo(path);
string tmp=string.Empty;
for(int i=0;i < count; i++) tmp= tmp+"";
strW.WriteLine(tmp+"<img src=data/folder.ico ><FONT class=content>"+di.Name+"</FONT><br>");
tmp= string.Empty;
for(int i=0;i < count+2; i++) tmp= tmp+"";
foreach(FileInfo fi in di.GetFiles())
{
strW.WriteLine(tmp +"<img src=data/file.ico ><FONT class=content>" +fi.Name+"</FONT><br>");
}
foreach(DirectoryInfo dii in di.GetDirectories())
{
Arbo(dii.FullName,strW,count+2); // appel récursif avec nouveaux paramètres
}
}
#endregion

Maintenant il nous faut créer une fonction qui fera une copie des fichiers sources. Malheureusement, il n'existe pas encore de méthode permettant la copie automatique d'un dossier et de son contenu, il faut donc travailler sur chaque fichier tout en redessinant la même arborescence et une fois encore, en utilisant une seule fonction récursive.

#region Fonction Backup
/// <summary>
/// Crée un dossier contenant les sources du projet
/// </summary>
/// <param name="path"></param>
/// <param name="Addpath"></param>
private void Backup(string path, string AddPath)
{
DirectoryInfo di = new DirectoryInfo(TB_Chemin.Text + "\\" + AddPath);
di.Create();
DirectoryInfo diSource =new DirectoryInfo(path);
foreach(FileInfo fi in diSource.GetFiles())
{
try
{
fi.CopyTo(di.FullName+"\\"+fi.Name, true);
}
catch{};
}
foreach(DirectoryInfo dii in diSource.GetDirectories())
{
Backup(dii.FullName,AddPath+"\\"+ dii.Name);
}
}
#endregion

Toutes les fonctions sont maintenantprêtes, il reste à gérer l'utilisation de celle-ci en fonction des choix de l'utilisateur (checkbox), vérifier d'un chemin d'export est bien renseigné, gérer la possibilité de caractères spéciaux.
Cliquez ici pour voir un rapport type tel qu'il sort de l'add-in.

Petit ajout: l'add-in étant présent dès le chargement de Visual Studio .Net et celui-ci ne contenant pas forcément à ce moment, de projet (ou solution) chargé. Il est donc nécessaire de vérifier à un moment donné la présence de ce projet (cette solution) afin de désactiver les fonctions de l'add-in pour éviter des comportements non voulus. Pour simplifier ce traitement, j'ai placé le code de vérification dans le code de chargement de la form principale.

Note : il eut été plus propre de le mettre dans la fonction Exec( ) de la classe connect.cs.

// on vérifie qu'une solution a été chargée
System.Array theProjects = (System.Array)applicationObject.ActiveSolutionProjects;
if(theProjects.Length <= 0)// Si la solution courante ne contient pas de projet
{
MessageBox.Show("Aucun projet ou solution n'est actuellement chargé \n L'add-in va alors se fermer");
this.Close();
}

Vous voilà en possession de votre premier add-in fonctionnel. Vous pouvez en télécharger les sources à la fin de ce cours.

IV - Add-in : Code Improver

IV-A - Description

Ici encore, nous verrons un autre exemple concret avec un add-in: code Improver, permettant d'agir directement sur le code. Il contiendra tout d'abord, une option permettant d'insérer un entête sur un fichier (comprenant nom de l'auteur et date de création), ainsi qu'une option pour ajouter les balises #region #endregion autour d'une sélection.
En plus, de voir comment travailler sur les fichiers ouverts à même Visual Studio .Net, nous verrons également l'intégration de l'add-in dans un menu contextuel spécifique.

IV-B - Intégration de l'add-in

Comme pour notre premier add-in, nous allons rajouter des menus, mais cette fois, nous allons le placer de façon à ce qu'il ne soit accessible que dans la fenêtre code. Pour cela, il nous suffit de placer notre menu dans une barre d'outils réservée au code : la barre « CodeWindow ».

Création des menus

#region OnConnection
public void OnConnection(object application, Extensibility.ext_ConnectMode connectMode,
object addInInst, ref System.Array custom)
{
applicationObject = (_DTE)application;
addInInstance = (AddIn)addInInst;
object []contextGUIDS = new object[] { };        // ref vide, mais nécessaire
Commands commands = applicationObject.Commands;  // représente les menus de visual studio
_CommandBars commandBars = applicationObject.CommandBars; // représente les barres de menu de visual studio

// Création des menus
try
{
CommandBar commandBar = (CommandBar)commandBars["Code Window"];  // menu contextuel des pages de code
Command command = commands.AddNamedCommand(addInInstance,
"AjoutRegione",
"Ajouter une region",
"Ajoute une region autour de la sélection",
true,
1554,
ref contextGUIDS,
(int)vsCommandStatus.vsCommandStatusSupported+
(int)vsCommandStatus.vsCommandStatusEnabled);
CommandBarControl commandBarControl = command.AddControl(commandBar, 1); // j'ajoute le menu au menu contextuel
command = commands.AddNamedCommand(addInInstance,
"AjouterHeadere",
"Ajouter un entête",
"Ajoute un entête en haut du document",
true,
1555,
ref contextGUIDS,
(int)vsCommandStatus.vsCommandStatusSupported+
(int)vsCommandStatus.vsCommandStatusEnabled);
commandBarControl = command.AddControl(commandBar, 2); // j'ajoute le menu au menu contextuel
}
catch(System.Exception e)
{
}
}

Une fois le menu crée de cette façon, nous avons la garantie qu'il n'apparaitra que dans la fenêtre de code, pas dans celle de design ni dans l'environnement de Visual Studio. Néanmoins, dans le cas de notre add-in, il nous reste deux vérifications à faire.
La première est de s'assurer que le code sur lequel se trouve le curseur est du C#. En effet, c'est le seul qui accepte les balises #region #endregion(VB.NET utilisant #Region et #End Region). La vérification peut se faire soit en allant vérifier l'extension du fichier soit plus facilement grâce aux variables de Visual Studio :

if(applicationObject.ActiveDocument.Language.Equals("CSharp"))

La deuxième est de vérifier qu'une sélection existe déjà. En effet, notre add-in se contentera de mettre des balises #region avec titre autour du texte sélectionné, chose inutile si aucun texte n'est sélectionné. La vérification se fait de cette manière :

TextSelection LaSelectionCourrante = applicationObject.ActiveDocument.Selection as TextSelection;
if(LaSelectionCourrante.Text.Length > 0)

Ce qui donne finalement :

préparation des menus

#region QueryStatus
public void QueryStatus(string commandName, EnvDTE.vsCommandStatusTextWanted neededText,
ref EnvDTE.vsCommandStatus status, ref object commandText)
{
if(neededText == EnvDTE.vsCommandStatusTextWanted.vsCommandStatusTextWantedNone)
{
if(commandName == "RegionsAddIn.Connect.AjoutRegion")
{
// Pour éviter des problèmes avec les langages (c++,etc.)
// nous devons donc désactiver ce menu si le code n'est pas du C#
if(applicationObject.ActiveDocument.Language.Equals("CSharp"))
{
// Nous devons maintenant vérifier si un texte est actuellement sélectionné
TextSelection LaSelectionCourrante = applicationObject.ActiveDocument.Selection as TextSelection;
if(LaSelectionCourrante.Text.Length > 0)
{
// Confirmation: on peut utiliser le menu
status = (vsCommandStatus)vsCommandStatus.vsCommandStatusSupported
| vsCommandStatus.vsCommandStatusEnabled;
// Note : j'active et rajoute ("supported") le menu au cas où précédemment il eut été supprimé
}
else status=(vsCommandStatus)vsCommandStatus.vsCommandStatusUnsupported;
}
else
{
// désactivation du menu
status=(vsCommandStatus)vsCommandStatus.vsCommandStatusUnsupported;
//Note: il n'est pas possible de "disabled" le menu. nous ne pouvons
//      que le retirer ou le rendre invisible.
}
}
}
}
#endregion

Enfin pour finir l'intégration, il nous restera à développer une Form qui permettra à l'utilisateur de spécifier le titre de la balise #region

IV-C - Développement du code fonctionnel

Pour ajouter les balises region autour de notre sélection, la méthode la plus simple est de modifier directement la sélection.
Pour cela, il nous suffit d'y insérer au début ou la fin selon les flags, le texte désiré. Ce que fait le code suivant :

TextDocument doc = applicationObject.ActiveDocument.Object("TextDocument") as TextDocument;
// code amélioré grâce aux sources de JW THOMAS (au niveau de détection de début de ligne)

//Si notre sélection de ne commence pas en début de ligne
// il nous faut rajouter au retour à la ligne avant notre #region
if(!doc.Selection.TopPoint.AtStartOfLine)
{
doc.Selection.Insert("\r\n#region " + TB_RegionName.Text + "\r\n", (int)vsInsertFlags.vsInsertFlagsInsertAtStart);
int selelectionStartLine = doc.Selection.TopLine + 1;
int selelectionEndLine = doc.Selection.BottomPoint.Line;
int selelectionEndColumn = doc.Selection.BottomPoint.LineCharOffset;
doc.Selection.MoveToLineAndOffset(selelectionStartLine, 1, false);
doc.Selection.MoveToLineAndOffset(selelectionEndLine, selelectionEndColumn, true);
// on place le curseur à la fin de la sélection
}
else doc.Selection.Insert("#region " + TB_RegionName.Text + "\r\n", (int)vsInsertFlags.vsInsertFlagsInsertAtStart);

// Si la sélection ne finit pas par un retour à la ligne
if(!doc.Selection.Text.EndsWith("\r\n"))
doc.Selection.Insert("\r\n#endregion\r\n", (int)vsInsertFlags.vsInsertFlagsInsertAtEnd);
else
doc.Selection.Insert("endregion\r\n", (int)vsInsertFlags.vsInsertFlagsInsertAtEnd);

Maintenant, nous allons créer un entête dynamique en haut du fichier code, ce qui va nous permettre de voir une seconde approche de l'édition de fichier dans Visual Studio
Cela se déroule en trois étapes :
-1- Récupérer le nom de l'auteur et la date

création de l'entête

// RegistryKey reg = Registry.CurrentUser.OpenSubKey(applicationobject.RegistryRoot,true);
// string szAuthor = reg.GetValue("UserName").ToString();
// tout est personnalisable, on pourrait y ajouter le nom du projet
szAuthor="MORAND Louis-Guillaume;
string szHeader= "/**************************  \r\n"
+ "/* auteur: "+ szAuthor +"\r\n"
+ "/* date de création: " +DateTime.Now.ToShortDateString()
+ "/*************************";

-2- Récupérer et créer un point de modification (EditPoint) au début du document

Création de l'EditPoint

TextDocument doc = applicationObject.ActiveDocument.Object("TextDocument") as TextDocument;
EditPoint edtPnt=doc.StartPoint.CreateEditPoint();

-3- Insérer notre entête à cet endroit

insertion de l'entête

edtPnt.Insert(szHeader);

Vous possédez maintenant un add-in permettant de rapidement écrire les zones regions ainsi qu'un entête. Il est possible de modifier son fonctionnement pour d'autres usages : insérer des TryCatch, ajouter des commentaires ou une licence, etc.
Il ne nous reste plus qu'à déployer notre add-in

V - Interactions possibles avec Visual Studio .Net

Cette partie rassemble différentes interactions possibles qu'il est possible de faire avec l'addin et/ou Visual Studio et qu'il est courant de rencontrer.

V-A - Ajouter un fichier au projet

Il est possible que vous soyez obligé de créer un fichier externe ou simplement de prendre un fichier déjà existant et que vous deviez l'ajouter au projet en cours. Heureusement pour nous, il est possible de le faire via la méthode ProjectItems.AddFromFile().
Voici donc un bout de code proposé par Eraim qui permet de créer un fichier csharp ayant pour nom le nom du projet courant, puis d'ajouter celui-ci au projet :

// Récupération du projet
Project proj = applicationObject.Solution.Item(1);

// Chemin et nom du fichier C#
String path = proj.FullName.Substring(0,proj.FullName.Length-7) + ".cs";

// Création du fichier
StreamWriter sw = new StreamWriter(path,false,System.Text.Encoding.UTF8);
sw.WriteLine("ligne de test");
sw.Close();

// Ajout du fichier au projet
proj.ProjectItems.AddFromFile(path);

De la même façon, il est possible d'ajouter au projet, un template déjà existant. Les templates sont les fichiers préremplis par Visual Studio lorsque vous faites Fichier > Ajouter > Nouveau Fichier > type voulu. Vous devez alors utiliser la méthode ProjectItems.AddFromTemplate().

V-B - Travailler avec le dossier racine de l'addin

Il serait erroné de croire que l'on peut travailler à partir du dossier racine de l'application (travailler avec des fichiers images, une base access ou autre) en utilisation l'habituelle Application.StartupApplication.

VI - Déploiement de l'add-in

VI-A - Préparation du setup

Comme vous avez dû le remarquer, lors de la création de la solution pour votre add-in, Visual Studio .Net a automatiquement généré un projet de déploiement permettant de redistribuer votre add-in sous forme d'un setup. Lorsque vous compiliez votre projet, une nouvelle instance de Visual Studio était lancée intégrant votre add-in. Néanmoins, à la fermeture de celle-ci, l'add-in était retiré. Plus précisément, il n'était que temporairement chargé dans l'IDE. Pour qu'il soit intégré définitivement, il faut qu'il soit enregistré et notre setup est là pour nous faciliter la vie, car il le fait automatiquement. Une fois, votre projet terminé, vous devez donc générer votre setup :

Puis une fois la génération terminée, vous pouvez l'installer.

Ouvrez maintenant une nouvelle instance de Visual Studio .Net et selon votre choix d'intégration, vous verrez peut-être dans le menu outils (ou autre) un menu correspondant à votre add-in.
Maintenant, tout est fonctionnel comme dans la plupart des cas, mais pour des déploiements plus professionnels, vous serez amené à modifier le fichier setup et ses actions sur le système.

VI-B - Configuration du setup

Il est maintenant temps de configurer le fichier setup afin que l'installation corresponde à vos attentes. Vous noterez que le projet de déploiement ne contient aucun fichier de configuration (à part le fichier tlb que je vous déconseille d'ouvrir, car il vous sera inutile (=illisible)). Pour configurer votre setup, il vous faut remarquer les nouveaux boutons apparus au-dessus de l'explorateur de ressources

Détailler toutes les actions possibles d'un déploiement prendrait un article tout entier donc nous ne verrons ici que les fonctions les plus utilisées.
Tout d'abord, l'éditeur de registre, c'est ici que vous pouvez indiquer les clés de registre à créer. Si vous développez un add-in « mémorisant » les préférences utilisateur, il est plus simple dans ce cas de le faire via le registre. Vous pouvez donc grâce à cet onglet créer les clés qui pourront servir à la configuration de base de votre add-in.

L'éditeur de l'interface utilisateur. Cette partie va vous permettre de designer votre fichier d'installation, aussi bien les images que les différents textes (CLUF, etc.). Il vous est également possible d'ajouter de nouvelles fenêtres avec des choix multiples permettant des installations différentes.

L'éditeur de condition de lancement. Il peut par exemple servir pour vérifier qu'un ancien add-in est déjà présent ou autre, car il vous permet de vérifier la présence de clé de registre ou de fichiers sur le disque.

L'éditeur de fichiers permet de voir le répertoire d'installation de votre add-in et d'y placer des fichiers complémentaires (des fichiers ressources) qui pourront être appelés par votre add-in durant son fonctionnement.

Les deux onglets que je ne décris pas ici sont l'éditeur des types de fichiers, car avec un add-in, du fait de son intégration dans VS.Net ayant lui même ses propres fichiers, vous risquez d'avoir des comportements non voulus. J'ai également omis l'éditeur d'actions personnalisées, car pour la plupart des compléments qui restent simples, il n'y a pas besoin d'interagir sur le système.

Tout ceci reste une description très rapide du déploiement d'un setup, il est possible de créer des setup bien plus poussés.

Conclusion

Vous avez donc vu avec quelle « simplicité » il est possible de développer son propre add-in. Ici, ce tutoriel présentait un add-in complet, mais j'y ai délibérément omis de parler des macros qui sont une méthode plus simple d'automatiser certaines tâches.
Pour toute information complémentaire ou remarque constructive sur l'article, n'hésitez pas à me contacter par mp ou à poser votre question sur le forum.

Téléchargements

Voici les sources des add-ins développés lors de ce cours. (Visual Studio 2003)
Sources de Project Manager (395ko) Miroir
Source de Code Improver (20ko) Miroir