Tutoriel : À la découverte d'EMF.Edit

Alain BERNARD

Avec EMF, l'Eclipse Modeling Framework, il est possible de générer du code rapidement pour un modèle donné. EMF ne se contente pas seulement de générer le code des classes, mais aussi d'instrumenter ce code. Grâce à cette instrumentation, il est possible de construire des frameworks de plus haut niveau, indépendants de la logique métier derrière un modèle. Parmi ces frameworks, EMF.Edit permet de générer et d'utiliser un ensemble de classes pour la visualisation et l'édition des modèles EMF. Cet article se propose de donner un aperçu des mécanismes de base de ce framework. Les sources de cet exemple sont disponibles à l'adresse suivante : FTP ou HTTP.

Pour toute remarque ou question sur ce tutoriel, profitez de cette discussion : Commentez

- Titre : Tutoriel : À la découverte d'EMF.Edit

- Auteur : Alain BERNARD

- Parution : 4 mars 2015

- Mise à jour : 12 avril 2015

- Public visé :

- Licence : Copyright ® 2014 Alain BERNARD. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. Création de l'infrastructure

- II-A. Création du modèle EMF

- II-B. Première génération

- II-C. Création de l'infrastructure

- III. Visualisation de notre modèle

- III-A. Visualisation « par défaut »

- III-B. Les options de génération

- III-C. Modifier la visualisation

- IV. Édition du modèle

- V. Databinding avec EMF.Edit

- V-A. Mise en place

- V-B. Validation

- VI. Conclusion et perspectives

- VII. Liens utiles

- VIII. Remerciements

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 :

- tutoriel de Mickaël BARON ;

- tutoriel de Lars VOGEL.

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 :

Diagramme du modèle EMF

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.

AirportsFormEditor.java

1. package com.abernard.airports.presentation;

3. import org.eclipse.core.runtime.IProgressMonitor;

4. import org.eclipse.core.runtime.Status;

5. import org.eclipse.ui.IEditorInput;

6. import org.eclipse.ui.IEditorSite;

7. import org.eclipse.ui.PartInitException;

8. import org.eclipse.ui.forms.editor.FormEditor;

9. import org.eclipse.ui.statushandlers.StatusManager;

11. /**

12. * Cet editeur contient une unique page pour editer notre modele EMF.

13. * @author A. BERNARD

14. *

15. */

16. public class AirportsFormEditor extends FormEditor {

18. /* Etape 1 : mise en place */

19. private AirportsFormEditorPage mainPage;

21. public AirportsFormEditor() {

22. //

23. }

25. @Override

26. protected void addPages() {

27. mainPage = new AirportsFormEditorPage(this, "com.abernard.airports.presentation.form1", "Contenu");

28. try {

29. addPage(mainPage);

30. } catch (PartInitException e) {

31. StatusManager.getManager().handle(new Status(Status.ERROR, "com.abernard.airports.editor",

32. e.getMessage(), e), StatusManager.SHOW);

33. }

35. }

37. @Override

38. public void doSave(IProgressMonitor monitor) {

39. }

41. @Override

42. public void doSaveAs() {

43. // nous n'autorisons pas le saveAs...

44. }

46. @Override

47. public void init(IEditorSite site, IEditorInput input)

48. throws PartInitException {

49. super.init(site, input);

50. }

52. @Override

53. public boolean isSaveAsAllowed() {

54. return false;

55. }

57. }

AirportsFormEditorPage.java

1. package com.abernard.airports.presentation;

3. import org.eclipse.swt.layout.FillLayout;

4. import org.eclipse.ui.forms.IManagedForm;

5. import org.eclipse.ui.forms.editor.FormPage;

6. import org.eclipse.ui.forms.widgets.FormToolkit;

7. import org.eclipse.ui.forms.widgets.ScrolledForm;

9. /**

10. * Cette unique page affiche notre modele EMF dans un arbre et permet son edition.

11. * @author A. BERNARD

12. *

13. */

14. public class AirportsFormEditorPage extends FormPage {

16. private AirportsFormEditor editor;

18. public AirportsFormEditorPage(AirportsFormEditor airportsFormEditor,

19. String id, String title) {

20. super(airportsFormEditor, id, title);

21. this.editor = airportsFormEditor;

22. }

24. @Override

25. protected void createFormContent(IManagedForm managedForm) {

26. FormToolkit toolkit = managedForm.getToolkit();

27. ScrolledForm scrolledForm = managedForm.getForm();

28. scrolledForm.setText("Airports Edition");

29. toolkit.decorateFormHeading(scrolledForm.getForm());

30. managedForm.getForm().getBody().setLayout(new FillLayout());

31. }

32. }

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.

