JFace Databinding sous Eclipse avec SWT

Alain BERNARD

Cet article se propose de présenter le databinding sous Eclipse en utilisant des composants SWT. Le databinding consiste à lier les données du modèle aux données affichées par l'interface, en s'affranchissant du mécanisme de listeners sur les éléments de l'interface. Nous verrons dans cet article les différents moyens de mettre en place ce databinding, sur des composants SWT. Le databinding avec des composants JFace sera abordé dans un autre article. 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 sous Eclipse avec SWT

- Auteur : Alain BERNARD

- Parution : 12 avril 2012

- Mise à jour : 7 juillet 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. Notre modèle

- III. Utilisation d'un listener

- IV. Binding avec un POJO

- V. Binding avec un objet Bean

- VI. Valider les données

- VII. Databinding avec WindowBuilder

- VIII. Quelques remarques

- IX. Liens utiles

- X. Conclusion

- XI. 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 un premier temps, nous mettrons en œuvre une méthode "classique" utilisant des listeners sur les champs de l'UI. Puis nous verrons comment lier les données de l'interface au modèle, dans le sens UI -> modèle dans un premier temps, puis dans les deux sens dans un deuxième temps.
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 - Notre modèle

Notre modèle est constitué d'un simple POJO, la classe Person.java. Cette classe contient les champs "Nom", "Prénom", "Sexe" et "Âge". Le problème que nous étudions dans l'article est de mettre à jour directement cette classe lorsque l'utilisateur interagit avec l'IHM, en reliant les champs comme sur l'image ci-dessous :

Liaison des données

Notre POJO est défini comme suit :

PersonPojo.java

1. package com.abernard.swtdatabinding.model;

3. /**

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

5. * presentees dans nos interfaces.

6. *

7. * @author A. Bernard

8. */

9. public class PersonPojo {

11. /**

12. * nom de la personne

13. */

14. private String name;

15. /**

16. * prenom de la personne

17. */

18. private String firstname;

19. /**

20. * sexe de la personne

21. */

22. private boolean male;

23. /**

24. * age de la personne

25. */

26. private int age;

28. /**

29. * Constructeur

30. */

31. public PersonPojo() {

32. this.name = "";

33. this.firstname = "";

34. this.male = true;

35. this.age = 0;

36. }

38. /**

39. * Constructeur avec arguments

40. */

41. public PersonPojo(String n, String f, int a, boolean m) {

42. this.name = n;

43. this.firstname = f;

44. this.male = m;

45. this.age = a;

46. }

48. // + GETTERS AND SETTERS

49. }

III - Utilisation d'un listener

La méthode qui vient le plus rapidement à l'esprit est d'utiliser un listener. Par exemple, créons la vue "Listener View" définie comme suit :

ListenerView.java

1. package com.abernard.swtdatabinding;

3. import org.eclipse.swt.SWT;

5. /**

6. * Cette vue lie le champ "Nom" au modele en utilisant un mecanisme de listener

7. *  classique.

8. * @author A. Bernard

9. */

10. public class ListenerView extends ViewPart {

12. /**

13. * champ texte pour le nom

14. */

15. private Text textName;

16. /**

17. * champ texte pour le prenom

18. */

19. private Text textFirstname;

20. /**

21. * bouton radio pour sexe masculin

22. */

23. private Button radioMale;

24. /**

25. * bouton radio pour sexe feminin

26. */

27. private Button radioFemale;

28. /**

29. * selecteur d'age

30. */

31. private Spinner spinnerAge;

32. /**

33. * bouton d'affichage du modele

34. */

35. private Button buttonPrint;

37. /**

38. * notre modele

39. */

40. private PersonPojo person;

44. @Override

45. public void createPartControl(Composite parent) {

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

48. //Definition du modele objet

49. person = new PersonPojo("Bernard", "Alain", 23, true);

51. //Champ nom

52. Label labelName = new Label(parent, SWT.NONE);

53. labelName.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false,

54. false, 1, 1));

55. labelName.setText("Name :");

57. textName = new Text(parent, SWT.BORDER);

58. textName.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false,

59. 1, 1));

60. textName.setText(person.getName());

61. textName.addKeyListener(new KeyAdapter() {

62. @Override

63. public void keyReleased(KeyEvent e) {

64. String newValue = textName.getText();

65. person.setName(newValue);

66. }

67. });

69. //Champ prenom

70. Label labelFirstname = new Label(parent, SWT.NONE);

71. labelFirstname.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false,

72. false, 1, 1));

73. labelFirstname.setText("Firstname :");

75. textFirstname = new Text(parent, SWT.BORDER);

76. textFirstname.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true,

77. false, 1, 1));

78. textFirstname.setText(person.getFirstname());

80. //Boutons radio pour le choix du sexe

81. Label lblGender = new Label(parent, SWT.NONE);

