JFace Databinding sous Eclipse avec SWT

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 : FTPftp-sources ou HTTPhttp-sources.

Pour toute remarque ou question sur ce tutoriel, profitez de cette discussion : 3 commentaires Donner une note à l'article (5)

Article lu   fois.

L'auteur

Site personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

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 :

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
Liaison des données

Notre POJO est défini comme suit :

PersonPojo.java
CacherSélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.

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 :

ListenerView.java
CacherSélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.

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 :

Binding avec listener
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
CacherSélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
161.
162.
163.
164.

/**
 * 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" :

Image non disponible
É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
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
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".

 
Sélectionnez

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.
 
Sélectionnez

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.
 
Sélectionnez

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.

 
Sélectionnez

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
CacherSélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
161.
162.
163.
164.
165.
166.

/**
 * 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" :

BeanBindingView.java
CacherSélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
161.
162.

/**
 * 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é :

Modification via l'interface
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
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
CacherSélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.

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 :

MaJ du bouton 'Female'
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
CacherSélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.

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 :

Validation
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
Design de l'interface

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

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
CacherSélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.

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 :

Image non disponible
Étape 1

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

Image non disponible
Étape 2

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

Image non disponible
É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 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 :

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

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

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.