Notre éditeur, vide

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 :

AirportsFormEditor.java

1. package com.abernard.airports.presentation;

3. import java.util.Map;

5. import org.eclipse.core.resources.IFile;

6. import org.eclipse.core.runtime.IProgressMonitor;

7. import org.eclipse.core.runtime.Status;

8. import org.eclipse.emf.common.util.URI;

9. import org.eclipse.emf.ecore.resource.Resource;

10. import org.eclipse.emf.ecore.resource.ResourceSet;

11. import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl;

12. import org.eclipse.emf.ecore.xmi.impl.XMIResourceFactoryImpl;

13. import org.eclipse.ui.IEditorInput;

14. import org.eclipse.ui.IEditorSite;

15. import org.eclipse.ui.IFileEditorInput;

16. import org.eclipse.ui.PartInitException;

17. import org.eclipse.ui.forms.editor.FormEditor;

18. import org.eclipse.ui.statushandlers.StatusManager;

20. import com.abernard.airports.WorldMap;

22. /**

23. * Cet editeur contient une unique page pour editer notre modele EMF.

24. * @author A. BERNARD

25. *

26. */

27. public class AirportsFormEditor extends FormEditor {

29. /* Etape 1 : mise en place */

30. private AirportsFormEditorPage mainPage;

31. /* Etape 2 : chargement du modele */

32. private WorldMap myModel;

34. // ...

36. @Override

37. public void init(IEditorSite site, IEditorInput input) throws PartInitException {

38. super.init(site, input);

39. loadModel();

40. }

42. /**

43. * Charge le modele a partir du fichier ouvert.

44. * @throws PartInitException

45. */

46. private void loadModel() throws PartInitException {

47. /* Etape 2 : chargement du modele */

48. if (getEditorInput() instanceof IFileEditorInput) {

49. IFile inputFile = ((IFileEditorInput)getEditorInput()).getFile();

50. Resource.Factory.Registry reg = Resource.Factory.Registry.INSTANCE;

51. Map<String, Object> m = reg.getExtensionToFactoryMap();

52. m.put("airports", new XMIResourceFactoryImpl());

54. ResourceSet resSet = new ResourceSetImpl();

55. Resource resource = resSet.getResource(URI

56. .createFileURI(inputFile.getLocation().toString()), true);

57. myModel = (WorldMap) resource.getContents().get(0);

58. } else {

59. throw new PartInitException(new Status(Status.ERROR, "com.abernard.airports.editor",

60. "Unable to open resource"));

61. }

62. }

64. /**

65. * Retourne le modele charge dans cet editeur.

66. * @return

67. */

68. WorldMap getModel() {

69. return myModel;

70. }

71. }

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. package com.abernard.airports.presentation;

3. import org.eclipse.emf.edit.ui.provider.AdapterFactoryContentProvider;

4. import org.eclipse.emf.edit.ui.provider.AdapterFactoryLabelProvider;

5. import org.eclipse.jface.viewers.TreeViewer;

6. import org.eclipse.swt.SWT;

7. import org.eclipse.swt.layout.FillLayout;

8. import org.eclipse.swt.layout.GridData;

9. import org.eclipse.swt.widgets.Composite;

10. import org.eclipse.swt.widgets.Tree;

11. import org.eclipse.ui.forms.IManagedForm;

12. import org.eclipse.ui.forms.editor.FormPage;

13. import org.eclipse.ui.forms.widgets.FormToolkit;

14. import org.eclipse.ui.forms.widgets.ScrolledForm;

16. import com.abernard.airports.provider.AirportsItemProviderAdapterFactory;

18. /**

19. * Cette unique page affiche notre modele EMF dans un arbre et permet son edition.

20. * @author A. BERNARD

21. *

22. */

23. public class AirportsFormEditorPage extends FormPage {

25. private AirportsFormEditor editor;

26. private TreeViewer viewer;

28. private AirportsItemProviderAdapterFactory adapterFactory;

30. public AirportsFormEditorPage(AirportsFormEditor airportsFormEditor,

31. String id, String title) {

32. super(airportsFormEditor, id, title);

33. this.editor = airportsFormEditor;

35. this.adapterFactory = new AirportsItemProviderAdapterFactory();

37. }

39. @Override

40. protected void createFormContent(IManagedForm managedForm) {

41. FormToolkit toolkit = managedForm.getToolkit();

42. ScrolledForm scrolledForm = managedForm.getForm();

43. scrolledForm.setText("Airports Edition");

44. toolkit.decorateFormHeading(scrolledForm.getForm());

45. managedForm.getForm().getBody().setLayout(new FillLayout());

47. Composite container = managedForm.getForm().getBody();

48. viewer = new TreeViewer(container, SWT.BORDER | SWT.MULTI);

49. Tree tree = viewer.getTree();

50. tree.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 3, 1));

