| Add-In Visual Studio .NET vs Plug-In Eclipse 1/2 par Sami Jaber (jaber@ifrance.com) | ||
'année 2003 sera sans nul doute l'année du Plug-In. A
l'heure où les termes "ouverture" et "extensibilité" prennent de plus en plus
d'intérêt, les Plug-Ins vont de toute évidence jouer un rôle prépondérant dans
la bataille que se livre actuellement les éditeurs du marché. Coté Java, l'IDE Eclipse
se démarque clairement par une forte orientation de son architecture vers un
socle basé sur des
Plug-Ins.
Visual
Studio .NET quant à lui, adopte une démarche similaire en proposant une intégration
étroite avec le Framework .NET et l'environnement COM sur lequel il s'appuie. Au passage, c'est là tout le paradoxe du Plug-In. Le Plug-In
est un programme qui permet de programmer l'éditeur chargé de programmer ces
mêmes programmes. Tout un art ! :-)
Aborder de manière exhaustive l'ensemble des Framework destinés aux Plug-In VS.NET ou Eclipse aurait pu faire l'objet d'un ouvrage de plusieurs centaines de pages tant les fonctionnalités de part et d'autres sont riches et complexes. Là n'est pas notre but, nous avons voulu à travers cet article vous présenter un exemple concret mettant en oeuvre les API les plus importantes des deux outils sans forcément chercher l'exhaustivité.
Dans cette optique, l'exemple qui nous a semblé le plus pertinent et le plus simple est celui d'un éditeur de fichier RSS (XML) permettant aux lecteurs de DNG de visualiser les derniers articles ou actualités de la sphère .NET à l'intérieur de leur environnement de développement préféré. Nous aborderons donc dans un premier temps Visual Studio .NET pour ensuite pénétrer au coeur de l'environnement Eclipse.
Notre AddIn Visual Studio se nomme DotNetGuruNewsReader. La copie d'écran suivante illustre le résultat final obtenu dans l'IDE une fois l'installation terminée.

