I. Introduction▲
EMF, Eclipse Modeling Framework, permet de générer, un peu comme le ferait
un générateur de code UML, une architecture de classes qui représentent
un modèle métier. EMF ne se contente pas de générer seulement les
classes Java, mais aussi toute une infrastructure associée. Ainsi, on
bénéficiera par exemple de la persistance du modèle au format XMI, mais
aussi d'un ensemble d'outils pour interroger le modèle de manière totalement
indépendante des objets qu'il contient. Cette infrastructure permet de construire
des outils de plus haut niveau pour traiter les modèles créés avec EMF.
Au sein de ce framework, une des fonctionnalités est notamment la visualisation et
l'édition de modèles grâce au framework EMF.Edit. Cet article se propose
d'expliquer les mécanismes de base d'EMF.Edit et comment les adapter à des
besoins propres. Nous verrons ainsi comment visualiser très rapidement
l'intégralité d'un modèle et comment le modifier de manière
simple.
Pour suivre cet article, il est indispensable de connaître comment créer et modifier un
modèle EMF. Vous pouvez pour cela consulter les articles suivants :
Dans cet article, j'ai utilisé Eclipse Luna équipé des outils
« Eclipse Modeling Framework SDK ».
Nous allons suivre les étapes suivantes :
- Création du modèle EMF et options de
génération ;
- Explication des mécanismes d'EMF.Edit pour la visualisation du
modèle ;
- Explication des mécanismes d'EMF.Edit pour l'édition du
modèle ;
- Databinding sur les propriétés des objets pour la construction
d'IHM.
II. Création de l'infrastructure▲
II-A. Création du modèle EMF▲
La première étape est évidemment de créer un modèle
EMF. Nous allons nous baser dans cet article sur un exemple simple,
déjà utilisé dans l'article sur Sirius. Ce modèle
représente les liaisons aériennes qui existent entre différents
aéroports. Le modèle contiendra donc un certain nombre
d'aéroports définis par un nom, une ville et un pays. Ces
aéroports ont un ensemble de portes identifiées par un numéro.
Ces portes peuvent référencer une porte d'un autre aéroport,
représentant ainsi une liaison entre deux aéroports.
Créez un nouveau projet de type « Ecore Modeling Project » avec l'ID
« com.abernard.aiports ». Sur la deuxième page, le nom du package
créé doit être « airports » et enfin sur la troisième
page choisissez le viewpoint « Design » pour la création du diagramme.
Cela va créer un fichier .ecore, un fichier .genmodel et ouvrir l'éditeur de
diagrammes EMF. Créez ensuite chacun des éléments du modèle pour obtenir le
diagramme suivant :
II-B. Première génération▲
À partir du fichier *.genmodel, lancez la génération du
plugin de modèle, ainsi que des plugins « Edit » et
« Editor ». Eclipse va générer le code de toutes
les classes ainsi que les plugins
« com.abernard.airports.edit » et
« com.abernard.airports.editor ». À partir de ce
moment, vous pouvez lancer une instance d'Eclipse via le plugin
« *.editor » et dès à présent créer un
petit projet contenant un exemple d'instance de votre modèle
II-C. Création de l'infrastructure▲
Afin de tester les fonctionnalités d'EMF.Edit, nous allons créer
une interface toute simple pour afficher notre modèle et l'éditer
dans le plugin « com.abernard.airports.editor ». Afin de
tirer parti des mécanismes Eclipse, nous allons utiliser un éditeur,
plus spécifiquement un « FormEditor ». Ce type
d'éditeur affiche des pages de type « Form », à
la manière de l'éditeur de fichier
« plugin.xml ». Il définit aussi un certain nombre
d'implémentations par défaut afin d'éviter d'avoir à
implémenter toutes les méthodes de l'interface
« IEditorPart ». Pour cela, n'oubliez pas la
dépendance au bundle « org.eclipse.ui.forms » dans le
MANIFEST.MF :
Puis créez un éditeur qui hérite de
« FormEditor » dans le point d'extension
« org.eclipse.ui.editors ».
Dans cet éditeur, créez une unique page de type
« FormPage » qui contiendra notre IHM. Vous pouvez utiliser
les codes suivants afin de mettre en place ces interfaces vides.
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
package
com.abernard.airports.presentation;
import
org.eclipse.core.runtime.IProgressMonitor;
import
org.eclipse.core.runtime.Status;
import
org.eclipse.ui.IEditorInput;
import
org.eclipse.ui.IEditorSite;
import
org.eclipse.ui.PartInitException;
import
org.eclipse.ui.forms.editor.FormEditor;
import
org.eclipse.ui.statushandlers.StatusManager;
@author
public
class
AirportsFormEditor extends
FormEditor {
private
AirportsFormEditorPage mainPage;
public
AirportsFormEditor
(
) {
}
@Override
protected
void
addPages
(
) {
mainPage =
new
AirportsFormEditorPage
(
this
, "com.abernard.airports.presentation.form1"
, "Contenu"
);
try
{
addPage
(
mainPage);
}
catch
(
PartInitException e) {
StatusManager.getManager
(
).handle
(
new
Status
(
Status.ERROR, "com.abernard.airports.editor"
,
e.getMessage
(
), e), StatusManager.SHOW);
}
}
@Override
public
void
doSave
(
IProgressMonitor monitor) {
}
@Override
public
void
doSaveAs
(
) {
}
@Override
public
void
init
(
IEditorSite site, IEditorInput input)
throws
PartInitException {
super
.init
(
site, input);
}
@Override
public
boolean
isSaveAsAllowed
(
) {
return
false
;
}
}
AirportsFormEditorPage.java
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
package
com.abernard.airports.presentation;
import
org.eclipse.swt.layout.FillLayout;
import
org.eclipse.ui.forms.IManagedForm;
import
org.eclipse.ui.forms.editor.FormPage;
import
org.eclipse.ui.forms.widgets.FormToolkit;
import
org.eclipse.ui.forms.widgets.ScrolledForm;
@author
public
class
AirportsFormEditorPage extends
FormPage {
private
AirportsFormEditor editor;
public
AirportsFormEditorPage
(
AirportsFormEditor airportsFormEditor,
String id, String title) {
super
(
airportsFormEditor, id, title);
this
.editor =
airportsFormEditor;
}
@Override
protected
void
createFormContent
(
IManagedForm managedForm) {
FormToolkit toolkit =
managedForm.getToolkit
(
);
ScrolledForm scrolledForm =
managedForm.getForm
(
);
scrolledForm.setText
(
"Airports Edition"
);
toolkit.decorateFormHeading
(
scrolledForm.getForm
(
));
managedForm.getForm
(
).getBody
(
).setLayout
(
new
FillLayout
(
));
}
}
Si vous lancez votre application et ouvrez votre modèle avec ce nouvel
éditeur, vous verrez une interface vide que nous allons utiliser par la
suite.
III. Visualisation de notre modèle▲
III-A. Visualisation « par défaut »▲
Nous allons maintenant visualiser notre modèle avec les outils de
visualisation d'EMF.Edit par défaut. Regardons de plus près le code
présent dans notre plugin « *.edit ». Une des classes
se nomme « AirportsEditPlugin », elle ne présente pas
d'intérêt intrinsèque : il s'agit de l'Activator du plugin.
Un ensemble de classes nommées « *ItemProvider » sont aussi
générées. Ces classes peuvent être utilisées pour afficher les
éléments du modèle dans des composants JFace via un mécanisme de
délégation : les adapters. Un adapter permet de « faire comme
si » un objet de type A était un objet de type B. Pour regrouper toutes ces
classes et fournir la bonne implémentation à la demander, une dernière classe
est générée : « AirportsItemProviderAdapterFactory ».
Elle génère les classes mentionnées précédemment au besoin.
Toutes ces classes contiennent du code généré, qui doit être considéré
comme tel : il est inutile de le modifier et nous verrons plus tard comment le faire de
manière adaptée. Ce qu'il faut retenir, c'est que ces classes vont nous éviter
beaucoup de codage manuel ! Voyons ça dans un exemple.
Nous voulons visualiser notre modèle dans un arbre, donc un TreeViewer. La première
étape est de charger notre modèle à partir de notre fichier .airports. Dans ce
premier exemple, nous allons utiliser des mécanismes bas niveau d'EMF. Enrichissez la
classe « AirportsFormEditor » comme suit :
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
package
com.abernard.airports.presentation;
import
java.util.Map;
import
org.eclipse.core.resources.IFile;
import
org.eclipse.core.runtime.IProgressMonitor;
import
org.eclipse.core.runtime.Status;
import
org.eclipse.emf.common.util.URI;
import
org.eclipse.emf.ecore.resource.Resource;
import
org.eclipse.emf.ecore.resource.ResourceSet;
import
org.eclipse.emf.ecore.resource.impl.ResourceSetImpl;
import
org.eclipse.emf.ecore.xmi.impl.XMIResourceFactoryImpl;
import
org.eclipse.ui.IEditorInput;
import
org.eclipse.ui.IEditorSite;
import
org.eclipse.ui.IFileEditorInput;
import
org.eclipse.ui.PartInitException;
import
org.eclipse.ui.forms.editor.FormEditor;
import
org.eclipse.ui.statushandlers.StatusManager;
import
com.abernard.airports.WorldMap;
@author
public
class
AirportsFormEditor extends
FormEditor {
private
AirportsFormEditorPage mainPage;
private
WorldMap myModel;
@Override
public
void
init
(
IEditorSite site, IEditorInput input) throws
PartInitException {
super
.init
(
site, input);
loadModel
(
);
}
@throws
private
void
loadModel
(
) throws
PartInitException {
if
(
getEditorInput
(
) instanceof
IFileEditorInput) {
IFile inputFile =
((
IFileEditorInput)getEditorInput
(
)).getFile
(
);
Resource.Factory.Registry reg =
Resource.Factory.Registry.INSTANCE;
Map<
String, Object>
m =
reg.getExtensionToFactoryMap
(
);
m.put
(
"airports"
, new
XMIResourceFactoryImpl
(
));
ResourceSet resSet =
new
ResourceSetImpl
(
);
Resource resource =
resSet.getResource
(
URI
.createFileURI
(
inputFile.getLocation
(
).toString
(
)), true
);
myModel =
(
WorldMap) resource.getContents
(
).get
(
0
);
}
else
{
throw
new
PartInitException
(
new
Status
(
Status.ERROR, "com.abernard.airports.editor"
,
"Unable to open resource"
));
}
}
@return
WorldMap getModel
(
) {
return
myModel;
}
}
Dans notre page, il suffit ensuite de créer un TreeViewer qui va
nécessiter un « IContentProvider » ainsi qu'un
« IBaseLabelProvider ». Si nous travaillions sur des objets
classiques, nous devrions écrire à la main toutes les méthodes
pour parcourir de manière hiérarchique notre modèle ainsi que la
manière d'afficher les éléments. EMF.Edit va ici entrer en jeu.
La première étape est d'instancier la classe conteneur
« AirportsItemProviderAdapterFactory », par exemple dans le
constructeur de la classe. Puis nous pouvons utiliser cet objet pour instancier
deux classes : « AdapterFactoryContentProvider » et
« AdapterFactoryLabelProvider ». Ces classes utilisent
notre « adapter factory » afin d'indiquer à notre
composant comment afficher notre modèle. C'est tout. Modifiez le code de
la classe « AirportsFormEditorPage » de la manière
suivante et observez le résultat!
AirportsFormEditorPage.java
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
package
com.abernard.airports.presentation;
import
org.eclipse.emf.edit.ui.provider.AdapterFactoryContentProvider;
import
org.eclipse.emf.edit.ui.provider.AdapterFactoryLabelProvider;
import
org.eclipse.jface.viewers.TreeViewer;
import
org.eclipse.swt.SWT;
import
org.eclipse.swt.layout.FillLayout;
import
org.eclipse.swt.layout.GridData;
import
org.eclipse.swt.widgets.Composite;
import
org.eclipse.swt.widgets.Tree;
import
org.eclipse.ui.forms.IManagedForm;
import
org.eclipse.ui.forms.editor.FormPage;
import
org.eclipse.ui.forms.widgets.FormToolkit;
import
org.eclipse.ui.forms.widgets.ScrolledForm;
import
com.abernard.airports.provider.AirportsItemProviderAdapterFactory;
@author
public
class
AirportsFormEditorPage extends
FormPage {
private
AirportsFormEditor editor;
private
TreeViewer viewer;
private
AirportsItemProviderAdapterFactory adapterFactory;
public
AirportsFormEditorPage
(
AirportsFormEditor airportsFormEditor,
String id, String title) {
super
(
airportsFormEditor, id, title);
this
.editor =
airportsFormEditor;
this
.adapterFactory =
new
AirportsItemProviderAdapterFactory
(
);
}
@Override
protected
void
createFormContent
(
IManagedForm managedForm) {
FormToolkit toolkit =
managedForm.getToolkit
(
);
ScrolledForm scrolledForm =
managedForm.getForm
(
);
scrolledForm.setText
(
"Airports Edition"
);
toolkit.decorateFormHeading
(
scrolledForm.getForm
(
));
managedForm.getForm
(
).getBody
(
).setLayout
(
new
FillLayout
(
));
Composite container =
managedForm.getForm
(
).getBody
(
);
viewer =
new
TreeViewer
(
container, SWT.BORDER |
SWT.MULTI);
Tree tree =
viewer.getTree
(
);
tree.setLayoutData
(
new
GridData
(
SWT.FILL, SWT.FILL, true
, true
, 3
, 1
));
managedForm.getToolkit
(
).paintBordersFor
(
tree);
viewer.setContentProvider
(
new
AdapterFactoryContentProvider
(
adapterFactory));
viewer.setLabelProvider
(
new
AdapterFactoryLabelProvider
(
adapterFactory));
viewer.setInput
(
editor.getModel
(
));
}
}
Remarquez qu'en instanciant simplement notre « adapter
factory », nous avons pu l'utiliser pour afficher notre modèle
très simplement dans l'arbre !
III-B. Les options de génération▲
Tous les éléments que nous avons utilisés dans ce paragraphe
ont été générés automatiquement dans le plugin *.edit
de notre application. Vous pouvez évidemment modifier les options de
génération pour personnaliser ces éléments. Ces options
doivent être modifiées dans le fichier
« airports.genmodel ». Les options plus générales
sont définies dans l'élément racine du fichier, comme le montre
la capture d'écran ci-dessous :
Ces options vous permettent notamment de définir les différents
« providers » JFace à générer. Par exemple,
la propriété « Stylde Providers » vous permettra,
après génération, d'accéder aux méthodes
« getStyledText » afin d'afficher des styles sur vos
labels. L'option « Table providers », elle, vous permettra
de spécifier le nom des colonnes dans un tableau.
Si vous sélectionnez des éléments du modèle, vous pouvez définir des
propriétés pour chacun, comme le fait d'associer une image aux éléments ou
l'attribut à utiliser par défaut pour afficher les éléments. Un autre
élément intéressant est le type de provider : cette propriété
peut être définie comme « singleton » ou
« stateful ». Avec un provider de type « singleton »,
un seul provider sera instancié et utilisé pour toutes les instances d'un objet de
votre modèle :
À l'inverse, avec un provider « stateful », un
provider sera instancié pour chaque instance des objets de votre
modèle :
III-C. Modifier la visualisation▲
Voir son modèle rapidement c'est bien, adapter l'affichage pour le
mettre à son goût c'est mieux ! Dans ce paragraphe, nous allons
modifier la manière dont les éléments de notre modèle sont
affichés : le label pour les aéroports affichera directement la
ville et le pays, et lorsque des portes sont reliées à d'autres
portes, cette information sera elle aussi directement visible. Une
première solution consiste à modifier directement le code des
providers afin de modifier la méthode « getText() ».
Pour peu qu'on supprime l'annotation « @generated », EMF ne
remplacera pas le code écrit à la main. Pour autant, une bonne
pratique consiste à traiter le code généré comme s'il
s'agissait de bytecode. L'astuce consiste donc à utiliser le design
pattern « Decorator ».
Cette méthode est directement issue d'une présentation
faite à différentes EclipseCon par Mikaël Barbero dont
vous trouverez le lien en fin d'article.
Nous pouvons donc créer un dossier de source
« src-dec » dans notre plugin
« com.abernard.airports.edit ».
N'oubliez pas de modifier le fichier
« build.properties » afin d'ajouter le dossier
« src-dec » aux dossiers sources.
Nous allons commencer par créer un décorateur générique
pour tous nos éléments, que nous allons appeler
« ItemProviderAdapterDecorator » :
ItemProviderAdapterDecorator.java
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
package
com.abernard.airports.provider.decorator;
import
java.util.ArrayList;
import
java.util.List;
import
org.eclipse.emf.common.notify.Adapter;
import
org.eclipse.emf.common.notify.AdapterFactory;
import
org.eclipse.emf.common.notify.Notifier;
import
org.eclipse.emf.edit.provider.IEditingDomainItemProvider;
import
org.eclipse.emf.edit.provider.IItemLabelProvider;
import
org.eclipse.emf.edit.provider.IItemPropertySource;
import
org.eclipse.emf.edit.provider.IStructuredItemContentProvider;
import
org.eclipse.emf.edit.provider.ITreeItemContentProvider;
import
org.eclipse.emf.edit.provider.ItemProviderDecorator;
@author
public
class
ItemProviderAdapterDecorator extends
ItemProviderDecorator implements
Adapter.Internal,
IEditingDomainItemProvider, IStructuredItemContentProvider, ITreeItemContentProvider,
IItemLabelProvider, IItemPropertySource {
private
List&
lt;Notifier>
targets;
public
ItemProviderAdapterDecorator
(
AdapterFactory adapterFactory) {
super
(
adapterFactory);
}
public
Notifier getTarget
(
) {
if
(
targets ==
null
||
targets.isEmpty
(
)) {
return
null
;
}
else
{
return
targets.get
(
targets.size
(
) -
1
);
}
}
public
void
setTarget
(
Notifier newTarget) {
if
(
targets ==
null
) {
targets =
new
ArrayList&
amp;lt;Notifier>(
);
}
targets.add
(
newTarget);
}
public
void
unsetTarget
(
Notifier oldTarget) {
if
(
targets !=
null
) {
targets.remove
(
oldTarget);
}
}
}
Puis nous créons tout d'abord un décorateur
« vide » : il servira à rediriger l'appel de
toutes les méthodes vers leur implémentation par défaut pour
tous les éléments que nous n'allons pas modifier.
ForwardingItemProviderAdapterDecorator.java
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
package
com.abernard.airports.provider.decorator;
@author
public
class
ForwardingItemProviderAdapterDecorator extends
ItemProviderAdapterDecorator {
public
ForwardingItemProviderAdapterDecorator
(
WorldMapDecoratorAdapterFactory airportsDecoratorAdapterFactory) {
super
(
airportsDecoratorAdapterFactory);
}
}
Nous pouvons ensuite créer les deux décorateurs pour nos objets
« Airport » et « Gate ». Ces deux
classes se contentent de redéfinir l'implémentation de la
méthode « getText() ».
AirportItemProviderDecorator.java
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
package
com.abernard.airports.provider.decorator;
import
com.abernard.airports.Airport;
{@link
}
@author
public
class
AirportItemProviderDecorator extends
ItemProviderAdapterDecorator {
public
AirportItemProviderDecorator
(
WorldMapDecoratorAdapterFactory airportsDecoratorAdapterFactory) {
super
(
airportsDecoratorAdapterFactory);
}
@Override
public
String getText
(
Object object) {
Airport airport =
(
Airport)object;
return
airport.getName
(
) +
" ("
+
airport.getCity
(
) +
"; "
+
airport.getCountry
(
) +
")"
;
}
}
GateItemProviderDecorator.java
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
package
com.abernard.airports.provider.decorator;
import
com.abernard.airports.AirportsPackage;
import
com.abernard.airports.Gate;
{@link
}
@author
public
class
GateItemProviderDecorator extends
ItemProviderAdapterDecorator {
public
GateItemProviderDecorator
(
WorldMapDecoratorAdapterFactory worldmapDecoratorAdapterFactory) {
super
(
worldmapDecoratorAdapterFactory);
}
@Override
public
String getText
(
Object object) {
Gate gate =
(
Gate)object;
StringBuilder builder =
new
StringBuilder
(
"Gate "
);
builder.append
(
gate.getNumber
(
));
if
(
gate.eIsSet
(
AirportsPackage.eINSTANCE.getGate_Destination
(
))) {
builder.append
(
" -> "
);
builder.append
(
gate.getDestination
(
).getAirport
(
).getName
(
));
builder.append
(
" (G"
);
builder.append
(
gate.getDestination
(
).getNumber
(
));
builder.append
(
")"
);
}
return
builder.toString
(
);
}
}
Enfin, il ne reste plus qu'à agréger tous ces éléments
au sein d'une « adapter factory » qui réutilisera
évidemment la classe par défaut
« AirportsItemProviderAdapterFactory » :
WorldMapDecoratorAdapterFactory.java
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
package
com.abernard.airports.provider.decorator;
import
org.eclipse.emf.edit.provider.DecoratorAdapterFactory;
import
org.eclipse.emf.edit.provider.IItemProviderDecorator;
import
com.abernard.airports.Airport;
import
com.abernard.airports.Gate;
import
com.abernard.airports.provider.AirportsItemProviderAdapterFactory;
{@link
}
@author
public
class
WorldMapDecoratorAdapterFactory extends
DecoratorAdapterFactory {
public
WorldMapDecoratorAdapterFactory
(
) {
super
(
new
AirportsItemProviderAdapterFactory
(
));
}
@Override
protected
IItemProviderDecorator createItemProviderDecorator
(
Object target, Object Type) {
if
(
target instanceof
Airport) {
return
new
AirportItemProviderDecorator
(
this
);
}
else
if
(
target instanceof
Gate) {
return
new
GateItemProviderDecorator
(
this
);
}
return
new
ForwardingItemProviderAdapterDecorator
(
this
);
}
}
Il ne nous reste plus qu'à observer le résultat. Dans la classe
« AirportsFormEditorPage », il suffit de modifier
l'instanciation de l'« adapter factory » avec notre
nouvelle classe (et de modifier le type de l'attribut en conséquence).
this
.adapterFactory =
new
WorldMapDecoratorAdapterFactory
(
);
On l'a vu, le système des « adapter factories »
permet d'afficher rapidement les éléments d'un modèle. Mais on
peut aller encore plus loin, en combinant plusieurs factories en une seule pour
afficher au sein d'un même viewer plusieurs modèles EMF
différents. Pour cela on passe par une classe spécifique, la
« ComposedAdapterFactory ». Une fois instanciée, on
peut lui ajouter toutes celles que l'on veut traiter. Par exemple, dans
l'éditeur généré par défaut par EMF, on a le code
suivant dans la méthode
« initializeEditingDomain() » :
protected
void
initializeEditingDomain
(
) {
adapterFactory =
new
ComposedAdapterFactory
(
ComposedAdapterFactory.Descriptor.Registry.INSTANCE);
adapterFactory.addAdapterFactory
(
new
ResourceItemProviderAdapterFactory
(
));
adapterFactory.addAdapterFactory
(
new
AirportsItemProviderAdapterFactory
(
));
adapterFactory.addAdapterFactory
(
new
ReflectiveItemProviderAdapterFactory
(
));
}
C'est ce mécanisme qui permet entre autres à l'éditeur par
défaut de combiner l'affichage de la « Resource »
contenant le modèle avec le modèle lui-même. Notez que si vous
voulez accéder à une adapter factory capable d'afficher n'importe
quel modèle disponible dans votre plateforme courante, vous pouvez
utiliser l'instruction suivante :
ComposedAdapterFactory composedAdapterFactory =
new
ComposedAdapterFactory
(
ComposedAdapterFactory.Descriptor.Registry.INSTANCE);
IV. Édition du modèle▲
Un des énormes avantages d'EMF.Edit est que le framework introduit des outils
pour faciliter la manipulation et l'édition des modèles. Ces outils sont
principalement la « CommandStack » et
« l'EditingDomain ». Le premier consiste en une « pile de
commandes » littéralement qui sont stockées lors de leur
exécution, afin d'être éventuellement annulées par la suite. Elles
sont principalement constituées de trois catégories principales : les
commandes de type « Add » (pour ajouter des éléments
à un modèle), les commandes de type « Remove » (pour
supprimer des éléments du modèle) et enfin des commandes de type
« Set » (pour modifier des attributs du modèle). Ces commandes
peuvent être combinées au sein de commandes appelées
« CompoundCommand ». L'objet « EditingDomain »
quant à lui permet d'accéder au modèle afin de l'éditer. Il permet
d'instancier les commandes citées précédemment, de les exécuter et
enfin de faciliter le chargement et la sauvegarde du modèle. Nous allons voir
comment mettre en place et utiliser ces éléments sur notre modèle. La
première étape consiste à initialiser l'EditingDomain et la CommandStack
dans notre éditeur, puis à s'en servir pour charger notre modèle. Nous
allons remplacer la méthode « loadModel » par deux autres,
« initializeEditingDomain » et
« loadModelWithDomain ». Toutes ces méthodes sont donc dans la
classe « AirportsFormEditor.java ».
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
public
class
AirportsFormEditor extends
FormEditor implements
IEditingDomainProvider {
private
ComposedAdapterFactory adapterFactory;
private
AdapterFactoryEditingDomain editingDomain;
@Override
public
void
doSave
(
IProgressMonitor monitor) {
editingDomainDoSave
(
monitor);
}
@Override
public
void
init
(
IEditorSite site, IEditorInput input)
throws
PartInitException {
super
.init
(
site, input);
initializeEditingDomain
(
);
loadModelWithDomain
(
);
}
{@link
}
@param
private
void
editingDomainDoSave
(
IProgressMonitor monitor) {
final
Map<
Object, Object>
saveOptions =
new
HashMap&
lt;Object, Object>(
);
saveOptions.put
(
Resource.OPTION_SAVE_ONLY_IF_CHANGED, Resource.OPTION_SAVE_ONLY_IF_CHANGED_MEMORY_BUFFER);
saveOptions.put
(
Resource.OPTION_LINE_DELIMITER, Resource.OPTION_LINE_DELIMITER_UNSPECIFIED);
WorkspaceModifyOperation operation =
new
WorkspaceModifyOperation
(
) {
@Override
public
void
execute
(
IProgressMonitor monitor) {
Resource resource =
editingDomain.getResourceSet
(
).getResources
(
).get
(
0
);
if
(!
editingDomain.isReadOnly
(
resource)) {
try
{
resource.save
(
saveOptions);
}
catch
(
Exception exception) {
AirportsEditorPlugin.INSTANCE.log
(
exception);
}
}
}
}
;
try
{
new
ProgressMonitorDialog
(
getSite
(
).getShell
(
)).run
(
true
, false
, operation);
((
BasicCommandStack)editingDomain.getCommandStack
(
)).saveIsDone
(
);
firePropertyChange
(
IEditorPart.PROP_DIRTY);
}
catch
(
Exception exception) {
AirportsEditorPlugin.INSTANCE.log
(
exception);
}
}
{@link
}
private
void
loadModelWithDomain
(
) {
URI resourceURI =
EditUIUtil.getURI
(
getEditorInput
(
));
Resource resource =
editingDomain.getResourceSet
(
).getResource
(
resourceURI, true
);
myModel =
(
WorldMap) resource.getContents
(
).get
(
0
);
}
{@link
}
{@link
}
{@link
}
private
void
initializeEditingDomain
(
) {
adapterFactory =
new
ComposedAdapterFactory
(
);
adapterFactory.addAdapterFactory
(
new
WorldMapDecoratorAdapterFactory
(
));
adapterFactory.addAdapterFactory
(
new
ResourceItemProviderAdapterFactory
(
));
adapterFactory.addAdapterFactory
(
new
EcoreAdapterFactory
(
));
adapterFactory.addAdapterFactory
(
new
ReflectiveItemProviderAdapterFactory
(
));
BasicCommandStack commandStack =
new
BasicCommandStack
(
);
commandStack.addCommandStackListener
(
new
CommandStackListener
(
) {
@Override
public
void
commandStackChanged
(
EventObject event) {
Display.getDefault
(
).asyncExec
(
new
Runnable
(
) {
@Override
public
void
run
(
) {
firePropertyChange
(
PROP_DIRTY);
}
}
);
}
}
);
editingDomain =
new
AdapterFactoryEditingDomain
(
adapterFactory, commandStack);
}
{@link
}
@return
ComposedAdapterFactory getAdapterFactory
(
) {
return
adapterFactory;
}
@Override
public
EditingDomain getEditingDomain
(
) {
return
editingDomain;
}
@Override
public
boolean
isDirty
(
) {
boolean
partsDirty =
super
.isDirty
(
);
boolean
emfDirty =
((
BasicCommandStack)editingDomain.getCommandStack
(
)).isSaveNeeded
(
);
return
partsDirty ||
emfDirty;
}
}
Dans le code que nous avons modifié, plusieurs points importants sont à
noter. Intéressons-nous tout d'abord à la méthode
« initializeEditingDomain ». Nous utilisons une implémentation
par défaut de l'interface « CommandStack »,
« BasicCommandStack » qui suffira à la plupart des besoins. De
la même manière, nous utilisons pour l'interface
« EditingDomain » une implémentation standard
« AdapterFactoryEditingDomain ». Cette implémentation utilise
les adapter factories que nous avons mentionnés dans les paragraphes
précédents pour accéder aux éléments du modèle via les
« *ItemProvider ». Nous ajoutons aussi un
« CommandStackListener » afin d'informer le workbench à chaque
fois qu'une commande est exécutée. Cela permettra la sauvegarde du
modèle à chaque fois qu'il est modifié.
Lorsque la propriété « PROP_DIRTY » est modifiée, le workbench
appelle la méthode « isDirty » sur l'éditeur courant. Il faut
donc aussi que nous modifiions cette fonction. Dans un FormEditor, nous pouvons déjà
utiliser l'implémentation par défaut qui vérifie si un des onglets de
l'éditeur est « dirty ». Nous ajoutons à cela un test sur notre
CommandStack qui peut nous indiquer si des commandes ont été exécutées sur
notre modèle depuis son état initial.
Enfin lorsque nous effectuons la sauvegarde de notre modèle, il faut aussi penser à informer
la CommandStack que la sauvegarde a été effectuée. Cela permet de faire en
quelque sorte un « reset » sur cet élément. Une fois que ces
mécanismes sont mis en place, il ne nous reste plus qu'à modifier notre interface
pour ajouter des commandes. Nous allons tester les trois commandes au sein de trois actions
simples. Pour cela, nous devons enrichir notre classe
« AirportsEditorFormPage » pour l'ajout des boutons idoines.
AirportsFormEditorPage.java
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
package
com.abernard.airports.presentation;
@author
public
class
AirportsFormEditorPage extends
FormPage {
private
AirportsFormEditor editor;
private
TreeViewer viewer;
public
AirportsFormEditorPage
(
AirportsFormEditor airportsFormEditor, String id, String title) {
super
(
airportsFormEditor, id, title);
this
.editor =
airportsFormEditor;
}
@Override
protected
void
createFormContent
(
IManagedForm managedForm) {
FormToolkit toolkit =
managedForm.getToolkit
(
);
ScrolledForm scrolledForm =
managedForm.getForm
(
);
scrolledForm.setText
(
"Airports Edition"
);
toolkit.decorateFormHeading
(
scrolledForm.getForm
(
));
managedForm.getForm
(
).getBody
(
).setLayout
(
new
FillLayout
(
));
Composite container =
managedForm.getForm
(
).getBody
(
);
container.setLayout
(
new
GridLayout
(
3
, false
));
Button buttonNewAirport =
managedForm.getToolkit
(
).createButton
(
container, "New Airport"
, SWT.NONE);
buttonNewAirport.setLayoutData
(
new
GridData
(
SWT.LEFT, SWT.CENTER, true
, false
, 1
, 1
));
buttonNewAirport.addSelectionListener
(
new
SelectionAdapter
(
) {
@Override
public
void
widgetSelected
(
SelectionEvent e) {
handleCreateNewAirport
(
);
}
}
);
Button buttonRemoveAirport =
managedForm.getToolkit
(
).createButton
(
container, "Remove selected airports"
,
SWT.NONE);
buttonRemoveAirport.setLayoutData
(
new
GridData
(
SWT.LEFT, SWT.CENTER, true
, false
, 1
, 1
));
buttonRemoveAirport.addSelectionListener
(
new
SelectionAdapter
(
) {
@Override
public
void
widgetSelected
(
SelectionEvent e) {
handleRemoveAirport
(
);
}
}
);
Button changeNameAirport =
managedForm.getToolkit
(
).createButton
(
container, "Change selection name"
, SWT.NONE);
changeNameAirport.setLayoutData
(
new
GridData
(
SWT.LEFT, SWT.CENTER, true
, false
, 1
, 1
));
changeNameAirport.addSelectionListener
(
new
SelectionAdapter
(
) {
@Override
public
void
widgetSelected
(
SelectionEvent e) {
handleChangeNameAirport
(
);
}
}
);
viewer =
new
TreeViewer
(
container, SWT.BORDER |
SWT.MULTI);
Tree tree =
viewer.getTree
(
);
tree.setLayoutData
(
new
GridData
(
SWT.FILL, SWT.FILL, true
, true
, 3
, 1
));
managedForm.getToolkit
(
).paintBordersFor
(
tree);
viewer.setContentProvider
(
new
AdapterFactoryContentProvider
(
editor.getAdapterFactory
(
)));
viewer.setLabelProvider
(
new
AdapterFactoryLabelProvider
(
editor.getAdapterFactory
(
)));
viewer.setInput
(
editor.getModel
(
));
getSite
(
).setSelectionProvider
(
viewer);
}
{@link
}
private
void
handleCreateNewAirport
(
) {
Command add =
AddCommand.create
(
editor.getEditingDomain
(
), editor.getModel
(
),
AirportsPackage.eINSTANCE.getWorldMap_Airports
(
), AirportsFactory.eINSTANCE.createAirport
(
));
editor.getEditingDomain
(
).getCommandStack
(
).execute
(
add);
}
{@link
}
private
void
handleRemoveAirport
(
) {
IStructuredSelection ssel =
(
IStructuredSelection)viewer.getSelection
(
);
if
(!
ssel.isEmpty
(
)) {
List<
Airport>
itemsToRemove =
new
ArrayList&
amp;lt;>(
);
for
(
Iterator<
?>
it =
ssel.iterator
(
); it.hasNext
(
);) {
Object o =
it.next
(
);
if
(
o instanceof
Airport) {
itemsToRemove.add
((
Airport)o);
}
}
Command c =
RemoveCommand.create
(
editor.getEditingDomain
(
), itemsToRemove);
editor.getEditingDomain
(
).getCommandStack
(
).execute
(
c);
}
}
{@code
}
{@link
}
private
void
handleChangeNameAirport
(
) {
IStructuredSelection ssel =
(
IStructuredSelection)viewer.getSelection
(
);
if
(!
ssel.isEmpty
(
)) {
for
(
Iterator<
?>
it =
ssel.iterator
(
); it.hasNext
(
);) {
Object o =
it.next
(
);
if
(
o instanceof
Airport) {
Command set =
SetCommand.create
(
editor.getEditingDomain
(
), (
Airport)o,
AirportsPackage.eINSTANCE.getAirport_Name
(
), "Toto"
);
editor.getEditingDomain
(
).getCommandStack
(
).execute
(
set);
}
}
}
}
}
Nous avons ajouté trois boutons pour ajouter un aéroport, en supprimer et
remplacer le nom par « Toto ». Tout d'abord la commande
« Add ». Si on regarde la syntaxe de la commande, rien de
transcendant. On communique l'EditingDomain afin que la commande puisse manipuler ledit
modèle, le modèle en question, le nom de la « feature »
à laquelle ajouter le nouvel élément ainsi que le nouvel
élément créé. On peut s'en douter, cette commande va ajouter le
nouvel élément créé à la fin de la liste
« airports » de notre modèle. Puis, au lieu d'appeler
simplement la méthode « execute » sur la commande, on passe
par la CommandStack. Utiliser ces mécanismes a de multiples avantages :
- on peut annuler l'action en utilisant simplement
« editingDomain.getCommandStack().undo() » ;
- on peut vérifier si un modèle doit être sauvegardé
(rappelez-vous la méthode « isSaveNeeded » que nous
avons utilisée dans la méthode « isDirty » de
notre éditeur) ;
- on peut enfin utiliser l'instruction
« myCommand.canExecute() » pour s'assurer que le contexte
courant permet l'exécution d'une commande.
Notez que pour personnaliser le comportement des commandes, on peut écrire des
sous-classes de toutes les commandes définies par le framework.
Les deux autres commandes fonctionnent exactement de la même manière et leur fonctionnement
est aisément compréhensible. On peut vérifier le bon fonctionnement de ces
commandes, et le fait que notre éditeur peut bien être sauvegardé lorsqu'on les
a utilisées.
La cerise sur le gâteau est que l'utilisation de ces mécanismes (commandes
et EditingDomain) va nous permettre de mettre en place des commandes de type
« Undo/Redo » ou « Copy/Paste » simplement.
Pour cela, il suffit de créer une barre d'outils d'éditeur qui est une
sous-classe de « EditingDomainActionBarContributor » (pour rappel,
cela se fait dans le champ « contributorClass » de votre
éditeur dans le fichier « plugin.xml »). Cette classe active
ses propriétés dès lors que l'éditeur courant implémente
l'interface « IEditingDomainProvider ». On peut dès lors
profiter de ces mécanismes sans autre action supplémentaire !
Pour que ces commandes fonctionnent correctement, il est impératif que
l'arbre soit enregistré comme fournisseur de sélection, grâce
à l'instruction
« getSite().setSelectionProvider(viewer); ».
Comme d'habitude, on peut modifier le comportement de la classe, par exemple, on
peut activer très simplement la validation de notre modèle en ajoutant dans
le code la commande de validation. Les contraintes basiques des objets de notre
modèle sont alors validées (comme dans l'éditeur Ecore).
FormEditorActionBarContributor.java
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
package
com.abernard.airports.presentation;
import
org.eclipse.emf.edit.ui.action.EditingDomainActionBarContributor;
import
org.eclipse.emf.edit.ui.action.ValidateAction;
import
org.eclipse.jface.action.IToolBarManager;
{@link
}
@author
public
class
FormEditorActionBarContributor extends
EditingDomainActionBarContributor {
public
FormEditorActionBarContributor
(
) {
}
@Override
public
void
contributeToToolBar
(
IToolBarManager toolBarManager) {
super
.contributeToToolBar
(
toolBarManager);
validateAction =
new
ValidateAction
(
);
toolBarManager.add
(
validateAction);
}
}
Enfin, si on veut bénéficier de commandes plus avancées, comme le
menu contextuel pour la création d'éléments mis en place dans
l'éditeur généré, il suffit de réutiliser ou sous-classer la
classe générée « AirportsActionBarContributor ».
V. Databinding avec EMF.Edit▲
V-A. Mise en place▲
Nous savons maintenant comment afficher les éléments contenus dans
notre modèle et éventuellement les modifier. Néanmoins nous
aurons vite le besoin ou l'envie de construire des interfaces plus
spécifiques pour modifier les propriétés avancées. Nous
allons voir dans ce paragraphe comment éditer les propriétés des
objets EMF via le databinding. Ce mécanisme consiste à lier
directement les valeurs affichées dans l'IHM aux valeurs contenues dans le
modèle. Lorsque le modèle est modifié, l'affichage l'est aussi
directement et vice-versa. Cela évite de passer par les classiques, mais
fastidieux mécanismes de listeners, etc. Le databinding peut être
réalisé sur des objets classiques, cf. ce tutorielDatabinding avec SWT. Nous
allons
créer un élément de type MasterDetails pour afficher les
détails des éléments sélectionnés dans l'arbre. Pour
cela, il faut de nouveau compléter la vue
« AirportsFormEditorPage ».
AirportsFormEditorPage.java
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
package
com.abernard.airports.presentation;
public
class
AirportsFormEditorPage extends
FormPage {
@Override
protected
void
createFormContent
(
IManagedForm managedForm) {
FormToolkit toolkit =
managedForm.getToolkit
(
);
ScrolledForm scrolledForm =
managedForm.getForm
(
);
scrolledForm.setText
(
"Airports Edition"
);
toolkit.decorateFormHeading
(
scrolledForm.getForm
(
));
managedForm.getForm
(
).getBody
(
).setLayout
(
new
FillLayout
(
));
MasterDetailsBlock masterDetails =
new
MasterDetailsBlock
(
) {
@Override
protected
void
registerPages
(
DetailsPart detailsPart) {
detailsPart.registerPage
(
AirportImpl.class
, new
AirportDetailsPage
(
editor));
}
@Override
protected
void
createToolBarActions
(
IManagedForm managedForm) {
}
@Override
protected
void
createMasterPart
(
final
IManagedForm managedForm, Composite parent) {
final
SectionPart sPart =
new
SectionPart
(
parent, managedForm.getToolkit
(
), Section.TITLE_BAR);
managedForm.addPart
(
sPart);
Composite container =
managedForm.getToolkit
(
).createComposite
(
sPart.getSection
(
), SWT.NONE);
sPart.getSection
(
).setClient
(
container);
sPart.getSection
(
).setText
(
"WorldMap"
);
container.setLayout
(
new
GridLayout
(
3
, false
));
Button buttonNewAirport =
managedForm.getToolkit
(
).createButton
(
container, "New Airport"
, SWT.NONE);
viewer.addSelectionChangedListener
(
new
ISelectionChangedListener
(
) {
@Override
public
void
selectionChanged
(
SelectionChangedEvent event) {
managedForm.fireSelectionChanged
(
sPart, event.getSelection
(
));
}
}
);
}
}
;
masterDetails.createContent
(
managedForm, managedForm.getForm
(
).getBody
(
));
}
}
Ce code supplémentaire crée le composant MasterDetails, il ne nous
reste plus qu'à créer une page de détails pour nos objets
« Airport ».
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
161.
162.
163.
164.
165.
166.
167.
168.
169.
170.
171.
172.
173.
174.
175.
package
com.abernard.airports.presentation;
import
org.eclipse.core.databinding.Binding;
import
org.eclipse.core.databinding.UpdateValueStrategy;
import
org.eclipse.core.databinding.observable.value.IObservableValue;
import
org.eclipse.core.databinding.observable.value.WritableValue;
import
org.eclipse.core.runtime.IStatus;
import
org.eclipse.core.runtime.Status;
import
org.eclipse.emf.databinding.EMFDataBindingContext;
import
org.eclipse.emf.databinding.IEMFValueProperty;
import
org.eclipse.emf.databinding.edit.EMFEditProperties;
import
org.eclipse.jface.databinding.fieldassist.ControlDecorationSupport;
import
org.eclipse.jface.databinding.swt.IWidgetValueProperty;
import
org.eclipse.jface.databinding.swt.WidgetProperties;
import
org.eclipse.jface.viewers.ISelection;
import
org.eclipse.jface.viewers.IStructuredSelection;
import
org.eclipse.swt.SWT;
import
org.eclipse.swt.layout.FillLayout;
import
org.eclipse.swt.layout.GridData;
import
org.eclipse.swt.layout.GridLayout;
import
org.eclipse.swt.widgets.Composite;
import
org.eclipse.swt.widgets.Display;
import
org.eclipse.swt.widgets.Label;
import
org.eclipse.swt.widgets.Text;
import
org.eclipse.ui.forms.IDetailsPage;
import
org.eclipse.ui.forms.IFormPart;
import
org.eclipse.ui.forms.IManagedForm;
import
org.eclipse.ui.forms.widgets.FormToolkit;
import
org.eclipse.ui.forms.widgets.Section;
import
com.abernard.airports.Airport;
import
com.abernard.airports.AirportsPackage;
{@link
}
@author
public
class
AirportDetailsPage implements
IDetailsPage {
private
AirportsFormEditor refEditor;
private
IManagedForm managedForm;
private
Text textName;
private
Text textCity;
private
Text textCountry;
private
IObservableValue modelValue =
new
WritableValue
(
);
public
AirportDetailsPage
(
AirportsFormEditor editor) {
this
.refEditor =
editor;
}
@Override
public
void
initialize
(
IManagedForm form) {
this
.managedForm =
form;
}
@Override
public
void
dispose
(
) {
}
@Override
public
boolean
isDirty
(
) {
return
false
;
}
@Override
public
void
commit
(
boolean
onSave) {
}
@Override
public
boolean
setFormInput
(
Object input) {
return
false
;
}
@Override
public
void
setFocus
(
) {
}
@Override
public
boolean
isStale
(
) {
return
false
;
}
@Override
public
void
refresh
(
) {
}
@Override
public
void
selectionChanged
(
IFormPart part, ISelection selection) {
IStructuredSelection ssel =
(
IStructuredSelection)selection;
Object first =
ssel.getFirstElement
(
);
if
(
first !=
null
&
amp;amp;&
amp;amp; first instanceof
Airport) {
modelValue.setValue
((
Airport)first);
}
}
@Override
public
void
createContents
(
Composite parent) {
parent.setLayout
(
new
FillLayout
(
SWT.HORIZONTAL));
FormToolkit toolkit =
managedForm.getToolkit
(
);
Section section =
toolkit.createSection
(
parent, Section.TITLE_BAR);
section.setText
(
"Airport details"
);
Composite container =
toolkit.createComposite
(
section);
container.setLayout
(
new
GridLayout
(
2
, false
));
section.setClient
(
container);
Label lblName =
new
Label
(
container, SWT.NONE);
lblName.setLayoutData
(
new
GridData
(
SWT.RIGHT, SWT.CENTER, false
, false
, 1
, 1
));
toolkit.adapt
(
lblName, true
, true
);
lblName.setText
(
"Name:"
);
textName =
new
Text
(
container, SWT.BORDER);
textName.setLayoutData
(
new
GridData
(
SWT.FILL, SWT.CENTER, true
, false
, 1
, 1
));
toolkit.adapt
(
textName, true
, true
);
Label lblCity =
new
Label
(
container, SWT.NONE);
lblCity.setLayoutData
(
new
GridData
(
SWT.RIGHT, SWT.CENTER, false
, false
, 1
, 1
));
toolkit.adapt
(
lblCity, true
, true
);
lblCity.setText
(
"City:"
);
textCity =
new
Text
(
container, SWT.BORDER);
textCity.setLayoutData
(
new
GridData
(
SWT.FILL, SWT.CENTER, true
, false
, 1
, 1
));
toolkit.adapt
(
textCity, true
, true
);
Label lblCountry =
new
Label
(
container, SWT.NONE);
lblCountry.setLayoutData
(
new
GridData
(
SWT.RIGHT, SWT.CENTER, false
, false
, 1
, 1
));
toolkit.adapt
(
lblCountry, true
, true
);
lblCountry.setText
(
"Country:"
);
textCountry =
new
Text
(
container, SWT.BORDER);
textCountry.setLayoutData
(
new
GridData
(
SWT.FILL, SWT.CENTER, true
, false
, 1
, 1
));
toolkit.adapt
(
textCountry, true
, true
);
createDatabinding
(
);
}
private
void
createDatabinding
(
) {
EMFDataBindingContext ctx =
new
EMFDataBindingContext
(
);
IWidgetValueProperty widgetValue =
WidgetProperties.text
(
SWT.Modify);
IEMFValueProperty shortProp =
EMFEditProperties.value
(
refEditor.getEditingDomain
(
),
AirportsPackage.Literals.AIRPORT__NAME);
Binding b =
ctx.bindValue
(
widgetValue.observeDelayed
(
200
, textName),
shortProp.observeDetail
(
modelValue));
shortProp =
EMFEditProperties.value
(
refEditor.getEditingDomain
(
),
AirportsPackage.Literals.AIRPORT__CITY);
b =
ctx.bindValue
(
widgetValue.observeDelayed
(
200
, textCity),
shortProp.observeDetail
(
modelValue));
shortProp =
EMFEditProperties.value
(
refEditor.getEditingDomain
(
),
AirportsPackage.Literals.AIRPORT__COUNTRY);
b =
ctx.bindValue
(
widgetValue.observeDelayed
(
200
, textCountry),
shortProp.observeDetail
(
modelValue));
}
}
Dans cette classe, intéressons-nous aux méthodes
« selectionChanged » et
« createDatabinding » (le reste ne constitue que de
l'interface pure et ne présente pas d'intérêt). La première
encapsule l'élément sélectionné dans l'arbre dans un objet
de type « WritableValue » qui va permettre de modifier et
lire les propriétés à observer et va déclencher les
événements lorsque ces valeurs sont modifiées (soit par le
modèle, soit par l'interface). L'objet IObservableValue va notamment
éviter des NullPointerException dans tous les sens si jamais l'objet du
modèle est « null » et que les champs de l'interface
ne peuvent donc rien afficher.
La deuxième méthode va lier chaque champ texte à une propriété EMF. Cela se
fait en utilisant les méthodes statiques de la classe EMFEditProperties, qui
évidemment va utiliser un objet EditingDomain. Cela va permettre directement de
bénéficier de la sauvegarde et de l'« Undo/Redo » ! Notez
aussi l'emploi de la méthode « observeDelayed » avec un temps en
millisecondes : au lieu de modifier le modèle à chaque fois que le champ texte
est modifié, un timer sera utilisé (ici 200 ms). Cela évite un trop grand
nombre d'événements lorsque l'utilisateur est en train de taper son texte. Avec ces
mécanismes, on peut vite vérifier que notre modèle est bien mis à jour
lorsqu'on modifie une des trois valeurs.
V-B. Validation▲
Évidemment, l'intérêt d'une interface est aussi
d'empêcher l'utilisateur de rentrer des valeurs abracadabrantes, pour cela
on peut évidemment réutiliser les mécanismes de validation du
databinding dans Eclipse sur nos éléments. Par exemple nous allons
stipuler que chacune des trois données doit commencer par une majuscule.
Nous utilisons donc un objet spécifique de type
« UpdateValueStrategy » pour valider la donnée
entrée par l'utilisateur avant qu'elle ne soit communiquée au
modèle.
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
package
com.abernard.airports.presentation;
public
class
AirportDetailsPage implements
IDetailsPage {
private
void
createDatabinding
(
) {
EMFDataBindingContext ctx =
new
EMFDataBindingContext
(
);
IWidgetValueProperty widgetValue =
WidgetProperties.text
(
SWT.Modify);
IEMFValueProperty shortProp =
EMFEditProperties.value
(
refEditor.getEditingDomain
(
),
AirportsPackage.Literals.AIRPORT__NAME);
Binding b =
ctx.bindValue
(
widgetValue.observeDelayed
(
200
, textName),
shortProp.observeDetail
(
modelValue), new
CapitalizedFirstLetter
(
textName), null
);
ControlDecorationSupport.create
(
b, SWT.TOP |
SWT.LEFT);
shortProp =
EMFEditProperties.value
(
refEditor.getEditingDomain
(
),
AirportsPackage.Literals.AIRPORT__CITY);
b =
ctx.bindValue
(
widgetValue.observeDelayed
(
200
, textCity),
shortProp.observeDetail
(
modelValue), new
CapitalizedFirstLetter
(
textCity), null
);
ControlDecorationSupport.create
(
b, SWT.TOP |
SWT.LEFT);
shortProp =
EMFEditProperties.value
(
refEditor.getEditingDomain
(
),
AirportsPackage.Literals.AIRPORT__COUNTRY);
b =
ctx.bindValue
(
widgetValue.observeDelayed
(
200
, textCountry),
shortProp.observeDetail
(
modelValue), new
CapitalizedFirstLetter
(
textCountry), null
);
ControlDecorationSupport.create
(
b, SWT.TOP |
SWT.LEFT);
}
@author
private
class
CapitalizedFirstLetter extends
UpdateValueStrategy {
private
Text text;
public
CapitalizedFirstLetter
(
Text textField) {
this
.text =
textField;
}
@Override
public
IStatus validateBeforeSet
(
Object value) {
IStatus status;
if
(
value instanceof
String) {
char
first =
((
String) value).charAt
(
0
);
if
(
Character.isUpperCase
(
first)) {
status =
Status.OK_STATUS;
}
else
{
status =
new
Status
(
Status.ERROR, "com.abernard.airports.presentation"
,
"Text shall start with an uppercase letter"
);
}
}
else
{
status =
super
.validateBeforeSet
(
value);
}
if
(
status.getSeverity
(
) !=
Status.OK) {
text.setBackground
(
Display.getDefault
(
).getSystemColor
(
SWT.COLOR_RED));
}
else
{
text.setBackground
(
Display.getDefault
(
).getSystemColor
(
SWT.COLOR_WHITE));
}
return
status;
}
}
}
Notez l'utilisation très directe des objets
« ControlDecorationSupport » qui vont afficher sur les
champs le texte d'erreur ainsi qu'un petit marqueur visuel. On peut donc tester
notre composant et vérifier que le modèle n'est pas modifié si
nos valeurs ne commencent pas par une majuscule. Sur la capture ci-dessous, on
voit bien le champ invalide avec la notification, on constate aussi que l'arbre
n'est pas modifié (on a rentré « paris », mais le
modèle contient toujours la valeur « Paris ») et
l'éditeur n'est pas marqué comme « dirty ».
VI. Conclusion et perspectives▲
Dans cet article nous avons eu une première approche du framework EMF.Edit et
j'espère qu'avec ces informations, vous aurez les éléments en main pour
construire des interfaces autour de vos modèles EMF. Les mécanismes de ce
framework permettent de mettre en œuvre rapidement une IHM avec tous les
éléments nécessaires pour l'utilisateur final avec un minimum de
code : validation des données, commandes classiques
d'« Undo/Redo » ou de gestion du presse-papiers. En utilisant ce
framework, on peut aussi aller encore plus loin et directement créer des
interfaces personnalisées sans écrire de code, grâce à des outils
comme EMFForms, EMF Editing Framework ou encore EMFParsley. Ces outils feront
peut-être l'objet de prochains articles ! En attendant, si vous pensez qu'un
diagramme représenterait mieux notre modèle, vous pouvez jeter un œil
à mon article sur SiriusTutoriel sur Eclipse Sirius.
Enfin,
l'éditeur généré par défaut ainsi que la barre d'outils
associée, même s'ils peuvent être verbeux, sont un bon réservoir
d'idées et de bonnes pratiques pour vos propres éditeurs !
VII. Liens utiles▲
Vous trouverez dans cette section quelques liens qui peuvent être utiles sur
EMF.Edit ou les frameworks évoqués en conclusion.
N'oubliez pas que pour ce qui a trait à EMF, le livre « EMF, 2d
Edition » reste une référence, bien qu'un peu daté !
VIII. Remerciements▲
Pour cet article, je remercie Yassine
OUHAMMOU, Claude
LELOUP et Malick SECK pour
leur relecture attentive. Je tiens aussi à remercier Mikaël Barbero pour ses
présentations d'EMF.Edit et pour la démarche des décorateurs pour les
classes générées.