51. managedForm.getToolkit().paintBordersFor(tree);

52. viewer.setContentProvider(new AdapterFactoryContentProvider(adapterFactory));

53. viewer.setLabelProvider(new AdapterFactoryLabelProvider(adapterFactory));

54. viewer.setInput(editor.getModel());

55. }

56. }

Visualisation de notre modèle

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 :

Options générales d'édition

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 :

Singleton pattern

À l'inverse, avec un provider « stateful », un provider sera instancié pour chaque instance des objets de votre modèle :

Stateful pattern

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. package com.abernard.airports.provider.decorator;

3. import java.util.ArrayList;

4. import java.util.List;

6. import org.eclipse.emf.common.notify.Adapter;

7. import org.eclipse.emf.common.notify.AdapterFactory;

8. import org.eclipse.emf.common.notify.Notifier;

9. import org.eclipse.emf.edit.provider.IEditingDomainItemProvider;

10. import org.eclipse.emf.edit.provider.IItemLabelProvider;

11. import org.eclipse.emf.edit.provider.IItemPropertySource;

12. import org.eclipse.emf.edit.provider.IStructuredItemContentProvider;

13. import org.eclipse.emf.edit.provider.ITreeItemContentProvider;

14. import org.eclipse.emf.edit.provider.ItemProviderDecorator;

16. /**

17. * Cette classe est un decorateur pour nos items providers.

18. * @author A. BERNARD

19. *

20. */

21. public class ItemProviderAdapterDecorator extends ItemProviderDecorator implements Adapter.Internal,

22. IEditingDomainItemProvider, IStructuredItemContentProvider, ITreeItemContentProvider,

23. IItemLabelProvider, IItemPropertySource {

25. private List&lt;Notifier> targets;

27. public ItemProviderAdapterDecorator(AdapterFactory adapterFactory) {

28. super(adapterFactory);

29. }

31. public Notifier getTarget() {

32. if (targets == null || targets.isEmpty()) {

33. return null;

34. } else {

35. return targets.get(targets.size() - 1);

36. }

37. }

39. public void setTarget(Notifier newTarget) {

40. if (targets == null) {

41. targets = new ArrayList&amp;lt;Notifier>();

42. }

43. targets.add(newTarget);

44. }

46. public void unsetTarget(Notifier oldTarget) {

47. if (targets != null) {

48. targets.remove(oldTarget);

49. }

50. }

51. }

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. package com.abernard.airports.provider.decorator;

3. /**

4. * Cette classe est un decorateur "vide" qui delegue tous les appels aux

5. * methodes par defaut.

6. * @author A. BERNARD

7. *

8. */

9. public class ForwardingItemProviderAdapterDecorator extends ItemProviderAdapterDecorator {

11. public ForwardingItemProviderAdapterDecorator(WorldMapDecoratorAdapterFactory airportsDecoratorAdapterFactory) {

12. super (airportsDecoratorAdapterFactory);

13. }

14. }

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. package com.abernard.airports.provider.decorator;

3. import com.abernard.airports.Airport;

5. /**

6. * Cette classe redefinit la methode {@link #getText(Object)} generee par defaut.

7. * @author A. BERNARD

8. *

9. */

10. public class AirportItemProviderDecorator extends ItemProviderAdapterDecorator {

12. public AirportItemProviderDecorator(WorldMapDecoratorAdapterFactory airportsDecoratorAdapterFactory) {

13. super(airportsDecoratorAdapterFactory);

14. }

16. @Override

17. public String getText(Object object) {

18. Airport airport = (Airport)object;

19. return airport.getName() + " (" + airport.getCity() + "; " + airport.getCountry() + ")";

20. }

21. }

GateItemProviderDecorator.java

1. package com.abernard.airports.provider.decorator;

3. import com.abernard.airports.AirportsPackage;

4. import com.abernard.airports.Gate;

6. /**

7. * Cette classe redefinit la methode {@link #getText(Object)} generee par defaut.

8. * @author A. BERNARD

9. *

10. */