82. lblGender.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false,

83. false, 1, 1));

84. lblGender.setText("Gender :");

86. radioMale = new Button(parent, SWT.RADIO);

87. radioMale.setText("Male");

88. radioMale.setSelection(person.isMale());

90. new Label(parent, SWT.NONE);

91. radioFemale = new Button(parent, SWT.RADIO);

92. radioFemale.setText("Female");

93. radioFemale.setSelection(!person.isMale());

95. //Champ d'age

96. Label labelAge = new Label(parent, SWT.NONE);

97. labelAge.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false,

98. false, 1, 1));

99. labelAge.setText("Age :");

101. spinnerAge = new Spinner(parent, SWT.BORDER);

102. spinnerAge.setSelection(person.getAge());

104. //bouton d'impression du modele sur console

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

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

107. buttonPrint.addSelectionListener(new SelectionAdapter() {

108. @Override

109. public void widgetSelected(SelectionEvent e) {

110. System.out.println(person.toString());

111. }

112. });

113. new Label(parent, SWT.NONE);

114. }

116. @Override

117. public void setFocus() {

118. //

119. }

120. }

Cette vue crée une instance de notre modèle, et initialise les éléments graphiques avec les valeurs du modèle. Nous avons ajouté au champ "Name" un listener qui permet de modifier le modèle en fonction de la frappe de l'utilisateur. Le bouton d'affichage du modèle nous permet de constater que le modèle est effectivement modifié lors de la frappe :

Binding avec listener

Cette manière de procéder est simple mais peut vite devenir lourde si les éléments à lier sont nombreux. Le framework JFace Databinding va nous permettre d'accomplir ces actions plus simplement et d'enrichir notre interface avec des outils de validation des données saisies.

IV - Binding avec un POJO

Dans cette première partie, nous ne modifions pas notre classe de modèle. Créons la vue "PojoBindingView" qui possède le code suivant :

PojoBindingView.java

1. /**

2. * Cette classe presente le mecanisme de databinding avec un POJO.

3. * @author A. Bernard

4. */

5. public class PojoBindingView extends ViewPart {

7. /**

8. * champ texte pour le nom

9. */

10. private Text textName;

11. /**

12. * champ texte pour le prenom

13. */

14. private Text textFirstname;

15. /**

16. * bouton radio pour sexe masculin

17. */

18. private Button radioMale;

19. /**

20. * bouton radio pour sexe feminin

21. */

22. private Button radioFemale;

23. /**

24. * selecteur d'age

25. */

26. private Spinner spinnerAge;

27. /**

28. * bouton d'affichage du modele

29. */

30. private Button buttonPrint;

31. /**

32. * bouton de modification du modele

33. */

34. private Button buttonModify;

36. /**

37. * notre modele

38. */

39. private PersonPojo person;

41. @Override

42. public void createPartControl(Composite parent) {

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

45. // Definition du modele objet

46. person = new PersonPojo("Bernard", "Alain", 23, true);

48. // Champ nom

49. Label labelName = new Label(parent, SWT.NONE);

50. labelName.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false,

51. false, 1, 1));

52. labelName.setText("Name :");

54. textName = new Text(parent, SWT.BORDER);

55. textName.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false,

56. 1, 1));

57. textName.setText(person.getName());

59. // Champ prenom

60. Label labelFirstname = new Label(parent, SWT.NONE);

61. labelFirstname.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false,

62. false, 1, 1));

63. labelFirstname.setText("Firstname :");

65. textFirstname = new Text(parent, SWT.BORDER);

66. textFirstname.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true,

67. false, 1, 1));

68. textFirstname.setText(person.getFirstname());

70. // Boutons radio pour le choix du sexe

71. Label lblGender = new Label(parent, SWT.NONE);

72. lblGender.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false,

73. false, 1, 1));

74. lblGender.setText("Gender :");

76. radioMale = new Button(parent, SWT.RADIO);

77. radioMale.setText("Male");

78. radioMale.setSelection(person.isMale());

80. new Label(parent, SWT.NONE);

81. radioFemale = new Button(parent, SWT.RADIO);

82. radioFemale.setText("Female");

83. radioFemale.setSelection(!person.isMale());

85. // Champ d'age

86. Label labelAge = new Label(parent, SWT.NONE);

87. labelAge.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false,

88. false, 1, 1));

89. labelAge.setText("Age :");

91. spinnerAge = new Spinner(parent, SWT.BORDER);

92. spinnerAge.setSelection(person.getAge());

94. // Bouton d'impression du modele sur console

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

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

97. buttonPrint.addSelectionListener(new SelectionAdapter() {

98. @Override

99. public void widgetSelected(SelectionEvent e) {

100. System.out.println(person.toString());

101. }

102. });

104. // Bouton de modification du modele

