| Add-In Visual Studio .NET vs Plug-In Eclipse 2/2 par Sami Jaber (jaber@ifrance.com) | ||
La plateforme Eclipse est composées de deux couches logicielles principales : la couche modèle et la couche d'interface utilisateur (IHM). Le modèle est également appelé Workspace. Il contient une collection de ressources identifiées par des projets, des répertoires ou des fichiers. L'interface utilisateur ou Workbench défini la présentation visuelle de ces ressources. A l'intérieur d'un Workbench, les perspectives sont utilisées pour contrôler la visibilité de chaque éléments du modèle et de l'interface utilisateur via le concept d'actions et de vues. Ces contrôles permettent de naviguer ou de modifier le Workspace courant tout en personnalisant l'environnement. Le développeur a également la possibilité d'implémenter de nouvelles perspectives et de nouvelles vues. A ce titre, il doit être en mesure d'identifier la nature exacte des composants qu'il souhaite développer ou étendre afin d'utiliser les API adéquates.

Le JDT est le Java Development Tool, il est décomposé en deux parties : le Core et l'UI et contient l'ensemble des classes constituant l'outil Eclipse. Des contrôles graphiques (SWT) en passant par le débogage ou encore l'éditeur de texte, tout y est. Pour plus d'informations à ce sujet, n'hésitez pas à consulter la liste des classes du JDT.
Le PlugIn DotNetGuruReader d'Eclipse possède les mêmes caractéristiques que son homologue Visual Studio .NET. La copie d'écran suivante illustre le résultat final obtenu dans l'IDE une fois l'installation effectuée.
Paramétrer le plug-In
L'application se paramètre de la même manière que VS.NET à l'aide d'un menu spécifique.