11. public class GateItemProviderDecorator extends ItemProviderAdapterDecorator {

13. public GateItemProviderDecorator(WorldMapDecoratorAdapterFactory worldmapDecoratorAdapterFactory) {

14. super(worldmapDecoratorAdapterFactory);

15. }

17. @Override

18. public String getText(Object object) {

19. Gate gate = (Gate)object;

20. StringBuilder builder = new StringBuilder("Gate ");

21. builder.append(gate.getNumber());

22. if (gate.eIsSet(AirportsPackage.eINSTANCE.getGate_Destination())) {

23. builder.append(" -> ");

24. builder.append(gate.getDestination().getAirport().getName());

25. builder.append(" (G");

26. builder.append(gate.getDestination().getNumber());

27. builder.append(")");

28. }

29. return builder.toString();

30. }

31. }

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. package com.abernard.airports.provider.decorator;

3. import org.eclipse.emf.edit.provider.DecoratorAdapterFactory;

4. import org.eclipse.emf.edit.provider.IItemProviderDecorator;

6. import com.abernard.airports.Airport;

7. import com.abernard.airports.Gate;

8. import com.abernard.airports.provider.AirportsItemProviderAdapterFactory;

10. /**

11. * Cette nouvelle classe remplace celle par défaut {@link AirportsItemProviderAdapterFactory}

12. * afin de rediriger les appels vers nos nouveaux items providers.

13. * @author A. BERNARD

14. *

15. */

16. public class WorldMapDecoratorAdapterFactory extends DecoratorAdapterFactory {

18. public WorldMapDecoratorAdapterFactory() {

19. super(new AirportsItemProviderAdapterFactory());

20. }

22. @Override

23. protected IItemProviderDecorator createItemProviderDecorator(Object target, Object Type) {

24. if (target instanceof Airport) {

25. return new AirportItemProviderDecorator(this);

26. } else if (target instanceof Gate) {

27. return new GateItemProviderDecorator(this);

28. }

29. return new ForwardingItemProviderAdapterDecorator(this);

30. }

32. }

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();
// ...

Nouveaux labels

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() {
// Create an adapter factory that yields item providers.
//
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);

Composed Factory

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 ».

AirportsFormEditor.java

1. // imports ...

2. public class AirportsFormEditor extends FormEditor implements IEditingDomainProvider {

4. /* Etape 3 : tester un composed adapter */

5. private ComposedAdapterFactory adapterFactory;

6. /* Etape 4 : creer un EditingDomain */

7. private AdapterFactoryEditingDomain editingDomain;

9. // ...

11. @Override

12. public void doSave(IProgressMonitor monitor) {

13. editingDomainDoSave(monitor);

14. }

16. @Override

17. public void init(IEditorSite site, IEditorInput input)

18. throws PartInitException {

19. super.init(site, input);

20. //    loadModel();

21. initializeEditingDomain();

22. loadModelWithDomain();

23. }

25. /**

26. * Sauvegarde le modele en utilisant les informations contenues dans l'{@link EditingDomain}.

27. * @param monitor

28. */

29. private void editingDomainDoSave(IProgressMonitor monitor) {

30. final Map<Object, Object> saveOptions = new HashMap&lt;Object, Object>();

31. saveOptions.put(Resource.OPTION_SAVE_ONLY_IF_CHANGED, Resource.OPTION_SAVE_ONLY_IF_CHANGED_MEMORY_BUFFER);

32. saveOptions.put(Resource.OPTION_LINE_DELIMITER, Resource.OPTION_LINE_DELIMITER_UNSPECIFIED);

34. // Utiliser une "WorkspaceModifyOperation" est tout indique pour une action qui peut

35. //   etre longue au sein du workspace

36. WorkspaceModifyOperation operation = new WorkspaceModifyOperation() {

37. @Override

38. public void execute(IProgressMonitor monitor) {

39. Resource resource = editingDomain.getResourceSet().getResources().get(0);

40. if (!editingDomain.isReadOnly(resource)) {

41. try {

42. resource.save(saveOptions);

43. } catch (Exception exception) {

44. AirportsEditorPlugin.INSTANCE.log(exception);

45. }

46. }

47. }

49. };

51. try {

52. new ProgressMonitorDialog(getSite().getShell()).run(true, false, operation);

53. ((BasicCommandStack)editingDomain.getCommandStack()).saveIsDone();

54. firePropertyChange(IEditorPart.PROP_DIRTY);

55. } catch (Exception exception) {

56. AirportsEditorPlugin.INSTANCE.log(exception);

57. }

58. }

60. /**

61. * Charge le modele en utilisant l'{@link EditingDomain}

62. */

63. private void loadModelWithDomain() {

64. URI resourceURI = EditUIUtil.getURI(getEditorInput());

65. Resource resource = editingDomain.getResourceSet().getResource(resourceURI, true);

66. myModel = (WorldMap) resource.getContents().get(0);

67. }

69. /**

70. * Initialise la {@link CommandStack} et l'{@link EditingDomain} en utilisant

71. * une {@link ComposedAdapterFactory}.

72. */

73. private void initializeEditingDomain() {

74. adapterFactory = new ComposedAdapterFactory();

75. adapterFactory.addAdapterFactory(new WorldMapDecoratorAdapterFactory());

76. adapterFactory.addAdapterFactory(new ResourceItemProviderAdapterFactory());

77. adapterFactory.addAdapterFactory(new EcoreAdapterFactory());

78. adapterFactory.addAdapterFactory(new ReflectiveItemProviderAdapterFactory());

80. BasicCommandStack commandStack = new BasicCommandStack();

81. commandStack.addCommandStackListener(new CommandStackListener() {

82. @Override

83. public void commandStackChanged(EventObject event) {

84. Display.getDefault().asyncExec(new Runnable() {

85. @Override

86. public void run() {

87. firePropertyChange(PROP_DIRTY);

88. }

89. });

90. }

91. });

92. editingDomain = new AdapterFactoryEditingDomain(adapterFactory, commandStack);

93. }

95. /**

96. * Retourne l'instance de {@link ComposedAdapterFactory} utilisee dans cet editeur.

97. * @return

98. */

99. ComposedAdapterFactory getAdapterFactory() {

100. return adapterFactory;

101. }

103. @Override

104. public EditingDomain getEditingDomain() {

105. return editingDomain;

106. }

108. @Override

109. public boolean isDirty() {

110. boolean partsDirty = super.isDirty();

111. boolean emfDirty = ((BasicCommandStack)editingDomain.getCommandStack()).isSaveNeeded();

112. return partsDirty || emfDirty;

113. }

114. // ...

115. }

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. package com.abernard.airports.presentation;