L'idée consiste à créer une fenêtre intégrée dans l'éditeur affichant les dernières actualités stockées sous la forme d'un fichier RDF. Ce format s'appuie sur la syntaxe XML et constitue l'un des standards les plus en vogue aujourd'hui dans la syndication de contenus.
La fenêtre chargée d'afficher le flux d'actualité est en réalité un navigateur Web. Pour séparer la présentation du contenu proprement dit, le Plug-In propose à l'utilisateur de paramétrer l'affichage à l'aide d'une feuille de style XSLT. La copie d'écran précédente illustre donc le fruit de la transformation du fichier XSL et du flux XML en HTML .
Paramétrer le plug-In
Tout Plug-In se doit de proposer le paramétrage de ses différentes options. C'est pourquoi, dans notre exemple, nous laissons le soin à l'utilisateur de modifier sa feuille de style XSL ainsi que les différents sites auxquels il souhaite accéder dans sa fenêtre de visualisation. L'écran suivant illustre cette fonctionnalité :
Vous imaginez sans doute que ce résultat est l'objet d'un travail colossal, il n'en est rien. Il nous a fallu quelques lignes de code pour constituer les fonctionnalités les plus importantes de l'application. Le reste étant généré par le concepteur de formulaire WYSIWYG.
Avant de rentrer dans le détail du source, attardons nous sur l'architecture des Plug-Ins de Visual Studio .NET.
Le concept de Plug-In est apparu bien avant l'arrivée de Visual Studio .NET. Les Plug-In ou plutôt AddIns existaient dans Visual Studio version 6 au travers des API Automation de COM (Component Object Model). Les utilisateurs de VB5-6 ou Visual C++ faisaient d'ailleurs partie des premiers à bénéficier de ce mode de programmation. L'IDE Visual Studio proposait alors un ensemble de composants COM appelés "Common Environment Object Model" constitués d'interfaces, de classes et de propriétés pouvant être accéder par n'importe quel programme tiers. Pour ceux ayant approché de près les macros à travers l'API Automation, le principe est très similaire.
|
Avec l'avènement de .NET, le Framework a fait son apparition au sein de l'IDE. Cela s'est traduit par la reprise du noyau existant et l'ajout de nouvelles fonctionnalités liées à l'environnement .NET. Le tout sous la forme d'API mixtes managées et non managées.
Cette mixité a permit de supporter certains AddIns COM et de proposer un socle managé aux AddIns .NET. Quant aux interactions avec l'IDE, elles restent pour leur part dépendantes de COM avec notamment l'API DTE sur laquelle viennent se greffer les Plug-Ins .
|
DTE (Development Tools Extensibility)
Le DTE est composé d'un ensemble de classes constituant une hiérarchie bien particulière de l'environnement Visual Studio. Ces classes peuvent représenter soit une fenêtre graphique, soit les caractéristiques d'un projet ouvert dans l'IDE. DTE vous donne également la possibilité de :
Créer ou intercepter un ensemble d'évènements vous permettant de fournir un prétraitement quelconque
Personnaliser le mécanisme de construction de projet (build, run, deploy)
Contrôler des portions de textes dans l'éditeur de code source
Implémenter des commandes qui vous aideront
Contrôler le concepteur WYSIWYG de formulaires et les éléments de projets (fichiers, répertoires, ...)
Redéfinir certains comportements standards et implémenter vos propres fenêtres ajoutées à celles existantes (toolbox, TaskList, Command Window, ...)
Bref, autant de fonctionnalités indispensables à la personnalisation de l'IDE. Le schéma suivant résume très sommairement le modèle objet du DTE.
Voyons maintenant comment a été conçu le Plug-In DotNetGuruNewsReader.
L'interface IDTExtensibility2
Toute application géré par un Framework technique s'articule autour d'une interface ou d'une classe de base. Dans le cas des API DTE, c'est l'interface IDTExtensibility2 qui joue ce rôle. Au passage, le nom de cette classe est assez étonnante et il n'est pas rare de trouver dans les API Win32 des classes numérotées ainsi, ce qui tend à semer une certaine confusion dans les esprits.
IDTExtensibility2 est situé dans le Namespace extensibility. Elle contient cinq méthodes que doivent implémenter les classes dérivées constituant l'AddIn. Chaque méthodes intervient dans le cycle de vie de l'AddIn et correspond à un évènement bien précis.
La méthode OnAddInsUpdate() est appelée lorsqu'un changement intervient dans la liste des AddIns proposée par Visual Studio à travers l'interface AddIn Manager. Cela peut se traduire par l'activation ou la désactivation d'un AddIn quelconque par l'utilisateur. Le tableau passé en paramètre correspond à la liste des AddIns chargés dans Visual Studio à cet instant.
La méthode OnConnection() est appelée lorsque l'AddIn est chargé dans l'environnement. Cet évènement survient lorsque l'utilisateur demande explicitement le chargement de l'AddIn à travers l'outil d'administration ou lorsque sa propriété Connect est positionnée à True.
La méthode OnBeginShutdown() est appelée avant la fermeture de Visual Studio afin d'opérer d'éventuelles fermetures de fichier ou simplement libérer proprement des ressources.
Quant aux méthodes OnDisconnection() et OnStartupComplete(), elles sont appelées lorsque l'utilisateur décharge un AddIn et après le lancement de l'application.
L'application "DotNetGuru News Reader"
Après ces quelques explications sur le cycle de vie d'un AddIn, rentrons dans le détail de l'implémentation technique de notre petit programme. La classe Connect.cs est le point d'entrée de notre AddIn, elle est régulée par le Framework et dicte le déroulement complet de l'application. Pour l'implémenter, nul besoin d'être un spécialiste rompu au maniement des API DTE. Visual Studio vous propose des assistants dont le rôle est justement de vous créer un squelette de code fonctionnel.
Voici le squelette de la classe Connect.cs généré par l'assistant après avoir passé à un certain nombre d'étapes.
namespace
DotNetGuruAddIn {
using
System;
using
Microsoft.Office.Core;
using
Extensibility;
using
System.Runtime.InteropServices;
using
EnvDTE;
// Rappelez
vous, VS.NET
est basé sur COM et les Add-In sont codés en .NET,
// Il est donc nécessaire de préciser le
ProgID du Stub d'Interop COM=>.NET
[GuidAttribute("AD04B648-10CD-49E8-B9FF-6B8457BF50BE"),
ProgId("DotNetGuruAddIn.Connect")]
public
class Connect : Object, Extensibility.IDTExtensibility2,
IDTCommandTarget {
/// <summary>
///
Implements the constructor for the Add-in object.
///
Place your initialization code within this method.
/// </summary>
private OutputWindowPane outputWindowPane;
private _DTE applicationObject;
private AddIn addInInstance;
private NewsForm _nf ;
public Connect(){}
/// <summary>
///
Implements the OnConnection method of the IDTExtensibility2 interface.
///
Receives notification that the Add-in is being loaded.
/// </summary>
/// <param
term='application'>
///
Root object of the host application.
/// </param>
/// <param
term='connectMode'>
///
Describes how the Add-in is being loaded.
/// </param>
/// <param
term='addInInst'>
///
Object representing this Add-in.
/// </param>
/// <seealso
class='IDTExtensibility2' />
public void
OnConnection(object application,
Extensibility.ext_ConnectMode connectMode,
object
addInInst, ref System.Array custom){
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;
Command command = commands.AddNamedCommand(addInInstance,
"DotNetGuruAddIn", "DotNetGuruAddIn",
"Executes the command for DotNetGuruAddIn", true,
59, ref
contextGUIDS,
(int)vsCommandStatus.vsCommandStatusSupported+(int)vsCommandStatus.vsCommandStatusEnabled);
CommandBar commandBar = (CommandBar)commandBars["Tools"];
CommandBarControl commandBarControl = command.AddControl(commandBar, 1);
}
catch(System.Exception /*e*/){}
}
}
public void
OnDisconnection(Extensibility.ext_DisconnectMode disconnectMode, ref
System.Array custom){ }
public void
OnAddInsUpdate(ref System.Array custom)
{
outputWindowPane.OutputString("\nOnAddInsUpdate appelé");
}
public void
OnStartupComplete(ref System.Array custom)
{
object ToolWinObj = null;
try {
OutputWindow outputWindow = (OutputWindow)applicationObject.Windows.Item(Constants.vsWindowKindOutput).Object;
outputWindowPane = outputWindow.OutputWindowPanes.Add("DotNetGuru
Logs");
m_ToolWindow = applicationObject.Windows.CreateToolWindow(
this.addInInstance,
"DotNetGuruAddIn.NewsForm",
"DotNetGuru",
"{B117A337-10C3-40e9-BF9F-265ACF3AA6BC}",
ref ToolWinObj);
m_ToolWindow.Visible = true;
///
C'est ici que nous demandons l'affichage des
news dans la fenêtre courante
_nf = (NewsForm)ToolWinObj;
_nf.DisplayNews();
outputWindowPane.OutputString("DotNetGuru News loaded");
}
catch (Exception e) {outputWindowPane.OutputString(e.StackTrace);
throw e; }
}
public void
OnBeginShutdown(ref System.Array custom)
{ }
///
Ces deux méthodes sont fournies par
l'interface IDTCommandTarget
public void
QueryStatus(string commandName,
EnvDTE.vsCommandStatusTextWanted neededText,
ref EnvDTE.vsCommandStatus status, ref object
commandText) {
if(neededText ==
EnvDTE.vsCommandStatusTextWanted.vsCommandStatusTextWantedNone)
{
if(commandName == "DotNetGuruAddIn.Connect.DotNetGuruAddIn")
{
status = (vsCommandStatus)vsCommandStatus.vsCommandStatusSupported|vsCommandStatus.vsCommandStatusEnabled;
}
}
}
public void
Exec(string commandName,
EnvDTE.vsCommandExecOption executeOption, ref object
varIn, ref object
varOut, ref bool
handled) {
if (outputWindowPane != null)
outputWindowPane.OutputString("\nVous venez de clicker dans le menu
Outils/DotNetGuruAddIn");
// Au
cas ou l'utilisateur aurait masqué la fenêtre RSS on la réaffiche
if (m_ToolWindow!=null)
m_ToolWindow.Visible=true;
handled = false;
if(executeOption ==
EnvDTE.vsCommandExecOption.vsCommandExecOptionDoDefault)
{
if(commandName == "DotNetGuruAddIn.Connect.DotNetGuruAddIn"){
handled = true;
return;
}
}
}
}
}
Connect.cs
Le code le plus important est situé dans la méthode OnStartUpComplete()
qui consiste à créer une nouvelle fenêtre à l'aide de la méthode CreateToolWindow(). Les arguments sont très importants car ils correspondent au
titre de la fenêtre et à la référence du contrôle WinForms fournit par
l'utilisateur. En pratique, cela signifie que tout contrôle graphique .NET est
potentiellement utilisable dans un AddIn Visual Studio. L'impact est d'ailleurs
immédiat sur la conception de ce formulaire qui est d'une simplicité
déconcertante via le mode WYSIWYG.
Notre AddIn étant constitué d'un navigateur Web intégré, il est donc nécessaire d'intégrer un contrôle un peu particulier existant au sein de Windows sous la forme d'un contrôle ActiveX SHDocViewer (shdocvw.dll). Après un rapide référencement dans notre projet, l'objet apparaît dans la boite à outils du concepteur WYSIWYG et un simple glisser/déplacer fini de nous convaincre sur l'efficacité de l'ensemble. Concernant les classes d'interopérabilité COM, Visual Studio se charge de les générer à notre place. Du grand art.
Voici le source correspondant à notre fenêtre principale, nous vous épargnons le code de la méthode InitializeComponent().
namespace
DotNetGuruAddIn {
///
<summary>
///
Summary description for NewsForm.
///
</summary>
public
class NewsForm :
System.Windows.Forms.UserControl {
private
AxSHDocVw.AxWebBrowser axWebBrowser1;
///
<summary>
///
Required designer variable.
///
</summary>
private
System.ComponentModel.Container components = null;
public
void DisplayNews() {
object o = null
;
RSSReader.ProcessXSLTransformation();
this.axWebBrowser1.Navigate("file://"+RSSReader.TempFileHtml,
ref o, ref o, ref
o, ref o);
}
}
}
La méthode DisplayNews() est chargée de générer le rendu HTML final via une transformation XSLT avec le fichier RSS/XML de notre site préféré. Cette opération est effectuée dans la classe RSSReader dont le rôle est télécharger le fichier RSS à partir d'une URL et de le stocker en local. La feuille XSLT par défaut fournie dans l'AddIn est ensuite appliquée au fichier RSS afin de générer le fichier HTML cible stocké dans un répertoire temporaire. Pour finir, le contrôle axWebBrowser correspondant à notre navigateur Web est mis à profit pour afficher la page Web finale à l'aide de la méthode Navigate().
namespace
DotNetGuruAddIn {
using
System;
using
System.Collections;
using
System.IO;
using
System.Text;
using
System.Xml;
using
System.Xml.Xsl;
using
System.Xml.XPath;
//
Attention, cette classe n'est pas Thread-Safe
public
class RSSReader {
public static
readonly string
TempFileXslt=System.IO.Path.ChangeExtension(System.IO.Path.GetTempFileName(),
"xslt");
public static
readonly string
TempFileHtml=System.IO.Path.ChangeExtension(System.IO.Path.GetTempFileName(),
"htm");
// public car on précharge la fenetre
d'options avec cette valeur,
on aurait utiliser une propriété
public static
string BufferXslt = "XSL File Not present";
// Cette méthode récupère le fichier RSS
ainsi que la feuille de style XSLT
et effectue la transformation
public static
void ProcessXSLTransformation() {
// La fenetre d'option vous permet de paramétrer
cette adresse - A implémenter
XPathDocument doc = new XPathDocument("http://www.dotnetguru.org/backend.php");
Stream strm = new RSSReader().GetType().Assembly.GetManifestResourceStream("DotNetGuruAddIn.RSS.xslt");
// Copy le fichier XSL stocké comme ressource
dans l'assembly en local
if (strm!=null)
{
StreamReader reader = new StreamReader(strm);
StreamWriter sw = new StreamWriter(TempFileXslt);
BufferXslt = reader.ReadToEnd();
sw.Write(BufferXslt);
reader.Close();
sw.Close();
}
XslTransform xslt = new XslTransform();
// Transformation XSL et génération du
fichier HTML
xslt.Load(TempFileXslt);
using (StreamWriter sw = new
StreamWriter(TempFileHtml, false))
{
xslt.Transform(doc, null, sw);
}
}
}
}
Paramétrer l'AddIn à l'aide des options de Visual Studio
L'avantage de ce petit exemple réside dans sa large couverture fonctionnelle nous permetant d'illustrer l'ensemble des caractéristiques du Framework DTE en quelques lignes. Il nous reste encore à aborder la gestion des Préférences de notre AddIn.
Faire appel à une feuille de style pour personnaliser l'affichage n'est pertinent que lorsque l'utilisateur a la possibilité de modifier sa feuille XSL et paramétrer l'ensemble des sites Web qu'il souhaite voir apparaître dans son mini Visualiseur de News. Dans l'exemple suivant nous avons paramétré deux sites bien connus du public : DotNetGuru.org et DotNet-fr.org.
Le résultat auquel on souhaite aboutir est illustré par la copie d'écran suivante.