105. buttonModify = new Button(parent, SWT.NONE);

106. buttonModify.setText("Modify model");

107. buttonModify.addSelectionListener(new SelectionAdapter() {

108. @Override

109. public void widgetSelected(SelectionEvent e) {

110. person.setName("Dupont");

111. person.setFirstname("Mireille");

112. person.setAge(32);

113. person.setMale(false);

114. }

115. });

117. // Realisation du databinding

118. bindValues();

119. }

121. /**

122. * Cette methode realise le binding entre le POJO de notre modele et les

123. * elements de l'interface.

124. */

125. private void bindValues() {

126. DataBindingContext context = new DataBindingContext();

128. // Liaison du champ "Name"

129. IObservableValue modelValue = PojoProperties.value("name",

130. PersonPojo.class).observe(person);

131. IObservableValue widgetValue = WidgetProperties.text(SWT.Modify)

132. .observe(textName);

133. context.bindValue(modelValue, widgetValue);

135. // Liaison du champ "Firstname"

136. modelValue = PojoProperties.value("firstname", PersonPojo.class)

137. .observe(person);

138. widgetValue = WidgetProperties.text(SWT.Modify).observe(textFirstname);

139. context.bindValue(modelValue, widgetValue);

141. // Liaison du champ "Gender"

142. modelValue = PojoProperties.value("male", PersonPojo.class)

143. .observe(person);

144. widgetValue = WidgetProperties.selection().observe(radioMale);

145. context.bindValue(modelValue, widgetValue);

147. // Liaison du champ "Age"

148. modelValue = PojoProperties.value("age", PersonPojo.class)

149. .observe(person);

150. widgetValue = WidgetProperties.selection().observe(spinnerAge);

151. context.bindValue(modelValue, widgetValue);

155. }

157. @Override

158. public void setFocus() {

159. //

161. }

163. }

JFace Databinding fournit un moteur de databinding générique indépendant de SWT où l'on peut lier ce que l'on souhaite (POJO avec UI SWT, XML avec SWT, etc.). JFace Databinding est découpé en plusieurs plugins (qui marchent dans un contexte OSGi ou Java "main" classique) dont :

- org.eclise.core.databinding : le moteur générique famework ;

- org.eclipse.core.databinding.beans : implémentation du databinding pour lier des classes Java POJOs ou Beans ;

- org.eclipse.core.databinding.property ;

- org.eclipse.jface.databinding : implémentation du databinding pour lier des widgets graphiques (SWT/JFace).

Ajoutons donc à la liste des dépendances de notre plugin les bundles précités. Lançons ensuite l'application pour observer le résultat. Au lancement les données initiales du modèle sont correctement affichées, chose que nous pouvons vérifier en appuyant sur le bouton "Print model" :

État initial

Si nous modifions des éléments de l'interface, les changements sont correctement répercutés dans le modèle :

Changement via l'IHM

Par contre, si nous appuyons sur le bouton "Modify model", l'interface ne reflète pas les changements du modèle :

Changement via modèle

Maintenant que ces constatations sont faites, étudions le code écrit.

On peut penser au premier abord que le code écrit est bien plus complexe que celui de notre premier exemple. Cependant cette manière de procéder nous permettra par la suite d'ajouter facilement beaucoup d'interactions, ce que nous verrons dans la partie suivante.
Nous avons commencé par créer une nouvelle instance d'un objet DataBindingContext. Cet objet permet de lier deux éléments de type IObservableValue, grâce à sa méthode "bindValue".

DataBindingContext context = new DataBindingContext();

Ensuite, nous créons deux éléments de type IObservableValue : un pour le modèle, et un pour l'interface. L'interface IObservableValue permet d'observer les propriétés de certains éléments. Afin de rendre la tâche plus aisée, nous disposons de trois classes :

- WidgetProperties : permet d'observer les propriétés des éléments graphiques. Dans notre exemple, nous observons la propriété "text" des champs texte, ou la propriété "selection" d'un bouton radio.

IObservableValue widgetValue = WidgetProperties.text(SWT.Modify)
.observe(textName);

- PojoProperties : permet d'observer les propriétés d'un POJO. La liaison se fait en donnant la classe du modèle ainsi que la clé de liaison, sous forme d'une chaîne de caractères. Pour que cela se fasse correctement, il est impératif de respecter le nommage des attributs de la classe pour ces clés. En effet, le framework reconstruit le nom des getters/setters grâce à cette clé. Par exemple, pour l'attribut "name", il est nécessaire que la classe contienne les méthodes "getName" et "setName". Pour un booléen, on remplacera le préfixe "get" par le préfixe "is" (isMale). Si ces conventions de nommage ne sont pas respectées, le framework lèvera une exception.

IObservableValue modelValue = PojoProperties.value("name",
PersonPojo.class).observe(person);

