I. Introduction▲
Nous avons vu dans un précédent article que, de base, l’API JavaFX fournit toutes les briques élémentaires pour créer des propriétés, expressions et valeurs observables sur des types simples. L’API fournit également les interfaces et méthodes nécessaires à la propagation des changements de valeur des propriétés, à la propagation des événements d’invalidation ou encore au binding (liaison à sens unique où le changement de valeur d’une propriété entraine un changement de valeur automatique d’une propriété liée) et au binding bidirectionnel (liaison dans les deux sens : la modification de valeur peut s’opérer sur n’importe lequel des deux bouts de la liaison et est automatiquement répercutée sur l’autre bout).
Cependant, parfois, l’API peut ne pas se révéler suffisante quand on doit être amené à devoir utiliser des valeurs observables ou devoir créer des liaisons complexes. Bien qu’on soit naturellement porté à créer un méli-mélo d’écouteurs en tout genre et de méthodes utilitaires invoquées en cascade pour tester tous les cas de figure, ce n’est pas la meilleure marche à suivre ; ni la plus simple.
Le binding de bas niveau – c'est-à-dire les classes de base sur lesquelles est construite l’API existante – est aussi prévu pour être étendu par le programmeur pour que celui-ci le plie à ses besoins. Dans bien des cas, cela mène également à un gain de performance et de mémoire.
Cet article suppose que vous sachiez déjà comment écrire et définir des propriétés en JavaFX ; il suppose également que vous sachiez comment fonctionnent les liaisons entre propriétés. Si ce n'est pas le cas, je vous invite à lire mes articles « Tutoriel sur les propriétés en JavaFX » ainsi que « Tutoriel sur le binding de haut niveau en JavaFX ».
II. Cas de figure▲
Prenons un cas de figure simple sur lequel j’ai dû me pencher récemment : dans une application, je dois afficher pas mal de labels contenant des dates (et aussi des coordonnées géographiques, ce qui présentait un problème similaire). C’est un problème qui peut paraitre simple au premier abord : on utilise un SimpleDateFormat et basta !
Hélas, cela ne l’est pas :
- La date est stockée dans un objet Date. Comme elle est amenée à changer, on va sans doute utiliser une propriété JavaFX de type ObjectProperty<Date>. Il va de soi que dès que la date change, le texte du label doit changer.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
private
final
ObjectProperty<
Date>
date =
new
SimpleObjectProperty<>(
this
, "date"
, null
);
public
final
Date getDate
(
) {
return
date.get
(
);
}
public
final
void
setDate
(
Date value) {
date.set
(
value);
}
public
final
ObjectProperty<
Date>
dateProperty
(
) {
return
date;
}
Note : ici, dans cet exemple, j’utilise les dates classiques de Java, je ne me penche pas sur la nouvelle API de gestion du temps du JDK8.
- Le format d’affichage de la date peut être configuré par l’utilisateur dans les options du programme parmi un choix de divers formats préétablis : long, moyen, court, très court, etc. Si l’utilisateur change et valide le format de la date dans les options, il faudrait pouvoir changer instantanément l’affichage de toutes les dates dans toute l’application… Comme il s’agit aussi là d’une propriété accessible dans les options, on va dire qu’on va stocker cela dans une ObjectProperty<DateFormat>.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
private
final
ObjectProperty<
DateFormat>
dateFormat =
new
SimpleObjectProperty<>(
this
, "dateFormat"
, DateFormat.getDateInstance
(
));
public
final
DateFormat getDateFormat
(
) {
return
dateFormat.get
(
);
}
public
final
void
setDateFormat
(
DateFormat value) {
dateFormat.set
(
value);
}
public
final
ObjectProperty<
DateFormat>
dateFormatProperty
(
) {
return
dateFormat;
}
Note : pour la conversion, on pourrait également utiliser d’autres choses telles que la classe DateStringConverter, une classe de l’API JavaFX.
- Éventuellement d’autres paramètres tels que la langue ou le pays peuvent aussi entrer en ligne de compte, mais nous allons les tenir à l’écart de manière à conserver ici un exemple simple.
Notre but est donc que tout changement apporté sur une date soit automatiquement répercuté sur tous les labels qui affichent cette date-ci ; tandis que tout changement apporté sur le format soit lui automatiquement répercuté sur tous les labels affichant des dates dans l’application. Ce n’est pas une mince affaire.
III. Première approche▲
La première approche est assez simple à mettre en place : on va mettre des écouteurs sur les deux propriétés sources et procéder à la modification du ou des labels. Par exemple, avec des instances de ChangeListener :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
dateProperty
(
).addListener
(
new
ChangeListener<
Date>(
) {
@Override
public
void
change
(
ObservableValue<
? extends
Date>
observableValue, Date oldValue, Date newValue) {
updateDateLabels
(
) ;
}
}
);
getApplication
(
).getSettings
(
).dateFormatProperty
(
).addListener
(
new
ChangeListener<
DateFormat>(
) {
@Override
public
void
change
(
ObservableValue<
? extends
DateFormat>
observableValue, DateFormat oldValue, DateFormat newValue) {
updateDateLabels
(
) ;
}
}
);
[...]
private
void
updateDateLabels
(
) {
Date date =
getDate
(
);
DateFormat format =
getApplication
(
).getSettings
(
).getDateFormat
(
);
String label =
((
date ==
null
) ||
(
format ==
null
)) ? null
: format.format
(
date);
myDateLabel.setText
(
label);
}
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
dateProperty
(
).addListener
((
ObservableValue<
? extends
Date>
observableValue, Date oldValue, Date newValue) ->
updateDateLabels
(
));
getApplication
(
).getSettings
(
).dateFormatProperty
(
).addListener
((
ObservableValue<
? extends
DateFormat>
observableValue, DateFormat oldValue, DateFormat newValue) ->
updateDateLabels
(
));
[...]
private
void
updateDateLabels
(
) {
Date date =
getDate
(
);
DateFormat format =
getApplication
(
).getSettings
(
).getDateFormat
(
);
String label =
((
date ==
null
) ||
(
format ==
null
)) ? null
: format.format
(
date);
myDateLabel.setText
(
label);
}
On pourrait écrire quelque chose de similaire en utilisant des instances d’InvalidationListener.
Bon, là, c’est juste pour un seul label. Imaginez maintenant que vous deviez faire cela partout dans toute l’application… Sans parler du fait qu’il faut également ajouter une meilleure gestion de la mémoire en désenregistrant les écouteurs après coup, etc. Bon courage…
IV. Seconde approche▲
Une bien meilleure approche consiste à utiliser le binding de bas niveau pour créer un nouveau Binding : une expression liée à une propriété ObjectProperty<Date> et une propriété ObjectProperty<DateFormat> en entrée, et qui va fournir une ObservableValue<String> en sortie. De cette manière, nous allons pouvoir lier la propriété text du label sur notre nouveau Binding et faire que le label se mette automatiquement à jour sans aucune intervention de notre part !
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.
public
final
class
DateLabelBinding extends
StringBinding {
private
ObjectProperty<
Date>
dateProperty;
private
ObjectProperty<
DateFormat>
dateFormatProperty;
public
DateLabelBinding
(
final
ObjectProperty<
Date>
dateProperty, final
ObjectProperty<
DateFormat>
dateFormatProperty) {
super
(
);
this
.dateProperty =
dateProperty;
this
.dateFormatProperty =
dateFormatProperty;
// Enregistrement des propriétés sources.
bind
(
dateProperty, dateFormatProperty);
}
@Override
public
void
dispose
(
) {
// Désenregistrement des propriétés sources.
unbind
(
dateProperty, dateFormatProperty);
dateProperty =
null
;
dateFormatProperty =
null
;
}
@Override
protected
String computeValue
(
) {
final
Date date =
(
dateProperty ==
null
) ? null
: dateProperty.get
(
);
final
DateFormat dateFormat =
(
dateFormatProperty ==
null
) ? null
: dateFormatProperty.get
(
);
// Calcul de la nouvelle valeur de la chaîne de texte.
return
((
date ==
null
) ||
(
dateFormat ==
null
)) ? null
: dateFormat.format
(
date);
}
}
Nous venons de créer un nouveau StringBinding, une classe qui implémente ObservableValue<String>, c'est-à-dire quelque chose sur quoi nous allons pouvoir mettre des écouteurs ou sur laquelle on peut lier une propriété.
Dans le constructeur de la classe, nous utilisons la méthode bind(), qui est une méthode protégée, pour lier notre Binding à ces deux propriétés sources. Ainsi, en cas de changement ou modification sur la date ou le format, la valeur de notre Binding sera invalidée et devra être recalculée.
La méthode computeValue() est la plus importante : c’est elle qui calculera la nouvelle valeur de notre Binding. Elle n’est appelée qu’en cas d’accès au getter de la valeur observable ; en effet, une invalidation n’implique pas nécessairement un nouveau calcul immédiat de la valeur.
La méthode dispose() est présente pour nous aider à casser les liaisons et vider les références de manière à faciliter la libération de la mémoire. Sa surcharge est optionnelle, mais après ne venez pas vous plaindre de fuites de mémoire si vous ne le faites pas…
Il existe également la méthode getDependencies() qui retourne la liste des dépendances d’un Binding. Ainsi nous pourrions écrire la méthode suivante :
2.
3.
4.
@Override
public
ObservableList<
?>
getDependencies
(
) {
return
FXCollections.unmodifiableObservableList
(
FXCollections.observableArrayList
(
dateProperty, dateFormatProperty));
}
Cependant, comme indiqué dans la javadoc de la classe javafx.beans.binding.Binding, cette méthode est avant tout destinée au programmeur lorsqu’il a besoin de tester ou d’explorer les changements de dépendances d’un binding (ici nos dépendances sont fixes, mais il est possible de créer des bindings avec des dépendances dynamiques). On évitera de l’utiliser ou de la définir dans un environnement de production.
Une fois notre nouvelle classe implémentée, la connexion au label devient alors très simple :
myLabel.textProperty
(
).bind
(
new
DateLabelBinding
(
dateProperty
(
), getApplication
(
).getSettings
(
).dateFormatProperty
(
)));
On peut aussi centraliser un même Binding pour que plusieurs labels se lient dessus. Du coup, cela simplifie largement le code de par l’application et évite de dupliquer des gestions d’écouteurs un peu partout.
Note : il faudra cependant continuer à faire attention aux liaisons et à la mémoire et penser à libérer les liaisons et disposer les bindings lorsque certains écrans ne sont plus utilisés.
V. Astuce▲
La classe Bindings présente dans l’API offre les méthodes utilitaires pour créer des expressions complexes, de même que les classes de base des expressions numériques telles que DoubleExpression (et donc par extension DoubleProperty, SimpleDoubleProperty, etc.). Cependant elles ont pour inconvénient d’utiliser beaucoup de mémoire, par exemple le code suivant :
NumberBinding g =
Bindings.add
(
Bindings.subtract
(
a.divide
(
2
d), b), c.multiply
(
d.add
(
e)).subract
(
f));
Cette ligne de code équivaut à l’expression suivante :
G = (A / 2.0) - B + ((C * (D + E)) – F) ;
Il faut bien comprendre que chaque appel à une des méthodes provoque la création de nouveaux Bindings, avec la gestion des écouteurs qui suivent, des références et liaisons entre propriétés, les invalidations en cascade, etc.
Sans compter, bien sûr, sur le fait que l’API ne fournit pas de méthode de binding pour des opérations arithmétiques plus complexes (sinus, cosinus, racine carrée, puissance, etc.).
Donc pour des équations longues ou utilisant des opérateurs autres que +, -, * et /, il devient vite intéressant de définir ses propres bindings de bas niveau pour offrir à la fois plus de fonctionnalités, plus de performance et une meilleure gestion de la mémoire !
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.
final
DoubleProperty a =
[...]
final
DoubleProperty b =
[...]
final
DoubleProperty c =
[...]
final
DoubleProperty d =
[...]
final
DoubleProperty e =
[...]
final
DoubleProperty f =
[...]
final
DoubleBinding g =
new
DoubleBinding
(
) {
{
bind
(
a, b, c, d, e, f);
}
@Override
public
void
dispose
(
) {
unbind
(
a, b, c, d, e, f);
}
@Override
protected
double
computeValue
(
) {
final
double
aVal =
a.getValue
(
);
final
double
bVal =
b.getValue
(
);
final
double
cVal =
c.getValue
(
);
final
double
dVal =
d.getValue
(
);
final
double
eVal =
e.getValue
(
);
final
double
fVal =
f.getValue
(
);
return
(
aVal /
2
d) -
bVal +
((
cVal *
(
dVal +
eVal)) -
fVal);
}
}
;
Le calcul en est également bien plus aisé à lire !
VI. Conclusion▲
Voilà, notre très rapide tour d’horizon du binding de bas niveau offert par JavaFX. La prise en main en est très simple et permet rapidement d’étendre les fonctionnalités de l’API de binding de haut niveau !
VII. Remerciements▲
Je tiens à remercier toute l’équipe du forum Développez ainsi que Logan Mauzaize et Mickael Baron pour leurs suggestions et leur relecture du présent article. Je tiens également à remercier Claude Leloup pour ses corrections orthographiques.