J'ai passé quelques heures de plus sur un plugin pour Team Explorer, de quoi partager quelques informations supplémentaires...
Malheureusement, la première chose qui saute aux yeux est l'absence totale de documentation sur le sujet... A priori, aucune des classes des différents namespaces utilisés pour développer un plugin pour Team Explorer n'est documenté. Si bien que l'on se retrouve à ré implémenter des fonctions virtuelles d'une classe dont on hérite sans trop savoir ce que la dite fonction est censée faire... génial ;) ! Heureusement pour nous qu'il y a le sample sur lequel se baser. Toutefois, il y a quand même quelques topics intéressants sur les forums anglais de la msdn. A utiliser en cas de grosse galère ;).
Ceci étant dit, rentrons dans le vif du sujet. Pour ceci, nous avons besoin du SDK pour Visual Studio, CF mon post précédant et plus précisément d'ouvrir la solution "VisualStudioTeamSystemIntegration\Team Explorer and Project Creation\Sample Solution\PcwTESample\PcwTESample.sln".
La solution contient 2 projets : PcwTESample et PcwTESampleUI. Le second est un projet C++ contenant notamment un fichier de ressources (Ressource Files\PcwTESampleUI.rc) avec des chaines de caractères. A priori, je ne crois pas qu'il y ait quoique ce soit à faire sur les autres fichiers, faites moi savoir si je me trompe :). Comme vous vous en doutez, l'essentiel de l'implémentation de notre plugin va donc se trouver dans le premier projet, PcwTESample...
PcwTESample fournit en réalité deux fonctionnalités bien distinctes. Il y a tout d'abord un Wizard qui va être exécuté pendant la création d'un nouveau projet dont le process template fait appel au plugin. Il va, par exemple, permettre au chef de projet d'entrer un certain nombre d'informations qui pourraient être requises par le plugin plus tard. Le wizard est implémenté dans les fichiers PluginPcwSample.cs et PluginSampleWizardPage.cs (l'interface utilisateur). N'ayant pas besoin de cette feature pour le moment, je ne pourrai vous en dire beaucoup plus... La deuxième fonctionnalité est le plugin pour Team Explorer en lui même, que chaque membre de l'équipe pourra utiliser au quotidien.
Avant d'aller plus loin, voyons comment ces deux fonctionnalités sont exposées à Visual Studio. Le projet contient une classe HostPackage qui se charge de cette étape. Son rôle est donc de déclarer la présence du wizard et du plugin et de les instancier le moment voulu et en fonction du contexte. La déclaration est effectuée grâce aux attributs suivants :
[ProvideService(typeof(TEPlugin))]
[PluginRegistration(Catalogs.TeamProject, "Sample TE Plugin", typeof(TEPlugin))]
[ProvideService(typeof(SamplePcwPlugin))]
[PluginRegistration(Catalogs.ProjectCreation, "Sample PCW Plugin", typeof(SamplePcwPlugin))]
Elle s'effectue donc en 2 étapes. D'un coté on déclare un nouveau service avec ProvideService et de l'autre on enregistre ce fameux service comme un plugin. Cette deuxième étape permet d'ailleurs de déclarer le type de plugin que l'on enregistre à l'aide du premier paramètre. Catalogs peut donc prendre la valeur TeamProject (un plugin pour Team Explorer), ProjectCreation (un wizard actif lors de la création du projet) ou ClientLinking (Alors la, mystère étant donné l'absence de doc ! D'après le nom, on peut penser que c'est peut être pour déclarer un plugin qui doit faire interagir TeamExplorer avec un autre client installé sur la machine... Si quelqu'un trouve, je suis preneur :))
On le voit bien ici, il est tout à fait possible de déclarer plusieurs plugins dans le même package. La classe HostPackage va donc avoir pour principale mission d'instancier et d'initialiser nos différents plugins. L'instanciation est faite à l'aide de la méthode OnCreateService, et l'initialisation avec la méthode Initialize. Ces deux méthodes sont appelées par le framework de Visual Studio au moment ou celui-ci s'apprête à utiliser un plugin, de façon complètement autonome (Par exemple, lors de la création d'un nouveau projet pour un wizard, ou lors du chargement d'un projet dans l'interface de Team Explorer pour un plugin Team Explorer).
Exemple d'implémentations pour les fonctions OnCreateService et Initialize :
public sealed class HostPackage : PluginHostPackage
{
-
protected override object OnCreateService(IServiceContainer container, Type serviceType)
{
if (serviceType == typeof(TEPlugin))
{
return new TEPlugin();
}
if (serviceType == typeof(SamplePcwPlugin))
{
return new SamplePcwPlugin();
}
throw new ArgumentException(serviceType.ToString());
}
protected override void Initialize()
{
Trace.WriteLine (string.Format(CultureInfo.CurrentCulture, "Entering Initialize() of: {0}", this.ToString()));
base.Initialize();
// Ce code permet par exemple de charger un control dans un onglet
// à coté des documents ouverts, lorsque le plugin est chargé
MSVSIP.ToolWindowPane pane = this.FindToolWindow(typeof(PlannificationPane), 0, true);
if (pane == null)
{
throw new COMException("@101");
}
IVsWindowFrame frame = pane.Frame as IVsWindowFrame;
if (frame == null)
{
throw new COMException("@102");
}
ErrorHandler.ThrowOnFailure(frame.Show());
}
[...]
}
Maintenant que l'on sait comment est chargé le plugin, nous allons pouvoir regarder comment ce dernier est conçu. Il est implémenté dans la classe TEPlugin (fichier VsPkg.cs) et cette dernière doit hériter soit de BasicAsyncPlugin, soit de BasicPlugin. La encore, pas de documentation... Toutefois, d'après les noms, on peut penser qu'hériter de BasicAsyncPlugin permet de ne pas bloquer l'interface utilisateur lorsque le plugin réalise certaines actions.
Deux fonctions sont significatives dans cette classe :
- GetNewUIHierarchy : Je sais que cette fonction est appelée avant que Team Explorer affiche l'arborescence des répertoires. Pour autant, je suis honnêtement incapable de dire quelle est son utilité réelle :). Tout indice sur le sujet est le bienvenue

- CreateNewTree : Cette fonction permet d'ajouter des éléments à l'arborescence affichée dans Team Explorer. C'est donc l'endroit rêvé pour fournir à l'utilisateur du plugin un point d'entrée vers la fonctionnalité que l'on souhaite exposer. CreateNewTree prend un objet BaseUIHierarchy en paramètre. Il va nous permettre d'accéder à quelques propriétés intéressantes, comme par exemple : le nom du serveur, l'URI du projet, le nom du projet, l'instance du package ayant construit le plugin, etc (respectivement les propriétés : ServerName, ProjectUri, ProjectName et HostPackage). Nous avons donc toutes les informations souhaitées pour afficher un nouveau dossier spécifique à notre plugin dans un projet de Team Explorer. Pour cela, plusieurs étapes :
- D'abord, il est nécessaire d'instancier un nouvel objet de type TEPluginRoot (héritant de BaseRoot), symbolisant la racine de l'arborescence que nous souhaitons afficher.
- Deuxièmement, nous devons déclarer quels seront les noeuds enfants de notre racine. Ceci peut être fait à l'aide des classes TEPluginFolder (un répertoire) et TEPluginLeaf (un élément n'ayant lui même pas de sous élément) dont le code est exposé plus bas. Ces deux classes héritent de BaseHierarchyNode qui représente un élément quelconque de l'arborescence, mais redéfinissent le comportement de base. Il est donc très facile de remplir l'arborescence, voici un exemple simple :
protected override BaseHierarchyNode CreateNewTree(BaseUIHierarchy hierarchy)
{
TEPluginUIHierarchy uiHierarchy = (TEPluginUIHierarchy)hierarchy;
TEPluginRoot root = new TEPluginRoot(hierarchy.ServerName + '/' + hierarchy.ProjectName);
TEPluginFolder myFolder = new TEPluginFolder(root.CanonicalName + "/MesSites", "Mes sites");
myFolder.AddChild(new TEPluginLeaf(myFolder.CanonicalName + "/google", "Google", "http://www.google.fr"));
myFolder.AddChild(new TEPluginLeaf(myFolder.CanonicalName + "/julien", "Blog julien", "http://julien.lavigneducadet.com"));
root.AddChild(myFolder);
return root;
}
Et comme promis, le code de TEPluginFolder et TEPluginLeaf :
public class TEPluginFolder : BaseHierarchyNode
{
public TEPluginFolder(string path, string name)
: base(path, name)
{
InitAsFolder();
}
public override String PropertiesClassName
{
get { return "Plugin Folder"; }
}
}
public class TEPluginLeaf : BaseHierarchyNode
{
public TEPluginLeaf(string path, string name, string uri)
: base(path, name)
{
this.uri = uri;
InitAsLeaf();
}
public override void DoDefaultAction()
{
// Do something !
}
public override string PropertiesClassName
{
get { return "Search Engine"; }
}
private string uri;
}
Nous sommes maintenant arrivés au stade ou nous avons pu afficher de nouveaux noeuds pour notre plugin. Il ne reste plus qu'a lancer une action quelconque lorsque l'utilisateur double clique dessus. Grace à la classe TEPluginLeaf, c'est très facile ! Il suffit de redéfinir la fonction DoDefaultAction et d'implémenter dedans l'action que l'on souhaite réaliser lors du double clique. Dans le cas du Sample, c'est par exemple d'ouvrir l'url correspondant au noeud en cours, avec le code suivant :
LaunchVsBrowserWindow(this.uri);
Evidemment, ce pourrait aussi être quelque chose de plus intéressant comme ouvrir une nouvelle fenêtre dans Visual Studio, et y charger un control que nous avons développé pour prendre en charge une fonctionnalité non gérée par Team Explorer. Les possibilités sont assez colossales :).
En conclusion, si vous avez compris ce que j'ai essayé d'expliquer dans cet article, vous êtes paré pour développer un bon gros plugin pour Team Explorer, bon courage ! 