- BeanProperties : permet d'observer les propriétés de Beans. Nous détaillerons leurs spécificités dans la partie suivante.

Une fois que les éléments IObservableValue sont créés, on peut lier chaque couple modèle-interface grâce à la méthode "bindValue" de l'élément DataBindingContext.

context.bindValue(modelValue, widgetValue);

La méthode que nous avons abordée ici nous permet de lier l'interface avec une classe du modèle sans modifier cette classe. Cependant, la liaison ne se fait que dans le sens UI -> POJO, ce qui limite son intérêt. Nous verrons dans la partie suivante comment effectuer le binding dans les deux sens.

V - Binding avec un objet Bean

Nous avons vu qu'avec WidgetProperties (UI) et PojoObservables (POJO) il est possible de binder un modèle POJO avec une UI. Lorsque l'on modifie l'UI le POJO est modifié, mais pas l'inverse. En effet l'UI est observable via des listeners que l'on peut ajouter alors que notre Pojo n'est pas observable. Autrement dit lorsque la propriété "text" d'un Text SWT change, il est possible de l'observer via un listener SWT. Notre POJO par contre ne déclenche aucun évènement lorsqu'une méthode set* est appelée. Pour gérer le sens Pojo -> UI, il faut que notre Pojo déclenche des évènements lors des appels des méthodes set*. Pour cela nous allons transformer notre Pojo en Beans en utilisant PropertyChangeListener. (Notons que si l'on utilise EMF, il n'y a pas besoin de coder ces listeners car ils sont générés.) Pour commencer, il faut modifier notre modèle, afin de créer un objet "Bean". Créons la classe "PersonBean" comme suit :

PersonBean.java

1. /**

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

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

4. * PropertyChangeSupport pour repercuter les modifications du modele sur

5. * l'interface graphique.

6. *

7. * @author A. Bernard

8. */

9. public class PersonBean {

11. /**

12. * nom de la personne

13. */

14. private String name;

15. /**

16. * prenom de la personne

17. */

18. private String firstname;

19. /**

20. * sexe de la personne

21. */

22. private boolean male;

23. /**

24. * age de la personne

25. */

26. private int age;

28. /**

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

30. a

31. * ete effectuee dans le modele

32. */

33. private PropertyChangeSupport propertyChange;

35. /**

36. * Constructeur

37. */

38. public PersonBean() {

39. this.name = "";

40. this.firstname = "";

41. this.male = true;

42. this.age = 0;

43. }

45. /**

46. * Constructeur avec arguments

47. */

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

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

50. this.setName(n);

51. this.setFirstname(f);

52. this.setMale(m);

53. this.setAge(a);

55. }

57. /**

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

59. * classe

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

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

62. */

63. public void addPropertyChangeListener(String propertyName,

64. PropertyChangeListener listener) {

65. propertyChange.addPropertyChangeListener(propertyName, listener);

66. }

68. /**

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

70. * classe

71. * @param listener l'element a supprimer

72. */

73. public void removePropertyChangeListener(PropertyChangeListener listener) {

74. propertyChange.removePropertyChangeListener(listener);

75. }

77. /**

78. * Donne le nom de la personne

79. * @return le nom

80. */

81. public String getName() {

82. return name;

83. }

85. /**

86. * Definit le nom de la personne

87. * @param name le nom

88. */

89. public void setName(String name) {

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

91. }

93. /**

94. * Donne le prenom de la personne

95. * @return le prenom

96. */

97. public String getFirstname() {

98. return firstname;

99. }

101. /**

102. * Definit le prenom de la personne

103. * @param firstname le prenom

104. */

105. public void setFirstname(String firstname) {

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

107. this.firstname = firstname);

108. }

110. /**

111. * Donne le sexe de la personne

112. * @return <code>true</code> si la personne est de sexe masculin,

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

114. */

115. public boolean isMale() {

116. return male;

117. }

119. /**

120. * Definit le sexe de la personne

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

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

123. */

124. public void setMale(boolean male) {

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

126. }

128. /**

129. * Donne l'age de la personne

130. * @return l'age

131. */

132. public int getAge() {

133. return age;

134. }

136. /**

137. * Definit l'age de la personne

138. * @param age l'age

139. */

140. public void setAge(int age) {

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

142. }

144. @Override

145. public String toString() {

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

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

148. s += "\n";

149. return s;

150. }

152. @Override

153. public boolean equals(Object obj) {

154. if (obj instanceof PersonBean) {

155. PersonBean p = (PersonBean) obj;

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

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

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

159. return true;

160. }

161. }

162. return false;

163. }

165. }

Créons ensuite la vue "BeanBindingView" :

BeanBindingView.java

1. /**

2. * Cette vue met en oeuvre le mecanisme de databinding avec notre objet

3. * PersonBean.

4. * @author A. Bernard

5. */