3. // imports...

5. /**

6. * Cette unique page affiche notre modele EMF dans un arbre et permet son edition.

7. * @author A. BERNARD

8. *

9. */

10. public class AirportsFormEditorPage extends FormPage {

12. private AirportsFormEditor editor;

13. private TreeViewer viewer;

15. public AirportsFormEditorPage(AirportsFormEditor airportsFormEditor, String id, String title) {

16. super(airportsFormEditor, id, title);

17. this.editor = airportsFormEditor;

19. }

21. @Override

22. protected void createFormContent(IManagedForm managedForm) {

23. FormToolkit toolkit = managedForm.getToolkit();

24. ScrolledForm scrolledForm = managedForm.getForm();

25. scrolledForm.setText("Airports Edition");

26. toolkit.decorateFormHeading(scrolledForm.getForm());

27. managedForm.getForm().getBody().setLayout(new FillLayout());

28. Composite container = managedForm.getForm().getBody();

29. container.setLayout(new GridLayout(3, false));

31. Button buttonNewAirport = managedForm.getToolkit().createButton(container, "New Airport", SWT.NONE);

32. buttonNewAirport.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, true, false, 1, 1));

33. buttonNewAirport.addSelectionListener(new SelectionAdapter() {

34. @Override

35. public void widgetSelected(SelectionEvent e) {

36. handleCreateNewAirport();

37. }

38. });

39. Button buttonRemoveAirport = managedForm.getToolkit().createButton(container, "Remove selected airports",

40. SWT.NONE);

41. buttonRemoveAirport.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, true, false, 1, 1));

42. buttonRemoveAirport.addSelectionListener(new SelectionAdapter() {

43. @Override

44. public void widgetSelected(SelectionEvent e) {

45. handleRemoveAirport();

46. }

47. });

48. Button changeNameAirport = managedForm.getToolkit().createButton(container, "Change selection name", SWT.NONE);

49. changeNameAirport.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, true, false, 1, 1));

50. changeNameAirport.addSelectionListener(new SelectionAdapter() {

51. @Override

52. public void widgetSelected(SelectionEvent e) {

53. handleChangeNameAirport();

54. }

55. });

57. viewer = new TreeViewer(container, SWT.BORDER | SWT.MULTI);

58. Tree tree = viewer.getTree();

59. tree.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 3, 1));

60. managedForm.getToolkit().paintBordersFor(tree);

61. viewer.setContentProvider(new AdapterFactoryContentProvider(editor.getAdapterFactory()));

62. viewer.setLabelProvider(new AdapterFactoryLabelProvider(editor.getAdapterFactory()));

63. viewer.setInput(editor.getModel());

64. getSite().setSelectionProvider(viewer);

65. }

67. /**

68. * Ajoute un aeroport au modele en utilisant une {@link AddCommand}.

69. */

