JFace Databinding avec des composants JFace

Alain Bernard

Ce tutoriel se propose de détailler l'utilisation du framework JFace Databinding sous Eclipse afin de permettre la liaison entre un modèle de données et des composants de visualisation JFace. Cet article se situe donc dans la droite lignée de l'article JFace Databinding avec SWT. Cet article est basé sur Eclipse 3.7. Les sources de l'exemple sont disponibles à l'adresse suivante : FTP ou HTTP.

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

- Titre : JFace Databinding avec des composants JFace

- Auteur : Alain Bernard

- Parution : 29 juillet 2012

- Mise à jour : 13 octobre 2012

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2012 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. Un premier exemple

- III. Observer les changements plus précisément

- IV. Utilisation de WindowBuilder

- V. Liens utiles

- VI. Conclusion

- VII. Remerciements

I - Introduction

L'API JFace Databinding d'Eclipse permet de lier facilement les données du modèle objet aux informations affichées par l'interface graphique. Il est en effet intéressant que les données entrées par l'utilisateur soient répercutées directement dans le modèle, et vice versa. Dans le précédent article, nous avons étudié les différents mécanismes de l'API au travers d'un exemple simple qui utilisait uniquement des composants graphiques SWT. On se propose ici de mettre en œuvre ce framework sur des composants graphiques JFace. Pour cet article, il est nécessaire d'avoir des connaissances de base dans les domaines suivants :

- développement de plugins avec Eclipse : création de vues, gestion des dépendances ;

- utilisation de SWT, notamment sur les différents composants et leurs propriétés.

II - Un premier exemple

Dans ce premier exemple, nous allons lier de manière la plus simple possible une liste d'éléments de notre modèle à un viewer JFace. Commençons par modifier notre modèle de manière à travailler sur une liste d'objets. Pour cela, nous nous basons sur la classe PersonBean.java que nous avons créée dans le premier article. Nous créons ensuite la classe PersonList qui peut nous retourner une liste d'objets PersonBean grâce à une méthode statique :

PersonBean.java

1. package com.abernard.jfacedatabinding.model;

3. import java.beans.PropertyChangeListener;

4. import java.beans.PropertyChangeSupport;

6. /**

7. * Cette classe definit une personne dont les informations peuvent etre

8. * presentees dans nos interfaces. Cette classe utilise un objet de type

9. * PropertyChangeSupport pour repercuter les modifications du modele sur

10. * l'interface graphique.

11. *

12. * @author A. Bernard

13. */

14. public class PersonBean {

16. /**

17. * nom de la personne

18. */

19. private String name;

20. /**

21. * prenom de la personne

22. */

23. private String firstname;

24. /**

25. * sexe de la personne

26. */

27. private boolean male;

28. /**

29. * age de la personne

30. */

31. private int age;

33. /**

34. * cet element permet de notifier tous les listeners qu'une modification a

35. * ete effectuee dans le modele

36. */

37. private PropertyChangeSupport propertyChange;

39. /**

40. * Constructeur

41. */

42. public PersonBean() {

43. this.propertyChange = new PropertyChangeSupport(this);

44. this.name = "";

45. this.firstname = "";

46. this.male = true;

47. this.age = 0;

48. }

50. /**

51. * Constructeur avec arguments

52. */

53. public PersonBean(String n, String f, int a, boolean m) {

54. this.propertyChange = new PropertyChangeSupport(this);

55. this.setName(n);

56. this.setFirstname(f);

57. this.setMale(m);

58. this.setAge(a);

60. }

62. /**

63. * Ajoute un element a la liste des ecouteurs des modifications de cette

64. * classe

65. * @param propertyName la propriete que l'element veut surveiller

66. * @param listener l'element qui s'abonne

67. */

68. public void addPropertyChangeListener(String propertyName,

69. PropertyChangeListener listener) {

70. propertyChange.addPropertyChangeListener(propertyName, listener);

71. }

73. /**

74. * Supprime un element a la liste des ecouteurs des modifications de cette

75. * classe

76. * @param listener l'element a supprimer

77. */

78. public void removePropertyChangeListener(PropertyChangeListener listener) {

79. propertyChange.removePropertyChangeListener(listener);

80. }

82. /**

83. * Definit le nom de la personne

84. * @param name le nom

85. */

86. public void setName(String name) {

87. propertyChange.firePropertyChange("name", this.name, this.name = name);

88. }

90. /**

91. * Definit le prenom de la personne

92. * @param firstname le prenom

93. */

94. public void setFirstname(String firstname) {

95. propertyChange.firePropertyChange("firstname", this.firstname,

96. this.firstname = firstname);

97. }

99. /**

100. * Definit le sexe de la personne

101. * @param male <code>true</code> si la personne est de sexe masculin,

102. * <code>false</code> sinon

103. */

104. public void setMale(boolean male) {

105. propertyChange.firePropertyChange("male", this.male, this.male = male);

106. }

108. /**

109. * Definit l'age de la personne

110. * @param age l'age

111. */

112. public void setAge(int age) {

113. propertyChange.firePropertyChange("age", this.age, this.age = age);

114. }

116. @Override

117. public String toString() {

118. String s = "Nom : " + name + ", Prenom : " + firstname;

119. s += ", Sexe : " + (male ? "Masculin" : "Feminin") + " , Age : " + age;

120. s += "\n";

121. return s;

122. }

124. @Override

125. public boolean equals(Object obj) {

126. if (obj instanceof PersonBean) {

127. PersonBean p = (PersonBean) obj;

128. if (p.getName().equalsIgnoreCase(name) && p.getFirstname()

129. .equalsIgnoreCase(firstname) && p.getAge() == age &&

130. p.isMale() == male) {

131. return true;

132. }

133. }

134. return false;

135. }

137. }