6. public class BeanBindingView extends ViewPart  {

8. /**

9. * champ texte pour le nom

10. */

11. private Text textName;

12. /**

13. * champ texte pour le prenom

14. */

15. private Text textFirstname;

16. /**

17. * bouton radio pour sexe masculin

18. */

19. private Button radioMale;

20. /**

21. * bouton radio pour sexe feminin

22. */

23. private Button radioFemale;

24. /**

25. * selecteur d'age

26. */

27. private Spinner spinnerAge;

28. /**

29. * bouton d'affichage du modele

30. */

31. private Button buttonPrint;

32. /**

33. * bouton de modification du modele

34. */

35. private Button buttonModify;

37. /**

38. * notre modele

39. */

40. private PersonBean person;

42. @Override

43. public void createPartControl(Composite parent) {

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

46. // Definition du modele objet

47. person = new PersonBean("Bernard", "Alain", 23, true);

49. // Champ nom

50. Label labelName = new Label(parent, SWT.NONE);

51. labelName.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false,

52. false, 1, 1));

53. labelName.setText("Name :");

55. textName = new Text(parent, SWT.BORDER);

56. textName.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false,

57. 1, 1));

58. textName.setText(person.getName());

60. // Champ prenom

61. Label labelFirstname = new Label(parent, SWT.NONE);

62. labelFirstname.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false,

63. false, 1, 1));

64. labelFirstname.setText("Firstname :");

66. textFirstname = new Text(parent, SWT.BORDER);

67. textFirstname.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true,

68. false, 1, 1));

69. textFirstname.setText(person.getFirstname());

71. // Boutons radio pour le choix du sexe

72. Label lblGender = new Label(parent, SWT.NONE);

73. lblGender.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false,

74. false, 1, 1));

75. lblGender.setText("Gender :");

77. radioMale = new Button(parent, SWT.RADIO);

78. radioMale.setText("Male");

79. radioMale.setSelection(person.isMale());

81. new Label(parent, SWT.NONE);

82. radioFemale = new Button(parent, SWT.RADIO);

83. radioFemale.setText("Female");

84. radioFemale.setSelection(!person.isMale());

86. // Champ d'age

87. Label labelAge = new Label(parent, SWT.NONE);

88. labelAge.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false,

89. false, 1, 1));

90. labelAge.setText("Age :");

92. spinnerAge = new Spinner(parent, SWT.BORDER);

93. spinnerAge.setSelection(person.getAge());

95. // Bouton d'impression du modele sur console

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

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

98. buttonPrint.addSelectionListener(new SelectionAdapter() {

99. @Override

100. public void widgetSelected(SelectionEvent e) {

101. System.out.println(person.toString());

102. }

103. });

105. // Bouton de modification du modele

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

107. buttonModify.setText("Modify model");

108. buttonModify.addSelectionListener(new SelectionAdapter() {

109. @Override

110. public void widgetSelected(SelectionEvent e) {

111. person.setName("Dupont");

112. person.setFirstname("Mireille");

113. person.setAge(32);

114. person.setMale(false);

115. }

116. });

118. // Realisation du databinding

119. bindValues();

121. }

123. /**

124. * Cette methode realise le binding entre la classe de notre modele et les

125. * elements de l'interface.

126. */

127. private void bindValues() {

128. DataBindingContext context = new DataBindingContext();

130. // Liaison du champ "Name"

131. IObservableValue modelValue = BeanProperties.value(PersonBean.class,

132. "name").observe(person);

133. IObservableValue widgetValue = WidgetProperties.text(SWT.Modify)

134. .observe(textName);

135. context.bindValue(widgetValue, modelValue);

137. // Liaison du champ "Firstname"

138. modelValue = BeanProperties.value(PersonBean.class, "firstname")

139. .observe(person);

140. widgetValue = WidgetProperties.text(SWT.Modify).observe(textFirstname);

141. context.bindValue(widgetValue, modelValue);

143. // Liaison du champ "Gender"

144. modelValue = BeanProperties.value(PersonBean.class, "male")

145. .observe(person);

146. widgetValue = WidgetProperties.selection().observe(radioMale);

147. context.bindValue(widgetValue, modelValue);

149. // Liaison du champ "Age"

150. modelValue = BeanProperties.value(PersonBean.class, "age")

151. .observe(person);

152. widgetValue = WidgetProperties.selection().observe(spinnerAge);

153. context.bindValue(widgetValue, modelValue);

155. }

157. @Override

158. public void setFocus() {

159. //

160. }

161. }

Observons maintenant le résultat : lorsque l'on modifie l'IHM le modèle est correctement modifié :

Modification via l'interface

De même, lorsque l'on modifie le modèle via notre bouton l'interface est modifiée :

Modification via le modèle