Pour y arriver, nous allons suivre le modèle utilisé par la classe Connect. Nous dérivons l'interface IDTToolsOptionsPage pour implémenter les méthodes GetProperties(), OnAfterCreated(), OnCancel(), OnHelp() et OnOK().

Là encore, la classe en question doit être un formulaire WinForm du type System.Windows.Forms.UserControl créé à l'aide du concepteur WYSIWYG. Voici son contenu :
using System;
using System.Collections;
using
System.ComponentModel;
using System.Drawing;
using System.Data;
using
System.Windows.Forms;
using EnvDTE;
using Microsoft.Win32;
using
System.Runtime.InteropServices;
namespace
DotNetGuruAddIn {
///
<summary>
///
Summary description for OptionsForm.
///
</summary>
public
class OptionsForm :
System.Windows.Forms.UserControl, IDTToolsOptionsPage
{
private
System.Windows.Forms.Button button1;
private
System.Windows.Forms.RichTextBox richTextBox1;
private
System.Windows.Forms.Panel panel1;
private
System.Windows.Forms.Label label2;
private
System.Windows.Forms.Button button2;
private
System.Windows.Forms.ListBox listBox1;
private
System.Windows.Forms.Label label1;
private
System.Windows.Forms.GroupBox groupBox1;
private
System.Windows.Forms.TextBox textBox1;
private
System.Windows.Forms.GroupBox groupBox2;
///
<summary>
///
Required designer variable.
///
</summary>
private
System.ComponentModel.Container components = null;
public
OptionsForm()
{
// La
création du formulaire et de ses contrôles sont placé dans cette méthode
// Pour plus de visibilité nous avons enlevé cette partie
InitializeComponent();
// TODO: Add any initialization after the
InitForm call
}
public
void OnAfterCreated(DTE DTEObject)
{
//
Mettre ici les différentes initialisations
}
public
void GetProperties(ref object
PropertiesObject)
{
PropertiesObject = null;
}
public
void OnOK(){}
public
void OnCancel(){}
public
void OnHelp(){}
private
void button1_Click(object
sender, System.EventArgs e){
this.listBox1.Items.Add(this.textBox1.Text);
}
}
}
OptionForm.cs
L'application ne traite pas la persistance des informations de préférences, il nous aurait fallu stocker ces options dans la base de registre ou dans un fichier texte quelconque. Nous vous laissons le soin d'implémenter tout cela en guise d'exercice ;-).
La base de registre pour relier le tout
Lorsque l'ensemble des fenêtres sont implémentées, il reste encore une dernière étape, la plus importante : l'enregistrement de la fenêtre d'option dans la base de registre. C'est au passage l'une des étapes qui prête le plus à discussion. Pourquoi contraindre l'utilisateur à réaliser une opération d'aussi bas niveau alors que l'outil pourrait aisément s'en charger à travers un fichier de configuration XML.
Mais passons, voici notre fichier d'extension .reg qui devra être lancé par l'utilisateur pour informer Visual Studio que notre Plug-In intègre une fenêtre d'options.
REGEDIT4
[HKEY_CURRENT_USER\SOFTWARE\Microsoft\VisualStudio\7.0\PreloadAddinState]
"DotNetGuruAddIn.Connect"=dword:1
[HKEY_CURRENT_USER\Software\Microsoft\VisualStudio\7.0\AddIns\DotNetGuru\Options]
[HKEY_CURRENT_USER\Software\Microsoft\VisualStudio\7.0\AddIns\DotNetGuru\Options\DotNetGuru]
[HKEY_CURRENT_USER\Software\Microsoft\VisualStudio\7.0\AddIns\DotNetGuru\Options\DotNetGuru\Settings]
"Control"="DotNetGuru.OptionsForm"
Le déploiement est un régal pour qui sait manipuler un minimum les options de l'IDE. Un installeur automatique se charge de vous insérer l'ensemble des Assembly référencées dans votre projet à l'intérieur du fichier de déploiement MSI (Microsoft installeur). Lors de la construction, les éventuels composants COM sont enregistrés puis supprimés de la base de registre en cas de renouvellement du tests. A noter que nous n'avons pratiquement jamais pu prendre en défaut l'outil qui s'est révélé d'une étonnante stabilité .