70. private void handleCreateNewAirport() {

71. Command add = AddCommand.create(editor.getEditingDomain(), editor.getModel(),

72. AirportsPackage.eINSTANCE.getWorldMap_Airports(), AirportsFactory.eINSTANCE.createAirport());

73. editor.getEditingDomain().getCommandStack().execute(add);

74. }

76. /**

77. * Supprime tous les aeroports selectionnes du modele en utilisant une {@link RemoveCommand}.

78. */

79. private void handleRemoveAirport() {

80. IStructuredSelection ssel = (IStructuredSelection)viewer.getSelection();

81. if (!ssel.isEmpty()) {

82. List<Airport> itemsToRemove = new ArrayList&amp;lt;>();

83. for (Iterator<?> it = ssel.iterator(); it.hasNext();) {

84. Object o = it.next();

85. if (o instanceof Airport) {

86. itemsToRemove.add((Airport)o);

87. }

88. }

89. Command c = RemoveCommand.create(editor.getEditingDomain(), itemsToRemove);

90. editor.getEditingDomain().getCommandStack().execute(c);

92. }

93. }

95. /**

96. * Change le nom de tous les aeroports selectionnes en {@code Toto} en utilisant

97. * une {@link SetCommand}.

98. */

99. private void handleChangeNameAirport() {

100. IStructuredSelection ssel = (IStructuredSelection)viewer.getSelection();

101. if (!ssel.isEmpty()) {

102. for (Iterator<?> it = ssel.iterator(); it.hasNext();) {

103. Object o = it.next();

104. if (o instanceof Airport) {

105. Command set = SetCommand.create(editor.getEditingDomain(), (Airport)o,

106. AirportsPackage.eINSTANCE.getAirport_Name(), "Toto");

107. editor.getEditingDomain().getCommandStack().execute(set);

108. }

109. }

110. }

111. }

112. }

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.

Test de la commande 'Add'

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. package com.abernard.airports.presentation;

3. import org.eclipse.emf.edit.ui.action.EditingDomainActionBarContributor;

4. import org.eclipse.emf.edit.ui.action.ValidateAction;

5. import org.eclipse.jface.action.IToolBarManager;

7. /**

8. * L'heritage de {@link EditingDomainActionBarContributor} permet de beneficier

9. * automatiquement des commandes Undo/Redo/etc...

10. * @author A. BERNARD

11. *

12. */

13. public class FormEditorActionBarContributor extends EditingDomainActionBarContributor {

15. public FormEditorActionBarContributor() {

16. //

17. }

19. @Override

20. public void contributeToToolBar(IToolBarManager toolBarManager) {

21. super.contributeToToolBar(toolBarManager);

22. validateAction = new ValidateAction();

23. toolBarManager.add(validateAction);

24. }

26. }

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 tutoriel. 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. package com.abernard.airports.presentation;

3. // imports...

5. public class AirportsFormEditorPage extends FormPage {

7. // ...

9. @Override

10. protected void createFormContent(IManagedForm managedForm) {

11. FormToolkit toolkit = managedForm.getToolkit();

12. ScrolledForm scrolledForm = managedForm.getForm();

13. scrolledForm.setText("Airports Edition");

14. toolkit.decorateFormHeading(scrolledForm.getForm());

15. managedForm.getForm().getBody().setLayout(new FillLayout());

16. MasterDetailsBlock masterDetails = new MasterDetailsBlock() {

18. @Override

19. protected void registerPages(DetailsPart detailsPart) {

20. detailsPart.registerPage(AirportImpl.class, new AirportDetailsPage(editor));

22. }

24. @Override

25. protected void createToolBarActions(IManagedForm managedForm) {

28. }

30. @Override

31. protected void createMasterPart(final IManagedForm managedForm, Composite parent) {

32. final SectionPart sPart = new SectionPart(parent, managedForm.getToolkit(), Section.TITLE_BAR);

33. managedForm.addPart(sPart);

34. Composite container = managedForm.getToolkit().createComposite(sPart.getSection(), SWT.NONE);

35. sPart.getSection().setClient(container);

36. sPart.getSection().setText("WorldMap");

38. container.setLayout(new GridLayout(3, false));

40. Button buttonNewAirport = managedForm.getToolkit().createButton(container, "New Airport", SWT.NONE);

42. // ... creation des boutons et du viewer...

44. viewer.addSelectionChangedListener(new ISelectionChangedListener() {

45. @Override

46. public void selectionChanged(SelectionChangedEvent event) {

47. managedForm.fireSelectionChanged(sPart, event.getSelection());

48. }

49. });

50. }

51. };

52. masterDetails.createContent(managedForm, managedForm.getForm().getBody());

54. }

56. // ...

57. }

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 ».