On remarque cependant une chose : le bouton radio "Female" n'est pas sélectionné automatiquement ! Nous reviendrons sur ce problème un peu plus tard.
En attendant, observons ce qui a changé depuis la vue que nous avions créée dans le paragraphe précédent. Côté interface, le seul changement se trouve dans l'utilisation de la classe BeanProperties au lieu de PojoProperties, ce qui permet de tirer bénéfice des spécificités de ces objets.
Côté modèle, notre classe PersonBean implémente deux méthodes supplémentaires : "addPropertyChangeListener" et "removePropertyChangeListener". Ces méthodes permettent aux autres objets d'être avertis des changements du modèle. La méthode "add" prend en entrée la clé à laquelle l'objet souhaite s'abonner. Comme pour les getters/setters, ces méthodes doivent suivre la convention de nommage et donc s'appeler exactement comme cela. D'autre part, la classe possède un objet PropertyChangeSupport utilisé dans les setters : lorsqu'une valeur est modifiée, on fait appel à cet objet pour transmettre aux listeners les changements du modèle, via la méthode "firePropertyChange".
Ce mécanisme implique que n'importe quel objet peut donc s'abonner aux changements du modèle. Mettons cela en œuvre directement en corrigeant notre petit problème d'interface sur les boutons radio. Pour cela, notre vue doit implémenter l'interface IPropertyChangeListener, en définissant la méthode "propertyChange". Afin d'être avertie des changements, notre vue doit s'abonner au modèle. Complétons notre vue comme suit :

BeanBindingView.java

1. public class BeanBindingView extends ViewPart implements PropertyChangeListener

2. {

4. /**

5. * Cette methode realise le binding entre la classe de notre modele et les

6. * elements de l'interface.

7. */

8. private void bindValues() {

10. // ...

12. // Que faire avec le bouton female ?!

13. // Abonnement "manuel" aux changements de la classe Person

14. person.addPropertyChangeListener("male", this);

15. }

17. @Override

18. public void propertyChange(PropertyChangeEvent evt) {

19. // On est notifie des changements de la valeur de l'attribut "male"

20. // On reajuste a chaque coup la selection du champ "female"

21. radioFemale.setSelection(!person.isMale());

22. }

24. @Override

25. public void dispose() {

26. // Ne pas oublier de supprimer le listener a la fermeture de la vue !!

27. person.removePropertyChangeListener(this);

28. }

30. }

Lorsque la méthode "propertyChange" est appelée, nous savons que nous devons mettre à jour la valeur du bouton radio "Female" car avons abonné notre vue spécifiquement aux changements sur la clé "male". Nous pouvons constater l'efficacité du mécanisme :

MaJ du bouton 'Female'

Au cas où nous aurions abonné la vue à d'autres événements, nous aurions dû évidemment gérer ces événements de manière distincte dans la méthode "propertyChange", en appelant par exemple "evt.getPropertyName()". Enfin, il ne faut pas oublier de désabonner la vue aux changements du modèle dans la méthode "dispose" !

VI - Valider les données

Le framework JFace Databinding nous permet de valider en temps réel les données saisies par l'utilisateur et de fournir à celui-ci un feedback direct. Cela évite aussi de mettre à jour le modèle avec des valeurs incohérentes ou autres.
Comme d'habitude, commençons par créer la vue "ValidationView". Elle est très proche de la vue "BeanBindingView" : hormis un label supplémentaire, tous les changements se situent dans la méthode de réalisation des bindings.

ValidationView.java

1. public class ValidationView extends ViewPart implements PropertyChangeListener

2. {

4. // ...

7. @Override

8. public void createPartControl(Composite parent) {

9. // ...

11. Label labelTitle = new Label(parent, SWT.NONE);

12. labelTitle.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false,

13. false, 1, 1));

14. labelTitle.setText("Errors : ");

16. labelErrors = new Label(parent, SWT.NONE);

17. labelErrors.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false,

18. false, 1, 1));

20. // Realisation du databinding

21. bindValues();

23. }

25. /**

26. * Cette methode realise le binding entre la classe de notre modele et les

27. * elements de l'interface. Dans cette vue, on utilise le mecanisme de

28. * validation afin de verifier les donnees entrees par l'utilisateur.

29. */