Les Vues
Les vues représentent l'élément de base permettant de naviguer dans une hiérarchie d'informations. Elles sont typiquement utilisées pour ouvrir un éditeur de texte, afficher des propriétés sur la fenêtre active ou récupérer des informations spécifiques sur l'application. Par défaut, l'IDE propose un certain nombre de vues telles que l'explorateur de package, la fenêtre des propriétés, l'explorateur de classes, etc ... Notre Plug-In possède donc les mêmes caractéristiques que ces vues standards.
L'API PDE représente le Framework chargé de gérer le cycle de vie d'un Plug-In mais également de fournir toute l'infrastructure nécessaire à l'interaction entre le Plug-In et l'IDE. Il est constitué des packages suivants : org.eclipse.pde.internal, org.eclipse.pde.core, org.eclipse.pde.internal.runtime.
Cet environnement a pour rôle de contrôler l'exécution du Plug-In sans pour autant fournir les fonctionnalités de base permettant à l'application d'interagir avec l'IDE. Pour cela le Plug-In devra faire appel aux API sur lesquelles s'appuie Eclipse, c'est à dire JFace, SWT et UI. Ce principe est assez similaire à celui utilisé par Visual Studio .NET dans la mesure où l'outil ne rajoute aucune couche supplémentaire entre le Plug-In et l'environnement, il se contente de jouer le rôle de facilitateur en contrôlant le cycle de vie et le déploiement du Plug-In. Au passage, dans Eclipse, le Plug-In est avant tout un constituant de base de l'IDE au même titre que n'importe quel composant graphique d'origine.
|
Voyons maintenant comment a été conçu le Plug-In DotNetGuruNewsReader.
L'interface AbstractUIPlugin
Tout Plug-In Eclipse s'intégrant au workspace courant doit implémenter une classe qui dérive de org.eclipse.ui.plugin.AbstractUIPlugIn. Comme on pourrait s'y attendre, cette classe est très proche que la classe Connect.cs de .NET. Elle contient des méthodes telles que loadPreferenceStore(), startup() ou shutdown() sur le principe de OnConnection() et OnDisconnection() de Connect.cs. Cependant, contrairement à .NET qui observe une séparation claire entre les préférences d'un projet et la classe principale du Plug-In, Eclipse lie étroitement ces deux notions. Cela est dû au caractère plus "graphique" de la classe principale du Plug-In Eclipse qui hérite de AbstractUIPlugIn.
Par ailleurs, sur le principe de .NET les méthodes déclarées dans AbstractPlugIn correspondent essentiellement à des évènements qui surviennent tout au long du cycle de vie de l'application.
L'application DotNetGuruNewsReader
Après ces quelques explications, rentrons dans le détail de l'implémentation technique de notre petit Plug-In. La classe DotNetGuruNewsReaderPlugin est illustrée ci-dessous :
package
DotNetGuruNewsReader;
import org.eclipse.ui.plugin.*;
import
org.eclipse.core.Runtime.*;
import org.eclipse.core.resources.*;
import java.util.*;
/**
* The main plugin class to be used in the desktop.
*/
public class
DotNetGuruNewsReaderPlugin extends
AbstractUIPlugin {
//The shared instance.
public static
final String REFRESH_INTERVAL_PREFERENCE = "refreshinterval";
public static
final String BACKENDS_PREFERENCE = "backends";
public static
final String BROWSER_PREFERENCE = "browser";
public static
final String BROWSER_TYPE_PREFERENCE = "type";
//Preferences concerning proxy
public static
final String USE_HTTP_PROXY_PREFERENCE = "httpproxy.use";
public static
final String HTTP_PROXY_HOST_PREFERENCE = "httpproxy.host";
public static
final String HTTP_PROXY_PORT_PREFERENCE = "httpproxy.port";
public static
final String HTTP_PROXY_LOGIN_PREFERENCE = "httpproxy.login";
public static
final String HTTP_PROXY_USER_PREFERENCE = "httpproxy.user";
public static
final String HTTP_PROXY_PASSWD_PREFERENCE = "passwd";
//Default values
public static
final int
DEFAULT_REFRESH_INTERVAL = 30;
public
static final
String DEFAULT_BROWSER_TYPE = "2";
private static
DotNetGuruNewsReaderPlugin plugin;
//Resource bundle.
private
ResourceBundle resourceBundle;
/** The constructor. */
public DotNetGuruNewsReaderPlugin(IPluginDescriptor
descriptor) {
super(descriptor);
plugin
= this;
try
{
resourceBundle= ResourceBundle.getBundle("DotNetGuruNewsReader.DotNetGuruNewsReaderPluginResources");
System.out.println(ResourceBundle.class);
} catch
(MissingResourceException x) {
resourceBundle =
null;
}
}
/**
*
Returns the shared instance.
*/
public
static DotNetGuruNewsReaderPlugin getDefault() {
return
plugin;
}
/**
*
Returns the workspace instance.
*/
public
static IWorkspace getWorkspace() {
return
ResourcesPlugin.getWorkspace();
}
/**
*
Returns the string from the plugin's resource bundle,
*
or 'key' if not found.
*/
public
static String getResourceString(String key) {
ResourceBundle
bundle= DotNetGuruNewsReaderPlugin.getDefault().getResourceBundle();
try
{
return bundle.getString(key);
}
catch (MissingResourceException e) {
return key;
}
}
/**
*
Returns the plugin's resource bundle,
*/
public
ResourceBundle getResourceBundle() {
return resourceBundle;
}
public
void
startup() throws
CoreException {
super.startup();
System.out.println("Plug-In
derramé");
}
public
void shutdown()
throws CoreException {
super.shutdown();
System.out.println("Plug-In
detruit");
}
}
Vous ne trouverez pas le code lié à la logique fonctionnelle du Plug-In dans cette classe. En effet, notre application représentant une Vue, il nous faudra développer une seconde classe dérivant du contrôle graphique Vue ou plutôt org.eclipse.ui.part.ViewPart. Voici cette classe.
package DotNetGuruNewsReader.views;
import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.ole.win32.OLE;
import org.eclipse.swt.ole.win32.OleAutomation;
import org.eclipse.swt.ole.win32.OleControlSite;
import org.eclipse.swt.ole.win32.OleFrame;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.ui.part.ViewPart;
public class
OleBrowserView extends
ViewPart {
private
Composite
displayArea;
private
OleFrame
webFrame;
private
OleWebBrowser
webBrowser;
private
OleControlSite webControlSite;
private
boolean activated
= false;
/**
* Constructs
the OLE browser
view.
*/
public
OleBrowserView() {}
/**
* Creates
the example.
*
* @see
ViewPart#createPartControl
*/
public
void createPartControl(Composite
parent) {
displayArea = parent
;
// We generate HTML file by transforming XLS predefined resource file
with DNG rss
RSSReader.processXSLTransformation();
// while the html temp file is created, we call the browser and open it
// if the file
transformation has failed, you will a customized error ...
createBrowserControl();
}
/**
* Cleanup
*/
public
void dispose()
{
if
(webBrowser !=
null)
webBrowser.dispose();
webBrowser = null;
super.dispose();
}
public
void setFocus()
{}
/**
* Creates
Web browser
control.
*/
private
void createBrowserControl()
{
// Every control must have an associated OleFrame:
webFrame = new
OleFrame(displayArea, SWT.CENTER);
try
{
// Create an Automation object for access to
extended capabilities
webControlSite =
new OleControlSite(webFrame,
SWT.CENTER, "Shell.Explorer");
OleAutomation oleAutomation
= new
OleAutomation(webControlSite);
webBrowser =
new OleWebBrowser(oleAutomation);
webBrowser.Navigate(RSSReader.htmlFile);
webControlSite.doVerb(OLE.OLEIVERB_INPLACEACTIVATE);
} catch (SWTException
ex) {
ex.printStackTrace();
return;
}
}
}
La méthode CreatePartControl() est la plus importante. Elle est invoquée par le PDE à chaque fois que vous demandez l'affichage de la fenêtre "DotNetGuru News Reader". Son fonctionnement est très proche de son équivalent .NET à travers la méthode Exec() de Connect.cs. Nous effectuons en premier lieu la transformation XSL via la classe RSSReader. Puis dans la méthode CreateBrowserControl() nous réalisons l'instanciation du contrôle graphique Navigateur Web via l'API Ole. C'est tout l'avantage d'un tel Plug-In, les fonctionnalités et les composants utilisés de part et d'autres sont exactement les mêmes, les versions C# et Java faisant appel à l'API COM pour instancier le même composant situé dans la bibliothèque SHDocVW.dll. Ne vous étonnez donc pas si l'apparence graphique (ou Look & Feel) des deux Plug-Ins est très proche.
Nous vous faisons grâce de la classe OleWebBrowser qui est en réalité un décorateur de la DLL précédente. N'hésitez cependant pas à parcourir son contenu, elle est assez symptomatique de la différence existante entre le monde Java et Microsoft lorsqu'il s'agit de faire appel à Ole. Sous Visual Studio, l'intégration de ce dernier a consisté à faire un simple glisser/déplacer du composant vers le formulaire WinForm alors que l'environnement Eclipse est relativement plus contraignant à ce niveau. En effet, l'outil propose un pont Java/COM dans le package org.eclipse.swt.ole.win32.OleAutomation sur laquelle s'appuie notre Plug-In.
Nous ne revenons pas sur le fonctionnement de la classe RSSReader dans laquelle nous avons essayé de reproduire le même fonctionnement que la version .NET.
package DotNetGuruNewsReader.views;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
public class
RSSReader {
public
RSSReader() {
}
public
static String
htmlFile;
public
final static
String ERROR_MSG
= "<html>Le
plug-In n'arrive pas à se connecter au site,
<br>vérifiez que votre connection est active</html>";
public
static void
processXSLTransformation() {
InputStream stream =
null;
URLConnection conn =
null;
try
{
htmlFile =
File.createTempFile("rss",
".html").getAbsolutePath();
Source xsltSource
=
new StreamSource(
RSSReader.class.getClassLoader().getResourceAsStream(
"RSS.xslt"));
TransformerFactory lc_factory
= TransformerFactory.newInstance();
Transformer lc_transformer
= lc_factory.newTransformer(xsltSource);
try {
// Should be improved by getting site list in
preferences
conn =
new URL("http://www.dotnetguru.org/backend.php")
.openConnection();
stream =
conn.getInputStream();
} catch
(IOException e)
{
DataOutputStream outs
= new
DataOutputStream(new
FileOutputStream(htmlFile));
outs.writeBytes(ERROR_MSG);
throw e
;
}
System.out.println(htmlFile);
lc_transformer.transform(
new StreamSource(stream),
new StreamResult(new
FileOutputStream(htmlFile)));
stream.close();
} catch (Exception
e) {
e.printStackTrace();
} finally {
try {
if (stream
!= null)
stream.close();
} catch
(Exception e) {
}
}
}
}
Implémenter les préférences
La dernière étape et non des moindres consiste à fournir l'interface graphique pour le paramétrage du Plug-In. Rappelez vous que Visual Studio proposait cette opération sous la forme d'un simple formulaire WinForm intégré dans une classe d'un type particulier : IDTToolsOptionsPage. Sous Eclipse, là encore le fonctionnement est très proche. Il est nécessaire de dériver de l'interface org.eclipse.ui.IWorkbenchPreferencePage et de la classe abstraite org.eclipse.jface.preference.FieldEditorPreferencePage intégrant les fonctionnalités graphiques de base nécessaires au cycle de vie du contrôle. Cette approche n'est pas sans rappeler le modèle de .NET avec System.Windows.Form.UserControl.
Le résultat auquel on souhaite aboutir est illustré par la copie d'écran suivante.

La classe FieldEditorPreferencePage est la super classe de notre formulaire de préférences. Vous pourrez constater que les méthodes présentes dans cette classe ressemblent à s'y méprendre aux méthodes de la classe OptionsForm.cs de Visual Studio. Ainsi .NET propose GetProperties() et Eclipse GetPreferenceStore(), .NET propose OnOk() et Eclipse PerformOk() .. Si vous avez encore des doutes sur les similitudes entre les deux environnements ...

Vous êtes sûrement impatient de lire le source de la page des préférences, la voici :
package DotNetGuruNewsReader.views;
import DotNetGuruNewsReader.DotNetGuruNewsReaderPlugin;
import org.eclipse.jface.preference.FieldEditorPreferencePage;
import org.eclipse.jface.preference.FileFieldEditor;
import org.eclipse.jface.preference.IntegerFieldEditor;
import org.eclipse.jface.preference.RadioGroupFieldEditor;
import org.eclipse.jface.preference.StringFieldEditor;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchPreferencePage;
public class
PreferencePage extends
FieldEditorPreferencePage implements
IWorkbenchPreferencePage {
protected
SiteListEditor fe1;
protected FileFieldEditor
fe2;
protected BooleanFieldEditor
fe3;
protected StringFieldEditor
fe4;
protected IntegerFieldEditor
fe5;
protected BooleanFieldEditor
fe6;
protected
StringFieldEditor fe7;
protected PasswordFieldEditor
fe8;
protected FileFieldEditor
fe9;
protected IntegerFieldEditor
fe10;
public
PreferencePage() {
super("DotNetGuru
Options", FieldEditorPreferencePage.GRID);
}
/**
* @see
FieldEditorPreferencePage#createFieldEditors()
*/
protected void
createFieldEditors() {
fe1
= new
SiteListEditor(DotNetGuruNewsReaderPlugin.BACKENDS_PREFERENCE,
"Sites",
getFieldEditorParent());
fe3
= new
BooleanFieldEditor(DotNetGuruNewsReaderPlugin.USE_HTTP_PROXY_PREFERENCE,
"Use proxy", getFieldEditorParent());
fe4
= new
StringFieldEditor(DotNetGuruNewsReaderPlugin.HTTP_PROXY_HOST_PREFERENCE,
"Proxy Host",
getFieldEditorParent());
fe5
= new
IntegerFieldEditor(DotNetGuruNewsReaderPlugin.HTTP_PROXY_PORT_PREFERENCE,
"Proxy Port",
getFieldEditorParent());
fe6
= new
BooleanFieldEditor(DotNetGuruNewsReaderPlugin.HTTP_PROXY_LOGIN_PREFERENCE,
"Proxy Login",
getFieldEditorParent());
fe7
= new
StringFieldEditor(DotNetGuruNewsReaderPlugin.HTTP_PROXY_USER_PREFERENCE,
"Proxy user",
getFieldEditorParent());
fe8 =
new PasswordFieldEditor(DotNetGuruNewsReaderPlugin.HTTP_PROXY_PASSWD_PREFERENCE,
"Proxy password",
getFieldEditorParent());
fe9 = new
FileFieldEditor("Fichier","XSL
File",true,getFieldEditorParent());
fe10 = new
IntegerFieldEditor(DotNetGuruNewsReaderPlugin.REFRESH_INTERVAL_PREFERENCE,"Refresh
interval (minutes)", getFieldEditorParent());
fe10.setValidRange(0,10000);
addField(fe1);addField(fe3);addField(fe4);addField(fe5);addField(fe6);addField(fe7);addField(fe8);addField(fe9);addField(fe10);
initUI();
}
/**
* @see
IWorkbenchPreferencePage#init(IWorkbench)
*/
public
void init(IWorkbench
workbench) {
setPreferenceStore(DotNetGuruNewsReaderPlugin.getDefault().getPreferenceStore());
}
public
void propertyChange(PropertyChangeEvent
event) {
super.propertyChange(event);
updateUI();
}
void initUI()
{
boolean
proxy = DotNetGuruNewsReaderPlugin.getDefault().getPluginPreferences().getBoolean(DotNetGuruNewsReaderPlugin.USE_HTTP_PROXY_PREFERENCE);
boolean
authent =
DotNetGuruNewsReaderPlugin.getDefault().getPluginPreferences().getBoolean(DotNetGuruNewsReaderPlugin.HTTP_PROXY_LOGIN_PREFERENCE);
updateFieldStatus(proxy, authent);
}
void updateUI()
{
boolean
proxy = fe3.getBooleanValue();
boolean authent
= fe6.getBooleanValue();
updateFieldStatus(proxy,
authent);
}
void
updateFieldStatus(boolean
proxy, boolean
authent) {
Composite
comp = getFieldEditorParent();
fe4.getTextControl(comp).setEditable(proxy);
fe4.getLabelControl(comp).setEnabled(proxy);
fe5.getTextControl(comp).setEditable(proxy);
fe5.getLabelControl(comp).setEnabled(proxy);
fe6.setEnabled(comp,
proxy);
fe7.getTextControl(comp).setEditable(authent
&& proxy);
fe7.getLabelControl(comp).setEnabled(authent
&& proxy);
fe8.getTextControl(comp).setEditable(authent
&& proxy);
fe8.getLabelControl(comp).setEnabled(authent
&& proxy);
}
/**
* @see
org.eclipse.jface.preference.IPreferencePage#performOk()
*/
public boolean
performOk() {
return
true;
}
}
Rien de bien sorcier, cette classe reprend les mêmes caractéristiques que .NET. Vous aurez au passage noté la présence des classes PasswordFieldEditor, StringFieldEditor, IntegerIFieldEditor qui représentent des contrôles prédéfinis permettant de stocker les préférences à l'aide d'un typage standard. Ceci est clairement un avantage par rapport à .NET qui impose à l'utilisateur de gérer les contraintes de type sur ses champs.
La configuration
Pour rappel, sous Visual Studio, le déploiement nécessitait l'enregistrement préalable du Plug-In dans la base de registre. D'ailleurs, si précedemment nous n'avons pas caché notre préférence pour l'emploi d'un fichier de configuration XML, c'est la démarche utilisée par Eclipse au travers du fichier plugin.xml qui est assurément la brique logicielle la plus importante dans notre projet.
<?xml version="1.0" encoding="UTF-8"?>
<plugin
id="DotNetGuruNewsReader"
name="DotNetGuruNewsReader
Plug-in"
version="1.0.0"
provider-name=""
class="DotNetGuruNewsReader.DotNetGuruNewsReaderPlugin">
<runtime>
<library
name="DotNetGuruNewsReader.jar"/>
</runtime>
<requires>
<import
plugin="org.eclipse.core.boot"/>
<import
plugin="org.eclipse.core.runtime"/>
<import
plugin="org.eclipse.core.resources"/>
<import
plugin="org.eclipse.ui"/>
<import
plugin="org.eclipse.swt"/>
<import
plugin="org.apache.xerces"/>
<import
plugin="org.jnegre.xalanpack"/>
<import
plugin="org.eclipse.update.ui"/>
</requires>
<extension
point="org.eclipse.ui.views">
<category
name="DotNetGuru
Plug-In"
id="DotNetGuruNewsReader">
</category>
<view
name="DotNetGuru
News Reader"
icon="icons/sample.gif"
category="DotNetGuruNewsReader"
class="DotNetGuruNewsReader.views.OleBrowserView"
id="DotNetGuruNewsReader.views.OleBrowserView">
</view>
</extension>
<extension
point="org.eclipse.ui.preferencePages">
<page
name="DotNetGuru"
class="DotNetGuruNewsReader.views.PreferencePage"
id="DotNetGuruNewsReader.views.PreferencePage">
</page>
</extension>
</plugin>
Vous trouverez dans ce fichier l'ensemble des informations
nécessaires à l'IDE pour la configuration du Plug-In. Du nom logique utilisé
Les tests sont du même ordre que ceux de Visual Studio. Eclipse lance une seconde instance de l'éditeur contenant le Plug-In et le debogage est réalisée à partir de la première instance. Quant au déploiement il est d'une simplicité déconcertante. Il vous suffit de créer le fichier archive Jar contenant toute l'application (équivalent du MSI .NET) à partir des menus et de le copier dans un répertoire sous <EclipseHome>/plugin.

Après un tel article, il serait bien aventureux d'affirmer lequel des deux environnements est le plus propice au développement de Plug-In. Visual Studio se prête avec brio aux exercices de conception graphique et de génération automatique de code alors qu'Eclipse nécessite de bonnes connaissances des API internes. A l'inverse, Visual Studio pêche dans l'homogénéité de son Framework encore dépendant de COM alors qu'Eclipse repose sur un ensemble cohérent et homogène de classes et d'interfaces. Malgré cela, ces quelques défauts mineures n'enlèvent strictement rien à la grandeur des deux environnements qui seront indéniablement au coeur des débats dans les années à venir. Il y a fort à parier que la différence se fera entre autre dans la richesse des Plug-Ins qu'ils proposeront chacun. A ce sujet, cet article vous démontre une chose, le développement d'un Plug-In n'est pas l'apanage d'un éditeur logiciel ou d'un expert technique quelconque. Aujourd'hui n'importe quel développeur peut écrire une extension personnalisée pour son éditeur préféré. Si l'exemple du lecteur de News n'est pas le plus judicieux en terme d'Architecture d'entreprise, rien n'empêche d'imaginer des outils tels que des générateurs dynamiques de code ou simplement des programmes destinés à veiller au bon respect des principes architecturaux établis dans le cadre d'un projet. Plus encore, bien malin sera celui qui écrira le premier Plug-In chargé d'avertir en temps réel l'ensemble des développeurs d'un projet lorsqu'un fichier commun est modifié avec la possibilité pour l'auteur de dialoguer en direct avec ses collègues au travers d'un Chat. Autant d'exemples et d'outils qu'il reste à créer ...
Enfin, cet article nous démontre qu'une fois encore, la double compétence est plus que jamais nécessaire, mais était-il encore besoin de le rappeler ....
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 les fichiers sont fournis sous la licence GPL.