PersonList.java

1. /**

2. * Cette classe comprend une unique methode statique qui permet de creer une

3. * liste d'objets PersonBean.

4. * @author A. Bernard

5. */

6. public class PersonList {

8. public static List<PersonBean> getPersonList() {

9. List<PersonBean> result = new ArrayList<PersonBean>();

10. result.add(new PersonBean("Bernard", "Alain", 24, true));

11. result.add(new PersonBean("Dupont", "Michel", 49, true));

12. result.add(new PersonBean("Jacques", "Jocelyne", 51, false));

13. result.add(new PersonBean("Martin", "Francois", 31, true));

14. return result;

16. }

17. }

Nous allons maintenant afficher notre liste de personnes dans un viewer JFace de type tableau. Créons la vue « FirstView » au sein de notre application de la manière suivante :

FirstView.java

1. /**

2. * Cette vue affiche une liste de personnes et permet de modifier cette liste.

3. * @author A. Bernard

4. */

5. public class FirstView extends ViewPart {

6. public FirstView() {

7. }

9. /**

10. * the table viewer

11. */

12. private TableViewer viewer;

13. /**

14. * the input for the viewer

15. */

16. private WritableList input;

18. @Override

19. public void createPartControl(Composite parent) {

20. parent.setLayout(new GridLayout(2, false));

22. // Construction du tableau

23. viewer = new TableViewer(parent, SWT.SINGLE | SWT.FULL_SELECTION);

24. Table table = viewer.getTable();

25. table.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, true, 2,

26. 1));

28. // Creation des colonnes

29. TableViewerColumn column = new TableViewerColumn(viewer, SWT.NONE);

30. column.getColumn().setWidth(100);

31. column.getColumn().setText("First Name");

32. column = new TableViewerColumn(viewer, SWT.NONE);

33. column.getColumn().setWidth(100);

34. column.getColumn().setText("Last Name");

35. column = new TableViewerColumn(viewer, SWT.NONE);

36. column.getColumn().setWidth(100);

37. column.getColumn().setText("Male ?");

38. column = new TableViewerColumn(viewer, SWT.NONE);

39. column.getColumn().setWidth(100);

40. column.getColumn().setText("Age");

41. viewer.getTable().setHeaderVisible(true);

43. // Creation des donnees d'entree du tableau

44. input = new WritableList(PersonList.getPersonList(), PersonBean.class);

45. // Mise en place du databinding

46. ViewerSupport.bind(viewer,

47. input,

48. BeanProperties.values(new String[] {"firstname", "name",

49. "male", "age"}));

52. // Creation des 4 boutons de test du databinding

53. // Bouton de suppression de la selection

54. Button buttonDelete = new Button(parent, SWT.PUSH);

55. buttonDelete.setLayoutData(new GridData(SWT.LEFT, SWT.FILL, true,

56. false, 1, 1));

57. buttonDelete.setText("Delete selection");

58. buttonDelete.addSelectionListener(new SelectionAdapter() {

59. @Override

60. public void widgetSelected(SelectionEvent e) {

61. if (!viewer.getSelection().isEmpty()) {

62. IStructuredSelection selection = (IStructuredSelection)

63. viewer

64. .getSelection();

65. PersonBean p = (PersonBean) selection.getFirstElement();

66. input.remove(p);

67. }

68. }

69. });

71. // Bouton d'ajout d'un element