AirportDetailsPage.java

1. package com.abernard.airports.presentation;

3. import org.eclipse.core.databinding.Binding;

4. import org.eclipse.core.databinding.UpdateValueStrategy;

5. import org.eclipse.core.databinding.observable.value.IObservableValue;

6. import org.eclipse.core.databinding.observable.value.WritableValue;

7. import org.eclipse.core.runtime.IStatus;

8. import org.eclipse.core.runtime.Status;

9. import org.eclipse.emf.databinding.EMFDataBindingContext;

10. import org.eclipse.emf.databinding.IEMFValueProperty;

11. import org.eclipse.emf.databinding.edit.EMFEditProperties;

12. import org.eclipse.jface.databinding.fieldassist.ControlDecorationSupport;

13. import org.eclipse.jface.databinding.swt.IWidgetValueProperty;

14. import org.eclipse.jface.databinding.swt.WidgetProperties;

15. import org.eclipse.jface.viewers.ISelection;

16. import org.eclipse.jface.viewers.IStructuredSelection;

17. import org.eclipse.swt.SWT;

18. import org.eclipse.swt.layout.FillLayout;

19. import org.eclipse.swt.layout.GridData;

20. import org.eclipse.swt.layout.GridLayout;

21. import org.eclipse.swt.widgets.Composite;

22. import org.eclipse.swt.widgets.Display;

23. import org.eclipse.swt.widgets.Label;

24. import org.eclipse.swt.widgets.Text;

25. import org.eclipse.ui.forms.IDetailsPage;

26. import org.eclipse.ui.forms.IFormPart;

27. import org.eclipse.ui.forms.IManagedForm;

28. import org.eclipse.ui.forms.widgets.FormToolkit;

29. import org.eclipse.ui.forms.widgets.Section;

31. import com.abernard.airports.Airport;

32. import com.abernard.airports.AirportsPackage;

34. /**

35. * Cette classe affiche certaines informations principales de nos objets {@link Airport} :

36. * le nom, le pays et la ville.

37. * @author A. BERNARD

38. *

39. */

40. public class AirportDetailsPage implements IDetailsPage {

42. private AirportsFormEditor refEditor;

43. private IManagedForm managedForm;

44. private Text textName;

45. private Text textCity;

46. private Text textCountry;

48. private IObservableValue modelValue = new WritableValue();

50. public AirportDetailsPage(AirportsFormEditor editor) {

51. this.refEditor = editor;

52. }

56. @Override

57. public void initialize(IManagedForm form) {

58. this.managedForm = form;

59. }

61. @Override

62. public void dispose() {

65. }

67. @Override

68. public boolean isDirty() {

69. return false;

70. }

72. @Override

73. public void commit(boolean onSave) {

76. }

78. @Override

79. public boolean setFormInput(Object input) {

80. return false;

81. }

83. @Override

84. public void setFocus() {

85. //

87. }

89. @Override

90. public boolean isStale() {

91. return false;

92. }

94. @Override

95. public void refresh() {

96. //

97. }

99. @Override

100. public void selectionChanged(IFormPart part, ISelection selection) {

101. IStructuredSelection ssel = (IStructuredSelection)selection;

102. Object first = ssel.getFirstElement();

103. if (first != null &amp;amp;&amp;amp; first instanceof Airport) {

104. modelValue.setValue((Airport)first);

105. }

106. }

108. @Override

109. public void createContents(Composite parent) {

110. parent.setLayout(new FillLayout(SWT.HORIZONTAL));

111. FormToolkit toolkit = managedForm.getToolkit();

112. Section section = toolkit.createSection(parent, Section.TITLE_BAR);

113. section.setText("Airport details");

114. Composite container = toolkit.createComposite(section);

115. container.setLayout(new GridLayout(2, false));

116. section.setClient(container);

118. Label lblName = new Label(container, SWT.NONE);

119. lblName.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));

120. toolkit.adapt(lblName, true, true);

121. lblName.setText("Name:");

123. textName = new Text(container, SWT.BORDER);

124. textName.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));

125. toolkit.adapt(textName, true, true);

127. Label lblCity = new Label(container, SWT.NONE);

128. lblCity.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));

129. toolkit.adapt(lblCity, true, true);

130. lblCity.setText("City:");

132. textCity = new Text(container, SWT.BORDER);

133. textCity.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));

134. toolkit.adapt(textCity, true, true);

136. Label lblCountry = new Label(container, SWT.NONE);