30. private void bindValues() {

31. DataBindingContext context = new DataBindingContext();

33. // Liaison du champ "Name"

34. IObservableValue modelValue = BeanProperties.value(PersonBean.class,

35. "name").observe(person);

36. IObservableValue widgetValue = WidgetProperties.text(SWT.Modify)

37. .observe(textName);

38. IValidator validator = new IValidator() {

39. @Override

40. public IStatus validate(Object value) {

41. // On considere que le nom entre est correct s'il contient au

42. // moins deux lettres

43. if (textName.getText().length() > 1) {

45. textName.setBackground(Display.getCurrent().getSystemColor(SWT.COLOR_LIST_BACKGROUND));

46. return ValidationStatus.ok();

47. } else {

49. textName.setBackground(Display.getCurrent().getSystemColor(SWT.COLOR_RED));

50. return ValidationStatus.error("Name must be longer than " +

51. "two letters");

52. }

53. }

55. };

56. Binding binding = context.bindValue(widgetValue, modelValue, new

57. UpdateValueStrategy()

58. .setBeforeSetValidator(validator), null);

59. ControlDecorationSupport.create(binding, SWT.TOP | SWT.LEFT);

61. // Liaison du champ "Firstname"

62. modelValue = BeanProperties.value(PersonBean.class, "firstname")

63. .observe(person);

64. widgetValue = WidgetProperties.text(SWT.Modify).observe(textFirstname);

65. validator = new IValidator() {

66. @Override

67. public IStatus validate(Object value) {

68. // On considere que le prenom entre est correct s'il contient

69. // au moins deux lettres

70. if (textFirstname.getText().length() > 1) {

72. textFirstname.setBackground(Display.getCurrent().getSystemColor(SWT.COLOR_LIST_BACKGROUND));

73. return ValidationStatus.ok();

74. } else {

76. textFirstname.setBackground(Display.getCurrent().getSystemColor(SWT.COLOR_RED));

77. return ValidationStatus.error("Firstname must be longer " +

78. "than two letters");

79. }

80. }

82. };

83. binding = context.bindValue(widgetValue, modelValue, new

84. UpdateValueStrategy()

85. .setBeforeSetValidator(validator), null);

86. ControlDecorationSupport.create(binding, SWT.TOP | SWT.LEFT);

88. // On affiche sur le label d'erreur la liste des erreurs possibles de

89. // validation

90. widgetValue = WidgetProperties.text().observe(labelErrors);

91. // This one listenes to all changes

92. context.bindValue(widgetValue,

93. new AggregateValidationStatus(context.getBindings(),

94. AggregateValidationStatus.MAX_SEVERITY));

96. // Liaison du champ "Gender"

97. modelValue = BeanProperties.value(PersonBean.class, "male")

98. .observe(person);

99. widgetValue = WidgetProperties.selection().observe(radioMale);

100. context.bindValue(widgetValue, modelValue);

102. // Liaison du champ "Age"

103. modelValue = BeanProperties.value(PersonBean.class, "age")

104. .observe(person);

105. widgetValue = WidgetProperties.selection().observe(spinnerAge);

106. context.bindValue(widgetValue, modelValue);

108. // Que faire avec le bouton female ?!

109. // Abonnement "manuel" aux changements de la classe Person

110. person.addPropertyChangeListener("male", this);

112. }

114. // ...

116. }

Nous avons ajouté sur chaque champ texte une validation, au travers d'un objet IValidator. Cet objet s'utilise au travers de la méthode "bindValue" qui prend alors en entrée quatre paramètres : les deux IObservableValue et deux objets UpdateValueStrategy (l'un pour la mise à jour UI->Bean et l'autre pour la mise à jour Bean->UI). Ces objets UpdateValueStrategy permettent de spécifier le type de validation à effectuer avant la mise à jour des champs. Pour cela trois méthodes sont disponibles sur ces objets, qui prennent en paramètre l'objet "Validator" à utiliser :

- setAfterConvertValidator : ici, la validation est effectuée juste après que l'élément source est converti dans le type de l'élément cible. En effet, les données peuvent être converties automatiquement depuis le type source vers le type cible, par exemple une chaîne de caractères peut être convertie en entier. Par exemple, à la place de l'élément Spinner pour l'âge, on peut utiliser un champ texte. Le framework est capable de convertir le contenu du champ texte en entier pour appeler la méthode setAge ;

- setAfterGetValidator : la validation est effectuée au début du processus de synchronisation juste après la récupération de la valeur de l'objet source ;

- setBeforeSetValidator : dans ce cas la validation est effectuée juste avant la modification de l'objet cible, à la fin du processus de synchronisation.

Notons que l'on peut aussi définir son propre convertisseur de données si ces données sont plus complexes, grâce à la méthode setConverter de l'objet UpdateValueStrategy.
L'objet IValidator doit redéfinir une seule méthode, "validate", qui retourne soit "ValidationStatus.ok()", soit "ValidationStatus.error(...)". Le texte que l'on indique dans l'erreur peut être affiché dans un élément de type ControlDecoration. Cet élément est une petite icône qui informe l'utilisateur avec le texte de l'erreur lorsque l'on passe la souris dessus, comme le montre cette capture d'écran :

Validation