72. Button buttonAdd = new Button(parent, SWT.PUSH);

73. buttonAdd.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, true, false,

74. 1, 1));

75. buttonAdd.setText("Add");

76. buttonAdd.addSelectionListener(new SelectionAdapter() {

77. @Override

78. public void widgetSelected(SelectionEvent e) {

79. PersonBean p = new PersonBean();

80. p.setFirstname("Test1");

81. p.setName("Test2");

82. input.add(p);

83. }

84. });

86. // Bouton de reinitialisation de la selection

87. Button buttonReset = new Button(parent, SWT.NONE);

88. buttonReset.setText("Reset selection");

89. buttonReset.addSelectionListener(new SelectionAdapter() {

90. @Override

91. public void widgetSelected(SelectionEvent e) {

92. if (!viewer.getSelection().isEmpty()) {

93. IStructuredSelection selection = (IStructuredSelection)

94. viewer

95. .getSelection();

96. PersonBean p = (PersonBean) selection.getFirstElement();

97. p.setName("Unknown");

98. p.setFirstname("Unknown");

99. p.setAge(0);

100. p.setMale(true);

101. }

102. }

103. });

105. // Bouton d'affichage du modele

106. Button buttonPrint = new Button(parent, SWT.NONE);

107. buttonPrint.setText("Print model");

108. buttonPrint.addSelectionListener(new SelectionAdapter() {

109. @Override

110. public void widgetSelected(SelectionEvent e) {

111. System.out.println(viewer.getInput());

112. }

113. });

115. }

117. @Override

118. public void setFocus() {

119. viewer.getControl().setFocus();

120. }

121. }

Si l'on observe ce code, on s'aperçoit que le viewer ne possède ni ContentProvider, ni LabelProvider. Il n'y a pas non plus d'appel effectué à la méthode setInput. Au lieu de cela, on utilise un objet de type WritableList que l'on initialise grâce à la ligne :

input = new WritableList(PersonList.getPersonList(),
PersonBean.class);

D'autre part, on lie le contenu du viewer directement à cette liste grâce à l'instruction :

ViewerSupport.bind(viewer,
input,
BeanProperties.values(new String[] {"firstname", "name",
"male", "age"}));

On indique que l'on lie les valeurs « firstname », « name », « male » et « age » aux différentes colonnes du tableau. La classe ViewerSupport permet de simplifier la mise en place du databinding pour les viewers JFace. Il permet d'enregistrer les changements effectués au sein du modèle dans sa globalité, mais aussi ceux effectués sur les éléments individuels. C'est cette classe qui se charge de créer automatiquement le ContentProvider et le LabelProvider. Lançons l'exemple pour constater le fonctionnement du mécanisme :

Application et modèle après réinitialisation et suppression d'un élément

III - Observer les changements plus précisément

La première vue que nous avons créée affiche tous les éléments, mais nous laisse peu de latitude quant à la personnalisation de l'affichage en lui-même : le LabelProvider est générique et on ne peut pas le modifier. Grâce aux éléments que nous allons mettre en œuvre, nous allons pouvoir remédier à ce problème. Créons la vue « SecondView » de la manière suivante :

/**
* Cette vue presente les mecanismes pour observer de maniere plus precise les
* elements affiches dans un viewer JFace.
* @author A. Bernard
*/
public class SecondView extends ViewPart {

/**
* the viewer
*/
private TableViewer viewer;
/**
* the input for the table
*/
private IObservableList input;

@Override
public void createPartControl(Composite parent) {
parent.setLayout(new GridLayout(2, false));
viewer = new TableViewer(parent, SWT.SINGLE | SWT.FULL_SELECTION);
Table table = viewer.getTable();
table.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 2,
1));
table.setHeaderVisible(true);
table.setLinesVisible(true);

// Creation des colonnes
TableViewerColumn column = new TableViewerColumn(viewer, SWT.NONE);
column.getColumn().setWidth(100);
column.getColumn().setText("Name");
column = new TableViewerColumn(viewer, SWT.NONE);
column.getColumn().setWidth(100);
column.getColumn().setText("Details");

// Definition du ContentProvider
ObservableListContentProvider contentProvider = new
ObservableListContentProvider();
viewer.setContentProvider(contentProvider);

IObservableSet knownElements = contentProvider.getKnownElements();

// Creation des maps contenant les differents attributs de la classe
PersonBean
final IObservableMap firstNames =
BeanProperties.value(PersonBean.class,
"firstname").observeDetail(knownElements);
final IObservableMap names = BeanProperties.value(PersonBean.class,
"name").observeDetail(knownElements);
final IObservableMap ages = BeanProperties.value(PersonBean.class,
"age").observeDetail(knownElements);
final IObservableMap genders = BeanProperties.value(PersonBean.class,
"male").observeDetail(knownElements);
IObservableMap[] labelMaps = { firstNames, names, ages, genders };