137. lblCountry.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));

138. toolkit.adapt(lblCountry, true, true);

139. lblCountry.setText("Country:");

141. textCountry = new Text(container, SWT.BORDER);

142. textCountry.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));

143. toolkit.adapt(textCountry, true, true);

145. createDatabinding();

146. }

149. /**

150. * Initialise le databinding entre l'element du modele et les composants graphiques.

151. */

152. private void createDatabinding() {

153. EMFDataBindingContext ctx = new EMFDataBindingContext();

154. IWidgetValueProperty widgetValue = WidgetProperties.text(SWT.Modify);

156. // Airport name

157. IEMFValueProperty shortProp = EMFEditProperties.value(refEditor.getEditingDomain(),

158. AirportsPackage.Literals.AIRPORT__NAME);

159. Binding b = ctx.bindValue(widgetValue.observeDelayed(200, textName),

160. shortProp.observeDetail(modelValue));

162. // Airport city

163. shortProp = EMFEditProperties.value(refEditor.getEditingDomain(),

164. AirportsPackage.Literals.AIRPORT__CITY);

165. b = ctx.bindValue(widgetValue.observeDelayed(200, textCity),

166. shortProp.observeDetail(modelValue));

168. // Airport country

169. shortProp = EMFEditProperties.value(refEditor.getEditingDomain(),

170. AirportsPackage.Literals.AIRPORT__COUNTRY);

171. b = ctx.bindValue(widgetValue.observeDelayed(200, textCountry),

172. shortProp.observeDetail(modelValue));

174. }

175. }

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.

Databinding simple

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.

AirportDetailsPage.java

1. package com.abernard.airports.presentation;

3. // imports...

5. public class AirportDetailsPage implements IDetailsPage {

7. // ...

9. /**

10. * Initialise le databinding entre l'element du modele et les composants graphiques.

11. */

12. private void createDatabinding() {

13. EMFDataBindingContext ctx = new EMFDataBindingContext();

14. IWidgetValueProperty widgetValue = WidgetProperties.text(SWT.Modify);

16. // Airport name

17. IEMFValueProperty shortProp = EMFEditProperties.value(refEditor.getEditingDomain(),

18. AirportsPackage.Literals.AIRPORT__NAME);

19. Binding b = ctx.bindValue(widgetValue.observeDelayed(200, textName),

20. shortProp.observeDetail(modelValue), new CapitalizedFirstLetter(textName), null);

21. ControlDecorationSupport.create(b, SWT.TOP | SWT.LEFT);

23. // Airport city

24. shortProp = EMFEditProperties.value(refEditor.getEditingDomain(),

25. AirportsPackage.Literals.AIRPORT__CITY);

26. b = ctx.bindValue(widgetValue.observeDelayed(200, textCity),

27. shortProp.observeDetail(modelValue), new CapitalizedFirstLetter(textCity), null);

28. ControlDecorationSupport.create(b, SWT.TOP | SWT.LEFT);

30. // Airport country

31. shortProp = EMFEditProperties.value(refEditor.getEditingDomain(),

32. AirportsPackage.Literals.AIRPORT__COUNTRY);

33. b = ctx.bindValue(widgetValue.observeDelayed(200, textCountry),

34. shortProp.observeDetail(modelValue), new CapitalizedFirstLetter(textCountry), null);

35. ControlDecorationSupport.create(b, SWT.TOP | SWT.LEFT);

37. }

39. /**

40. * Cette classe valide que les donnees entrees commencent par une majuscule.

41. * @author A. BERNARD

42. *

43. */

44. private class CapitalizedFirstLetter extends UpdateValueStrategy {

46. private Text text;

48. public CapitalizedFirstLetter(Text textField) {

49. this.text = textField;

50. }

52. @Override

53. public IStatus validateBeforeSet(Object value) {

54. IStatus status;

55. if (value instanceof String) {

56. char first = ((String) value).charAt(0);

57. if (Character.isUpperCase(first)) {

58. status = Status.OK_STATUS;

59. } else {

60. status = new Status(Status.ERROR, "com.abernard.airports.presentation",

61. "Text shall start with an uppercase letter");

62. }

63. } else {

64. status = super.validateBeforeSet(value);

65. }

66. if (status.getSeverity() != Status.OK) {

67. text.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_RED));

68. } else {

69. text.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE));

70. }

71. return status;

72. }

73. }

74. }

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 ».

Databinding avec validation

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 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.

- EMF.Edit overview dans l'aide d'Eclipse

- Présentation d'EMF.Edit

- Tutoriel sur EMFForms

- Extended Editing Framework

- EMF Parsley

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.