L'affichage de cet élément est automatiquement géré grâce à l'objet de type Binding retourné par la méthode "bindValue".
Enfin, on peut afficher les diverses erreurs de validation sur un Label dédié grâce à un objet de type AggregationValidationStatus qui récupère tous les bindings créés sur un même objet DataBindingContext et affiche l'erreur la plus sévère.
Ce mécanisme de validation nous permet d'avoir un contrôle très précis sur les données entrées par l'utilisateur et complète la liste des avantages de l'utilisation du framework JFace Databinding.

VII - Databinding avec WindowBuilder

Dans ce paragraphe, nous allons voir comment faire du databinding simplement en utilisant l'outil de conception d'interfaces WindowBuilder. Nous ne nous attarderons pas ici sur l'installation et l'utilisation de WindowBuilder, mais uniquement sur la fonctionnalité de binding.
Tout d'abord, créons l'interface de notre nouvelle vue "WindowBuilderView" dans l'onglet "Design" de l'outil :

Design de l'interface

Une fois l'interface créée nous nous rendons dans l'onglet Bindings :

Onglet Bindings

La zone 1 regroupe les bindings déjà créés. On en voit un déjà en place sur le champ "name" de notre modèle.
Les zones 2 et 3 permettent de sélectionner les deux objets à lier. Lorsqu'on sélectionne un objet dans la zone supérieure, on peut sélectionner une propriété de cet objet dans la partie inférieure. La zone 2 contient les éléments de l'interface. La zone 3 contient les objets de type "Bean".
Regardons ensemble pas à pas comment lier le champ "Firstname" et la propriété "firstName" du modèle. Avant tout, créons un objet StringValidator comme suit :

StringValidator.java

1. public class StringValidator implements IValidator {

3. @Override

4. public IStatus validate(Object value) {

5. if (value instanceof String) {

6. String s = (String) value;

7. if (s.length() > 1) {

8. return ValidationStatus.ok();

9. }

10. }

11. return ValidationStatus.error("String must be longer than two");

12. }

13. }

Sélectionnons ensuite les champs adéquats dans l'onglet "Bindings" et cliquons sur l'un des boutons idoines (encadrés en rouge) pour ouvrir l'assistant de binding :

Étape 1

Remplissons ensuite les champs comme indiqué sur cette prise d'écran :

Étape 2

On peut ensuite valider : le binding est créé !

Étape 3

Cet outil nous permet de définir rapidement les bindings. Par contre, il ne met pas en place le système de ControlDecoration afin d'informer l'utilisateur des divers problèmes !

VIII - Quelques remarques

À l'issue de cet article, nous avons pu constater la puissance du framework JFace Databinding. Cependant, deux points me semblent importants à préciser :

- le framework JFace Databinding est indépendant de la plateforme Eclipse. Il est tout à fait possible d'implémenter son propre binding. Voir à ce sujet la section "liens utiles" ;

- à ce titre, il est possible d'utiliser le framework dans des applications SWT "de base", mais il est alors nécessaire de prendre en compte Realm. L'utilisation du framework au sein d'Eclipse nous affranchit de cette contrainte. Pour un exemple de binding dans une application SWT pure, voir ce snippet.

IX - Liens utiles

JFace Databinding sur le site de Lars Vogel.
Des exemples de bindings autres que via le framework JFace Databinding :

- JFace DOM Databinding : lier un DOM XML avec une UI, un POJO etc. ;

- JFace DOM SSE Databinding : lier un DOM SSE (DOM de l'éditeur XML) avec une UI, POJO, etc. ;

- JFace Rhino Databinding : lier des objets Javascripts (Rhino) avec un POJO, UI, etc. ;

- le projet UFacekit : une implémentation de JFace Databinding pour gérer le databinding avec des widgets Swing (au lieu de SWT).

À voir aussi : Pojo Bindables qui permet d'utiliser BeansObservables avec des POJO qui ne codent pas leur listener PropertyChangeListener. En effet, les listeners sont ajoutés en dynamique au chargement de la classe Pojo, par changement de bytecode à la volée.

X - Conclusion

Au travers de cet article, nous avons vu comment le framework JFace Databinding peut simplifier la liaison des données entre l'interface graphique et le modèle, tout en proposant un ensemble d'outils permettant d'enrichir le processus. Cet article s'est concentré sur le databinding avec des composants graphiques SWT. Dans un prochain article, nous nous intéresserons au databinding sur des composants JFace.
Rappelons tout de même pour finir que, comme mentionné en section V, des solutions de plus haut niveau permettent de simplifier ces développements, telles qu'EMF.

XI - Remerciements

Je tiens à remercier les membres de la communauté Java qui m'ont aidé et conseillé pour la rédaction de cet article, tout particulièrement Mickaël (keulkeul), Marc (Gueritarish) et Angelo (azerr). Je remercie aussi Claude (ClaudeLELOUP) pour sa relecture orthographique attentive.