// Creation du LabelProvider pour le tableau
ILabelProvider labelProvider = new
ObservableMapLabelProvider(labelMaps) {
@Override
public String getColumnText(Object element, int columnIndex) {
if (columnIndex == 0) {
return names.get(element) + " (" + firstNames.get(element)
+ ")";
} else {
return genders.get(element) + " (" + ages.get(element) +
")";
}
}
};

viewer.setLabelProvider(labelProvider);

// Initialisation des donnees
List<PersonBean> persons = PersonList.getPersonList();
input = Properties.selfList(PersonBean.class).observe(persons);
viewer.setInput(input);

// Creation des 4 boutons de test du databinding
// Bouton de suppression de la selection
Button buttonDelete = new Button(parent, SWT.PUSH);
buttonDelete.setLayoutData(new GridData(SWT.LEFT, SWT.FILL, true,
false, 1, 1));
buttonDelete.setText("Delete selection");
buttonDelete.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
if (!viewer.getSelection().isEmpty()) {
IStructuredSelection selection = (IStructuredSelection)
viewer
.getSelection();
PersonBean p = (PersonBean) selection.getFirstElement();
input.remove(p);
}
}
});

// Bouton d'ajout d'un element
Button buttonAdd = new Button(parent, SWT.PUSH);
buttonAdd.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, true, false,
1, 1));
buttonAdd.setText("Add");
buttonAdd.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
PersonBean p = new PersonBean();
p.setFirstname("Test1");
p.setName("Test2");
input.add(p);
}
});

// Bouton de reinitialisation de la selection
Button buttonReset = new Button(parent, SWT.NONE);
buttonReset.setText("Reset selection");
buttonReset.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
if (!viewer.getSelection().isEmpty()) {
IStructuredSelection selection = (IStructuredSelection)
viewer
.getSelection();
PersonBean p = (PersonBean) selection.getFirstElement();
p.setName("Unknown");
p.setFirstname("Unknown");
p.setAge(0);
p.setMale(true);
}
}
});

// Bouton d'affichage du modele
Button buttonPrint = new Button(parent, SWT.NONE);
buttonPrint.setText("Print model");
buttonPrint.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
System.out.println(viewer.getInput());
}
});
}

@Override
public void setFocus() {
//
}

}

Nous pouvons visualiser le résultat et vérifier le bon fonctionnement de l'application :

Utilisation des ObservableMap

Il nous faut cette fois-ci définir le ContentProvider. La classe ObservableListContentProvider nécessite que les données d'entrée du viewer implémentent l'interface IObservableList. Notons qu'une liste classique peut être transformée en IObservableList grâce à la classe Properties, comme le montre l'exemple ci-dessous :

List<PersonBean> persons = PersonList.getPersonList();
IObservableList input =
Properties.selfList(PersonBean.class).observe(persons);

Afin d'observer les modifications dans les listes d'éléments, on utilise des objets IObservableMap. Ces objets permettent d'observer les modifications d'un attribut au sein des différents éléments d'une collection de type IObservableSet. Cette collection est obtenue directement à partir du ContentProvider grâce à l'instruction :

IObservableSet knownElements = contentProvider.getKnownElements();

On peut ensuite créer un objet IObservableMap pour chaque attribut du modèle pour lequel on souhaite observer les changements. Enfin, on peut créer un LabelProvider pour notre viewer, en passant en paramètre un tableau d'IObservableMap :

// Creation du LabelProvider pour le tableau
ILabelProvider labelProvider = new
ObservableMapLabelProvider(labelMaps) {
@Override
public String getColumnText(Object element, int columnIndex) {
if (columnIndex == 0) {
return names.get(element) + " (" + firstNames.get(element)
+ ")";
} else {
return genders.get(element) + " (" + ages.get(element) +
")";
}
}
};

Comme nous avons pu le constater lorsque nous avons exécuté notre exemple, les données sont bien mises à jour en temps réel.

IV - Utilisation de WindowBuilder

Une fois encore, WindowBuilder nous permet de réaliser nos bindings facilement. Créons la vue « ThirdView » en l'initialisant de la manière suivante :

