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 EclipseTutoriels sur Eclipse RCP : création de vues, gestion des dépendances ;
- utilisation de SWTTutoriels sur 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 :
Notre POJO est défini comme suit :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
package
com.abernard.swtdatabinding.model;
/**
* Cette classe definit une personne dont les informations peuvent etre
* presentees dans nos interfaces.
*
*
@author
A. Bernard
*/
public
class
PersonPojo {
/**
* nom de la personne
*/
private
String name;
/**
* prenom de la personne
*/
private
String firstname;
/**
* sexe de la personne
*/
private
boolean
male;
/**
* age de la personne
*/
private
int
age;
/**
* Constructeur
*/
public
PersonPojo
(
) {
this
.name =
""
;
this
.firstname =
""
;
this
.male =
true
;
this
.age =
0
;
}
/**
* Constructeur avec arguments
*/
public
PersonPojo
(
String n, String f, int
a, boolean
m) {
this
.name =
n;
this
.firstname =
f;
this
.male =
m;
this
.age =
a;
}
// + GETTERS AND SETTERS
}
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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
package
com.abernard.swtdatabinding;
import
org.eclipse.swt.SWT;
/**
* Cette vue lie le champ "Nom" au modele en utilisant un mecanisme de listener
* classique.
*
@author
A. Bernard
*/
public
class
ListenerView extends
ViewPart {
/**
* champ texte pour le nom
*/
private
Text textName;
/**
* champ texte pour le prenom
*/
private
Text textFirstname;
/**
* bouton radio pour sexe masculin
*/
private
Button radioMale;
/**
* bouton radio pour sexe feminin
*/
private
Button radioFemale;
/**
* selecteur d'age
*/
private
Spinner spinnerAge;
/**
* bouton d'affichage du modele
*/
private
Button buttonPrint;
/**
* notre modele
*/
private
PersonPojo person;
@Override
public
void
createPartControl
(
Composite parent) {
parent.setLayout
(
new
GridLayout
(
2
, false
));
//Definition du modele objet
person =
new
PersonPojo
(
"Bernard"
, "Alain"
, 23
, true
);
//Champ nom
Label labelName =
new
Label
(
parent, SWT.NONE);
labelName.setLayoutData
(
new
GridData
(
SWT.RIGHT, SWT.CENTER, false
,
false
, 1
, 1
));
labelName.setText
(
"Name :"
);
textName =
new
Text
(
parent, SWT.BORDER);
textName.setLayoutData
(
new
GridData
(
SWT.FILL, SWT.CENTER, true
, false
,
1
, 1
));
textName.setText
(
person.getName
(
));
textName.addKeyListener
(
new
KeyAdapter
(
) {
@Override
public
void
keyReleased
(
KeyEvent e) {
String newValue =
textName.getText
(
);
person.setName
(
newValue);
}
}
);
//Champ prenom
Label labelFirstname =
new
Label
(
parent, SWT.NONE);
labelFirstname.setLayoutData
(
new
GridData
(
SWT.RIGHT, SWT.CENTER, false
,
false
, 1
, 1
));
labelFirstname.setText
(
"Firstname :"
);
textFirstname =
new
Text
(
parent, SWT.BORDER);
textFirstname.setLayoutData
(
new
GridData
(
SWT.FILL, SWT.CENTER, true
,
false
, 1
, 1
));
textFirstname.setText
(
person.getFirstname
(
));
//Boutons radio pour le choix du sexe
Label lblGender =
new
Label
(
parent, SWT.NONE);
lblGender.setLayoutData
(
new
GridData
(
SWT.RIGHT, SWT.CENTER, false
,
false
, 1
, 1
));
lblGender.setText
(
"Gender :"
);
radioMale =
new
Button
(
parent, SWT.RADIO);
radioMale.setText
(
"Male"
);
radioMale.setSelection
(
person.isMale
(
));
new
Label
(
parent, SWT.NONE);
radioFemale =
new
Button
(
parent, SWT.RADIO);
radioFemale.setText
(
"Female"
);
radioFemale.setSelection
(!
person.isMale
(
));
//Champ d'age
Label labelAge =
new
Label
(
parent, SWT.NONE);
labelAge.setLayoutData
(
new
GridData
(
SWT.RIGHT, SWT.CENTER, false
,
false
, 1
, 1
));
labelAge.setText
(
"Age :"
);
spinnerAge =
new
Spinner
(
parent, SWT.BORDER);
spinnerAge.setSelection
(
person.getAge
(
));
//bouton d'impression du modele sur console
buttonPrint =
new
Button
(
parent, SWT.NONE);
buttonPrint.setText
(
"Print model"
);
buttonPrint.addSelectionListener
(
new
SelectionAdapter
(
) {
@Override
public
void
widgetSelected
(
SelectionEvent e) {
System.out.println
(
person.toString
(
));
}
}
);
new
Label
(
parent, SWT.NONE);
}
@Override
public
void
setFocus
(
) {
//
}
}
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 :
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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
161.
162.
163.
/**
* Cette classe presente le mecanisme de databinding avec un POJO.
*
@author
A. Bernard
*/
public
class
PojoBindingView extends
ViewPart {
/**
* champ texte pour le nom
*/
private
Text textName;
/**
* champ texte pour le prenom
*/
private
Text textFirstname;
/**
* bouton radio pour sexe masculin
*/
private
Button radioMale;
/**
* bouton radio pour sexe feminin
*/
private
Button radioFemale;
/**
* selecteur d'age
*/
private
Spinner spinnerAge;
/**
* bouton d'affichage du modele
*/
private
Button buttonPrint;
/**
* bouton de modification du modele
*/
private
Button buttonModify;
/**
* notre modele
*/
private
PersonPojo person;
@Override
public
void
createPartControl
(
Composite parent) {
parent.setLayout
(
new
GridLayout
(
2
, false
));
// Definition du modele objet
person =
new
PersonPojo
(
"Bernard"
, "Alain"
, 23
, true
);
// Champ nom
Label labelName =
new
Label
(
parent, SWT.NONE);
labelName.setLayoutData
(
new
GridData
(
SWT.RIGHT, SWT.CENTER, false
,
false
, 1
, 1
));
labelName.setText
(
"Name :"
);
textName =
new
Text
(
parent, SWT.BORDER);
textName.setLayoutData
(
new
GridData
(
SWT.FILL, SWT.CENTER, true
, false
,
1
, 1
));
textName.setText
(
person.getName
(
));
// Champ prenom
Label labelFirstname =
new
Label
(
parent, SWT.NONE);
labelFirstname.setLayoutData
(
new
GridData
(
SWT.RIGHT, SWT.CENTER, false
,
false
, 1
, 1
));
labelFirstname.setText
(
"Firstname :"
);
textFirstname =
new
Text
(
parent, SWT.BORDER);
textFirstname.setLayoutData
(
new
GridData
(
SWT.FILL, SWT.CENTER, true
,
false
, 1
, 1
));
textFirstname.setText
(
person.getFirstname
(
));
// Boutons radio pour le choix du sexe
Label lblGender =
new
Label
(
parent, SWT.NONE);
lblGender.setLayoutData
(
new
GridData
(
SWT.RIGHT, SWT.CENTER, false
,
false
, 1
, 1
));
lblGender.setText
(
"Gender :"
);
radioMale =
new
Button
(
parent, SWT.RADIO);
radioMale.setText
(
"Male"
);
radioMale.setSelection
(
person.isMale
(
));
new
Label
(
parent, SWT.NONE);
radioFemale =
new
Button
(
parent, SWT.RADIO);
radioFemale.setText
(
"Female"
);
radioFemale.setSelection
(!
person.isMale
(
));
// Champ d'age
Label labelAge =
new
Label
(
parent, SWT.NONE);
labelAge.setLayoutData
(
new
GridData
(
SWT.RIGHT, SWT.CENTER, false
,
false
, 1
, 1
));
labelAge.setText
(
"Age :"
);
spinnerAge =
new
Spinner
(
parent, SWT.BORDER);
spinnerAge.setSelection
(
person.getAge
(
));
// Bouton d'impression du modele sur console
buttonPrint =
new
Button
(
parent, SWT.NONE);
buttonPrint.setText
(
"Print model"
);
buttonPrint.addSelectionListener
(
new
SelectionAdapter
(
) {
@Override
public
void
widgetSelected
(
SelectionEvent e) {
System.out.println
(
person.toString
(
));
}
}
);
// Bouton de modification du modele
buttonModify =
new
Button
(
parent, SWT.NONE);
buttonModify.setText
(
"Modify model"
);
buttonModify.addSelectionListener
(
new
SelectionAdapter
(
) {
@Override
public
void
widgetSelected
(
SelectionEvent e) {
person.setName
(
"Dupont"
);
person.setFirstname
(
"Mireille"
);
person.setAge
(
32
);
person.setMale
(
false
);
}
}
);
// Realisation du databinding
bindValues
(
);
}
/**
* Cette methode realise le binding entre le POJO de notre modele et les
* elements de l'interface.
*/
private
void
bindValues
(
) {
DataBindingContext context =
new
DataBindingContext
(
);
// Liaison du champ "Name"
IObservableValue modelValue =
PojoProperties.value
(
"name"
,
PersonPojo.class
).observe
(
person);
IObservableValue widgetValue =
WidgetProperties.text
(
SWT.Modify)
.observe
(
textName);
context.bindValue
(
modelValue, widgetValue);
// Liaison du champ "Firstname"
modelValue =
PojoProperties.value
(
"firstname"
, PersonPojo.class
)
.observe
(
person);
widgetValue =
WidgetProperties.text
(
SWT.Modify).observe
(
textFirstname);
context.bindValue
(
modelValue, widgetValue);
// Liaison du champ "Gender"
modelValue =
PojoProperties.value
(
"male"
, PersonPojo.class
)
.observe
(
person);
widgetValue =
WidgetProperties.selection
(
).observe
(
radioMale);
context.bindValue
(
modelValue, widgetValue);
// Liaison du champ "Age"
modelValue =
PojoProperties.value
(
"age"
, PersonPojo.class
)
.observe
(
person);
widgetValue =
WidgetProperties.selection
(
).observe
(
spinnerAge);
context.bindValue
(
modelValue, widgetValue);
}
@Override
public
void
setFocus
(
) {
//
}
}
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" :
Si nous modifions des éléments de l'interface, les changements sont correctement répercutés dans le modèle :
Par contre, si nous appuyons sur le bouton "Modify model", l'interface ne reflète pas les changements du 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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
161.
162.
163.
164.
165.
/**
* Cette classe definit une personne dont les informations peuvent etre
* presentees dans nos interfaces. Cette classe utilise un objet de type
* PropertyChangeSupport pour repercuter les modifications du modele sur
* l'interface graphique.
*
*
@author
A. Bernard
*/
public
class
PersonBean {
/**
* nom de la personne
*/
private
String name;
/**
* prenom de la personne
*/
private
String firstname;
/**
* sexe de la personne
*/
private
boolean
male;
/**
* age de la personne
*/
private
int
age;
/**
* cet element permet de notifier a tous les listeners qu'une modification
a
* ete effectuee dans le modele
*/
private
PropertyChangeSupport propertyChange;
/**
* Constructeur
*/
public
PersonBean
(
) {
this
.name =
""
;
this
.firstname =
""
;
this
.male =
true
;
this
.age =
0
;
}
/**
* Constructeur avec arguments
*/
public
PersonBean
(
String n, String f, int
a, boolean
m) {
this
.propertyChange =
new
PropertyChangeSupport
(
this
);
this
.setName
(
n);
this
.setFirstname
(
f);
this
.setMale
(
m);
this
.setAge
(
a);
}
/**
* Ajoute un element a la liste des ecouteurs des modifications de cette
* classe
*
@param
propertyName
la propriete que l'element veut surveiller
*
@param
listener
l'element qui s'abonne
*/
public
void
addPropertyChangeListener
(
String propertyName,
PropertyChangeListener listener) {
propertyChange.addPropertyChangeListener
(
propertyName, listener);
}
/**
* Supprime un element a la liste des ecouteurs des modifications de cette
* classe
*
@param
listener
l'element a supprimer
*/
public
void
removePropertyChangeListener
(
PropertyChangeListener listener) {
propertyChange.removePropertyChangeListener
(
listener);
}
/**
* Donne le nom de la personne
*
@return
le nom
*/
public
String getName
(
) {
return
name;
}
/**
* Definit le nom de la personne
*
@param
name
le nom
*/
public
void
setName
(
String name) {
propertyChange.firePropertyChange
(
"name"
, this
.name, this
.name =
name);
}
/**
* Donne le prenom de la personne
*
@return
le prenom
*/
public
String getFirstname
(
) {
return
firstname;
}
/**
* Definit le prenom de la personne
*
@param
firstname
le prenom
*/
public
void
setFirstname
(
String firstname) {
propertyChange.firePropertyChange
(
"firstname"
, this
.firstname,
this
.firstname =
firstname);
}
/**
* Donne le sexe de la personne
*
@return
<
code
>
true
<
/code
>
si la personne est de sexe masculin,
*
<
code
>
false
<
/code
>
sinon
*/
public
boolean
isMale
(
) {
return
male;
}
/**
* Definit le sexe de la personne
*
@param
male
<
code
>
true
<
/code
>
si la personne est de sexe masculin,
*
<
code
>
false
<
/code
>
sinon
*/
public
void
setMale
(
boolean
male) {
propertyChange.firePropertyChange
(
"male"
, this
.male, this
.male =
male);
}
/**
* Donne l'age de la personne
*
@return
l'age
*/
public
int
getAge
(
) {
return
age;
}
/**
* Definit l'age de la personne
*
@param
age
l'age
*/
public
void
setAge
(
int
age) {
propertyChange.firePropertyChange
(
"age"
, this
.age, this
.age =
age);
}
@Override
public
String toString
(
) {
String s =
"Nom : "
+
name +
", Prenom : "
+
firstname;
s +=
", Sexe : "
+
(
male ? "Masculin"
: "Feminin"
) +
" , Age : "
+
age;
s +=
"
\n
"
;
return
s;
}
@Override
public
boolean
equals
(
Object obj) {
if
(
obj instanceof
PersonBean) {
PersonBean p =
(
PersonBean) obj;
if
(
p.getName
(
).equalsIgnoreCase
(
name) &&
p.getFirstname
(
)
.equalsIgnoreCase
(
firstname) &&
p.getAge
(
) ==
age &&
p.isMale
(
) ==
male) {
return
true
;
}
}
return
false
;
}
}
Créons ensuite la vue "BeanBindingView" :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
161.
/**
* Cette vue met en oeuvre le mecanisme de databinding avec notre objet
* PersonBean.
*
@author
A. Bernard
*/
public
class
BeanBindingView extends
ViewPart {
/**
* champ texte pour le nom
*/
private
Text textName;
/**
* champ texte pour le prenom
*/
private
Text textFirstname;
/**
* bouton radio pour sexe masculin
*/
private
Button radioMale;
/**
* bouton radio pour sexe feminin
*/
private
Button radioFemale;
/**
* selecteur d'age
*/
private
Spinner spinnerAge;
/**
* bouton d'affichage du modele
*/
private
Button buttonPrint;
/**
* bouton de modification du modele
*/
private
Button buttonModify;
/**
* notre modele
*/
private
PersonBean person;
@Override
public
void
createPartControl
(
Composite parent) {
parent.setLayout
(
new
GridLayout
(
2
, false
));
// Definition du modele objet
person =
new
PersonBean
(
"Bernard"
, "Alain"
, 23
, true
);
// Champ nom
Label labelName =
new
Label
(
parent, SWT.NONE);
labelName.setLayoutData
(
new
GridData
(
SWT.RIGHT, SWT.CENTER, false
,
false
, 1
, 1
));
labelName.setText
(
"Name :"
);
textName =
new
Text
(
parent, SWT.BORDER);
textName.setLayoutData
(
new
GridData
(
SWT.FILL, SWT.CENTER, true
, false
,
1
, 1
));
textName.setText
(
person.getName
(
));
// Champ prenom
Label labelFirstname =
new
Label
(
parent, SWT.NONE);
labelFirstname.setLayoutData
(
new
GridData
(
SWT.RIGHT, SWT.CENTER, false
,
false
, 1
, 1
));
labelFirstname.setText
(
"Firstname :"
);
textFirstname =
new
Text
(
parent, SWT.BORDER);
textFirstname.setLayoutData
(
new
GridData
(
SWT.FILL, SWT.CENTER, true
,
false
, 1
, 1
));
textFirstname.setText
(
person.getFirstname
(
));
// Boutons radio pour le choix du sexe
Label lblGender =
new
Label
(
parent, SWT.NONE);
lblGender.setLayoutData
(
new
GridData
(
SWT.RIGHT, SWT.CENTER, false
,
false
, 1
, 1
));
lblGender.setText
(
"Gender :"
);
radioMale =
new
Button
(
parent, SWT.RADIO);
radioMale.setText
(
"Male"
);
radioMale.setSelection
(
person.isMale
(
));
new
Label
(
parent, SWT.NONE);
radioFemale =
new
Button
(
parent, SWT.RADIO);
radioFemale.setText
(
"Female"
);
radioFemale.setSelection
(!
person.isMale
(
));
// Champ d'age
Label labelAge =
new
Label
(
parent, SWT.NONE);
labelAge.setLayoutData
(
new
GridData
(
SWT.RIGHT, SWT.CENTER, false
,
false
, 1
, 1
));
labelAge.setText
(
"Age :"
);
spinnerAge =
new
Spinner
(
parent, SWT.BORDER);
spinnerAge.setSelection
(
person.getAge
(
));
// Bouton d'impression du modele sur console
buttonPrint =
new
Button
(
parent, SWT.NONE);
buttonPrint.setText
(
"Print model"
);
buttonPrint.addSelectionListener
(
new
SelectionAdapter
(
) {
@Override
public
void
widgetSelected
(
SelectionEvent e) {
System.out.println
(
person.toString
(
));
}
}
);
// Bouton de modification du modele
buttonModify =
new
Button
(
parent, SWT.NONE);
buttonModify.setText
(
"Modify model"
);
buttonModify.addSelectionListener
(
new
SelectionAdapter
(
) {
@Override
public
void
widgetSelected
(
SelectionEvent e) {
person.setName
(
"Dupont"
);
person.setFirstname
(
"Mireille"
);
person.setAge
(
32
);
person.setMale
(
false
);
}
}
);
// Realisation du databinding
bindValues
(
);
}
/**
* Cette methode realise le binding entre la classe de notre modele et les
* elements de l'interface.
*/
private
void
bindValues
(
) {
DataBindingContext context =
new
DataBindingContext
(
);
// Liaison du champ "Name"
IObservableValue modelValue =
BeanProperties.value
(
PersonBean.class
,
"name"
).observe
(
person);
IObservableValue widgetValue =
WidgetProperties.text
(
SWT.Modify)
.observe
(
textName);
context.bindValue
(
widgetValue, modelValue);
// Liaison du champ "Firstname"
modelValue =
BeanProperties.value
(
PersonBean.class
, "firstname"
)
.observe
(
person);
widgetValue =
WidgetProperties.text
(
SWT.Modify).observe
(
textFirstname);
context.bindValue
(
widgetValue, modelValue);
// Liaison du champ "Gender"
modelValue =
BeanProperties.value
(
PersonBean.class
, "male"
)
.observe
(
person);
widgetValue =
WidgetProperties.selection
(
).observe
(
radioMale);
context.bindValue
(
widgetValue, modelValue);
// Liaison du champ "Age"
modelValue =
BeanProperties.value
(
PersonBean.class
, "age"
)
.observe
(
person);
widgetValue =
WidgetProperties.selection
(
).observe
(
spinnerAge);
context.bindValue
(
widgetValue, modelValue);
}
@Override
public
void
setFocus
(
) {
//
}
}
Observons maintenant le résultat : lorsque l'on modifie l'IHM le modèle est correctement modifié :
De même, lorsque l'on modifie le modèle via notre bouton l'interface est modifiée :
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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
public
class
BeanBindingView extends
ViewPart implements
PropertyChangeListener
{
/**
* Cette methode realise le binding entre la classe de notre modele et les
* elements de l'interface.
*/
private
void
bindValues
(
) {
// ...
// Que faire avec le bouton female ?!
// Abonnement "manuel" aux changements de la classe Person
person.addPropertyChangeListener
(
"male"
, this
);
}
@Override
public
void
propertyChange
(
PropertyChangeEvent evt) {
// On est notifie des changements de la valeur de l'attribut "male"
// On reajuste a chaque coup la selection du champ "female"
radioFemale.setSelection
(!
person.isMale
(
));
}
@Override
public
void
dispose
(
) {
// Ne pas oublier de supprimer le listener a la fermeture de la vue !!
person.removePropertyChangeListener
(
this
);
}
}
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 :
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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
public
class
ValidationView extends
ViewPart implements
PropertyChangeListener
{
// ...
@Override
public
void
createPartControl
(
Composite parent) {
// ...
Label labelTitle =
new
Label
(
parent, SWT.NONE);
labelTitle.setLayoutData
(
new
GridData
(
SWT.RIGHT, SWT.CENTER, false
,
false
, 1
, 1
));
labelTitle.setText
(
"Errors : "
);
labelErrors =
new
Label
(
parent, SWT.NONE);
labelErrors.setLayoutData
(
new
GridData
(
SWT.FILL, SWT.CENTER, false
,
false
, 1
, 1
));
// Realisation du databinding
bindValues
(
);
}
/**
* Cette methode realise le binding entre la classe de notre modele et les
* elements de l'interface. Dans cette vue, on utilise le mecanisme de
* validation afin de verifier les donnees entrees par l'utilisateur.
*/
private
void
bindValues
(
) {
DataBindingContext context =
new
DataBindingContext
(
);
// Liaison du champ "Name"
IObservableValue modelValue =
BeanProperties.value
(
PersonBean.class
,
"name"
).observe
(
person);
IObservableValue widgetValue =
WidgetProperties.text
(
SWT.Modify)
.observe
(
textName);
IValidator validator =
new
IValidator
(
) {
@Override
public
IStatus validate
(
Object value) {
// On considere que le nom entre est correct s'il contient au
// moins deux lettres
if
(
textName.getText
(
).length
(
) >
1
) {
textName.setBackground
(
Display.getCurrent
(
).getSystemColor
(
SWT.COLOR_LIST_BACKGROUND));
return
ValidationStatus.ok
(
);
}
else
{
textName.setBackground
(
Display.getCurrent
(
).getSystemColor
(
SWT.COLOR_RED));
return
ValidationStatus.error
(
"Name must be longer than "
+
"two letters"
);
}
}
}
;
Binding binding =
context.bindValue
(
widgetValue, modelValue, new
UpdateValueStrategy
(
)
.setBeforeSetValidator
(
validator), null
);
ControlDecorationSupport.create
(
binding, SWT.TOP |
SWT.LEFT);
// Liaison du champ "Firstname"
modelValue =
BeanProperties.value
(
PersonBean.class
, "firstname"
)
.observe
(
person);
widgetValue =
WidgetProperties.text
(
SWT.Modify).observe
(
textFirstname);
validator =
new
IValidator
(
) {
@Override
public
IStatus validate
(
Object value) {
// On considere que le prenom entre est correct s'il contient
// au moins deux lettres
if
(
textFirstname.getText
(
).length
(
) >
1
) {
textFirstname.setBackground
(
Display.getCurrent
(
).getSystemColor
(
SWT.COLOR_LIST_BACKGROUND));
return
ValidationStatus.ok
(
);
}
else
{
textFirstname.setBackground
(
Display.getCurrent
(
).getSystemColor
(
SWT.COLOR_RED));
return
ValidationStatus.error
(
"Firstname must be longer "
+
"than two letters"
);
}
}
}
;
binding =
context.bindValue
(
widgetValue, modelValue, new
UpdateValueStrategy
(
)
.setBeforeSetValidator
(
validator), null
);
ControlDecorationSupport.create
(
binding, SWT.TOP |
SWT.LEFT);
// On affiche sur le label d'erreur la liste des erreurs possibles de
// validation
widgetValue =
WidgetProperties.text
(
).observe
(
labelErrors);
// This one listenes to all changes
context.bindValue
(
widgetValue,
new
AggregateValidationStatus
(
context.getBindings
(
),
AggregateValidationStatus.MAX_SEVERITY));
// Liaison du champ "Gender"
modelValue =
BeanProperties.value
(
PersonBean.class
, "male"
)
.observe
(
person);
widgetValue =
WidgetProperties.selection
(
).observe
(
radioMale);
context.bindValue
(
widgetValue, modelValue);
// Liaison du champ "Age"
modelValue =
BeanProperties.value
(
PersonBean.class
, "age"
)
.observe
(
person);
widgetValue =
WidgetProperties.selection
(
).observe
(
spinnerAge);
context.bindValue
(
widgetValue, modelValue);
// Que faire avec le bouton female ?!
// Abonnement "manuel" aux changements de la classe Person
person.addPropertyChangeListener
(
"male"
, this
);
}
// ...
}
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
:
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 :
Une fois l'interface créée nous nous rendons dans l'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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
public
class
StringValidator implements
IValidator {
@Override
public
IStatus validate
(
Object value) {
if
(
value instanceof
String) {
String s =
(
String) value;
if
(
s.length
(
) >
1
) {
return
ValidationStatus.ok
(
);
}
}
return
ValidationStatus.error
(
"String must be longer than two"
);
}
}
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 :
Remplissons ensuite les champs comme indiqué sur cette prise d'écran :
On peut ensuite valider : le binding est créé !
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 RealmRealm. 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 snippetSnippet Realm.
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.