Quant aux tests, là encore le confort est au rendez-vous. Il suffit simplement de lancer le projet à l'aide du menu "exécuter", ce qui a pour effet de créer une nouvelle instance de Visual Studio intégrant l'AddIn en cours de développement. Comble du luxe, l'environnement vous permet même de déboguer votre application à partir de l'environnement source.
L'une des grande forces de Visual Studio est son aptitude à enregistrer sous la forme de macros les différentes actions effectuées par l'utilisateur pour en générer le code source. Lorsque vous souhaitez réaliser une tâche bien particulière et n'êtes pas (tout comme moi ;-)) un spécialiste de l'API DTE, il vous suffit simplement de faire enregistrer votre opération et de copier/coller le code généré dans votre AddIn. Cette caractéristique est rendue possible par le fait que Visual Studio utilise pour les macros la même API que les AddIns. Alors ne vous en privez pas....
Maintenant que vous avez eu un aperçu du Framework Visual Studio .NET, voyons coté Java ce que propose l'outil Eclipse pour réaliser la même opération.
Page suivante - Développer un Plug-In avec Eclipse
Auteur : Sami Jaber
Copyright © Janvier 2003
Ressources
Site officiel d'Eclipse : www.eclipse.org
Tutorial sur les Plug-Ins Visual Studio : http://msdn.microsoft.com/library/default.asp?url=/library/en-us/vsintro7/html/vxprodteproperty.asp
Exemples de Plug-In Eclipse : http://www.improve-technologies.com/pages/Java/IDE/Eclipse/Plug-ins/
Téléchargez les sources et les binaires
Visual Studio Plug-In (Binaires) : DotNetGuruPlugIn_0.1.msi (116 Ko, Installer automatique)
Visual Studio Plug-In (Sources) : DNGVSPlugInSrc_0.1.zip (116 Ko, Fichier zip contenant la solution VS.NET)
Eclipse Plug-In : (Binaires et sources ) DotNetGuruNewsReader_0.1.zip (1,8 Mo avec les bibliothèques XSL, Voir fichier install.txt)
Licence GPL
Tous fichiers sont fournis sous la licence GPL.