public class ThirdView extends ViewPart {

/**
* the viewer
*/
private TableViewer viewer;
/**
* the input for the table
*/
private IObservableList input;

public ThirdView() {
//
}

@Override
public void createPartControl(Composite parent) {
parent.setLayout(new GridLayout(2, false));
viewer = new TableViewer(parent, SWT.SINGLE | SWT.FULL_SELECTION);
Table table = viewer.getTable();
table.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 2,
1));
table.setHeaderVisible(true);
table.setLinesVisible(true);

// Creation des colonnes
TableViewerColumn column = new TableViewerColumn(viewer, SWT.NONE);
column.getColumn().setWidth(100);
column.getColumn().setText("Name");
column = new TableViewerColumn(viewer, SWT.NONE);
column.getColumn().setWidth(100);
column.getColumn().setText("Details");

// Initialisation des donnees
List<PersonBean> persons = PersonList.getPersonList();
input = Properties.selfList(PersonBean.class).observe(persons);

// Creation des 4 boutons de test du databinding
// Bouton de suppression de la selection
Button buttonDelete = new Button(parent, SWT.PUSH);
buttonDelete.setLayoutData(new GridData(SWT.LEFT, SWT.FILL, true,
false, 1, 1));
buttonDelete.setText("Delete selection");
buttonDelete.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
if (!viewer.getSelection().isEmpty()) {
IStructuredSelection selection = (IStructuredSelection)
viewer
.getSelection();
PersonBean p = (PersonBean) selection.getFirstElement();
input.remove(p);
}
}
});

// Bouton d'ajout d'un element
Button buttonAdd = new Button(parent, SWT.PUSH);
buttonAdd.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, true, false,
1, 1));
buttonAdd.setText("Add");
buttonAdd.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
PersonBean p = new PersonBean();
p.setFirstname("Test1");
p.setName("Test2");
input.add(p);
}
});

// Bouton de reinitialisation de la selection
Button buttonReset = new Button(parent, SWT.NONE);
buttonReset.setText("Reset selection");
buttonReset.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
if (!viewer.getSelection().isEmpty()) {
IStructuredSelection selection = (IStructuredSelection)
viewer
.getSelection();
PersonBean p = (PersonBean) selection.getFirstElement();
p.setName("Unknown");
p.setFirstname("Unknown");
p.setAge(0);
p.setMale(true);
}
}
});

// Bouton d'affichage du modele
Button buttonPrint = new Button(parent, SWT.NONE);
buttonPrint.setText("Print model");
buttonPrint.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
System.out.println(viewer.getInput());
}
});

}

@Override
public void setFocus() {
//

}
}

Nous avons initialisé nos données d'entrée au travers de la variable « input », mais nous n'avons pas encore défini le ContentProvider ni le LabelProvider. Ouvrons la vue avec WindowBuilder, et rendons-nous dans l'onglet « Bindings ». Dans la partie gauche, sélectionnons l'objet graphique « viewer » et sa caractéristique « input » (carrés rouges). Dans la partie droite, sélectionnons l'objet « input » et la caractéristique « Object as IObservableList » (carrés oranges). Enfin, créons le binding en cliquant sur le bouton idoine (carré vert) :

Première étape avec WindowBuilder

Une fenêtre s'ouvre et nous permet de sélectionner les propriétés à afficher dans les colonnes de notre viewer. Elle nous permet aussi de définir des éditeurs pour les cellules :

Deuxième étape avec WindowBuilder

Le binding est créé. On retrouve dans le code source généré les éléments que nous avons mentionnés dans cet article :

protected DataBindingContext initDataBindings() {
DataBindingContext bindingContext = new DataBindingContext();
//
ObservableListContentProvider listContentProvider = new
ObservableListContentProvider();
viewer.setContentProvider(listContentProvider);
//
IObservableMap[] observeMaps =
BeansObservables.observeMaps(listContentProvider.getKnownElements(),
PersonBean.class, new String[]{"firstname", "age"});
viewer.setLabelProvider(new ObservableMapLabelProvider(observeMaps));
//
viewer.setInput(input);
//
return bindingContext;
}

En lançant l'application, nous pouvons constater le fonctionnement correct du databinding :

Résultat avec WindowBuilder

V - Liens utiles

JFace Databinding sur le site de Lars Vogel.

VI - Conclusion

Dans ce second article, nous avons vu comment mettre en œuvre le framework JFace Databinding sur des composants JFace. Cela nous permet d'observer les changements effectués sur une collection d'objets, soit de manière très simple et rapide, soit de manière plus précise.

VII - Remerciements

Je tiens à remercier les membres de la communauté Java qui m'ont aidé et conseillé pour la rédaction de cet article, ainsi que Fabien (f-leb) et Maxime (_Max_) pour leur relecture orthographique attentive.