I. Introduction▲
Nous avons vu dans un précédent article comment créer des propriétés en JavaFX. L’avantage de telles propriétés est la gestion automatisée de la propagation des événements d’invalidation et de modification. Cependant, c’est lorsqu’on effectue du binding (du verbe anglais to bind, qui signifie « attacher » ou « lier »), c'est-à-dire, des liaisons avec mise à jour automatique des valeurs, que l’utilisation de ces propriétés prend tout son sens.
Dans cet article, nous allons nous concentrer sur le binding de haut niveau, c'est-à-dire les liaisons entre propriétés et expressions effectuées de manière simple avec les méthodes préexistantes fournies dans l’API JavaFX. Il existe un autre type de liaison, le binding de bas niveau qui permet au développeur d’écrire ses propres expressions ; cependant, nous ne l’aborderons pas ici.
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 déjà comment définir des écouteurs d’invalidation (InvalidationListener) et de modification (ChangeListener). Si ce n’est pas le cas, je vous invite à lire mon article « Tutoriel sur les propriétés en JavaFX ».
II. Liaison simple▲
Ce type de liaison est le type de liaison le plus simple : si une propriété B est liée à une propriété A, alors tout changement de valeur de A est automatiquement répercuté sur la propriété B. De plus, tant que la liaison est active, il est impossible de modifier manuellement la valeur de la propriété B. Si B est en lecture seule, il est bien sûr totalement impossible de la lier sur A. Si A est en lecture seule, elle peut sans problème être la source de liaisons effectuées par d’autres propriétés. C’est le binding simple ou binding unidirectionnel.
II-A. La méthode bind() ▲
Supposons que nous ayons les deux propriétés suivantes, A et B :
2.
IntegerProperty aProperty =
new
SimpleIntegerProperty
(-
10
); // A
IntegerProperty bProperty =
new
SimpleIntegerProperty
(
10
); // B
Ajoutons un ChangeListener sur B pour superviser ses changements de valeur :
Pour le moment, il ne se passe pas grand-chose. Maintenant, lions B à A en appelant la méthode bind() de la propriété B et en passant A en paramètre :
bProperty.bind
(
aProperty); // A est à -10.
Immédiatement, nous voyons s’afficher sur la console :
B changé: 10 -> -10 // A est à -10.
De même, si nous continuons à modifier la valeur de A :
2.
3.
4.
5.
aProperty.set
(
20
);
aProperty.set
(
30
);
aProperty.set
(
40
);
aProperty.set
(
50
);
aProperty.set
(
60
);
Nous obtiendrons alors :
2.
3.
4.
5.
B changé : -10 -> 20
B changé : 20 -> 30
B changé : 30 -> 40
B changé : 40 -> 50
B changé : 50 -> 60
Si nous essayons désormais de modifier directement B en appelant ses méthodes set() ou setValue(), une exception de type java.lang.RuntimeException contenant le message « A bound value cannot be set. » sera levée.
On remarquera, de plus, qu’il n’est pas possible de binder B sur n’importe quel type de propriété. Dans notre exemple, B ne peut être liée qu’à une valeur de type ObservableValue<Number> (souvenez-vous, IntegerProperty hérite de ObservableValue<Number> et non pas de ObservableValue<Integer>). De la même manière, une StringProperty, ne peut être liée qu’à une valeur de type ObservableValue<String>, etc.
Si jamais vous essayez de binder une propriété sur elle-même, une exception de type java.lang.IllegalArgumentException contenant le message « Cannot bind property to itself » sera levée.
Si jamais vous tentez de créer un cycle entre propriétés, par exemple :
2.
bProperty.bind
(
aProperty);
aProperty.bind
(
bProperty); // Fermeture du cycle.
Ou :
2.
3.
4.
5.
6.
bProperty.bind
(
aProperty);
cProperty.bind
(
bProperty);
dProperty.bind
(
cProperty);
eProperty.bind
(
dProperty);
fProperty.bind
(
eProperty);
aProperty.bind
(
fProperty); // Fermeture du cycle.
Dans ce cas, une exception de type java.lang.StackOverflowError sera levée lors de la fermeture du cycle car, l’évaluation de la valeur de la propriété entrainera une boucle infinie.
II-B. La méthode unbind()▲
Il est possible de casser une liaison entre deux propriétés en appelant la méthode unbind() de la propriété liée. Dans notre exemple, cela donne :
bProperty.unbind
(
);
Et si l’on fait :
2.
3.
4.
aProperty.set
(
70
);
aProperty.set
(
80
);
aProperty.set
(
90
);
System.out.printf
(
"Valeur de B : %d"
, bProperty.get
(
)).println
(
);
La seule chose qui sera affichée sera :
Valeur de B : 60
B est resté à la dernière valeur mise sur A avant qu’on ne casse la liaison.
II-C. Rebind▲
Le rebind a lieu quand on invoque à nouveau la méthode bind() de la propriété B sur une nouvelle propriété : C.
Cela a pour effet de casser la liaison entre A et B et d’établir une nouvelle liaison entre B et C.
Le résultat est :
2.
3.
4.
5.
B changé : 10 -> -10 // A est à -10.
B changé : -10 -> 20
B changé : 20 -> 1000 // C est à 1000.
B changé : 1000 -> 1001
Valeur de B : 1001
Une fois la nouvelle liaison établie, les changements de A ne sont plus propagés vers B.
Par contre, il n’y a bien sûr aucune limite au nombre de propriétés qu’on peut « binder » sur la propriété A.
2.
3.
4.
bProperty.bind
(
aProperty);
cProperty.bind
(
aProperty);
dProperty.bind
(
aProperty);
[...]
III. Liaison bidirectionnelle▲
Parfois, il peut être utile d’établir des liaisons bidirectionnelles : c'est-à-dire des liaisons dans les deux sens. Si la propriété A et la propriété B sont liées entre elles par une liaison bidirectionnelle, alors tout changement de valeur de la propriété A se répercute automatiquement sur la propriété B ; mais tout changement de valeur de la propriété B se répercute automatiquement sur la propriété A également !
III-A. La méthode bindBidirectionnal() ▲
Reprenons notre exemple initial :
Nous allons ajouter un ChangeListener sur la propriété A également :
Et maintenant, lions la propriété B sur la propriété A en appelant sa méthode bindBidirectionnal() et recommençons nos tests :
2.
3.
4.
5.
6.
7.
bProperty.bindBidirectional
(
aProperty); // A est à -10.
System.out.println
(
"------------------"
);
aProperty.set
(
20
);
aProperty.set
(
30
);
aProperty.set
(
40
);
aProperty.set
(
50
);
aProperty.set
(
60
);
Pour le moment, pas de changement, nous obtenons exactement le même résultat que précédemment (avec les événements de modification de A en plus) :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
B changé : 10 -> -10
------------------
A changé : -10 -> 20
B changé : -10 -> 20
A changé : 20 -> 30
B changé : 20 -> 30
A changé : 30 -> 40
B changé : 30 -> 40
A changé : 40 -> 50
B changé : 40 -> 50
A changé : 50 -> 60
B changé : 50 -> 60
Maintenant, essayons de faire ce qui ne fonctionnait pas dans la section précédente : modifions la valeur de B !
2.
3.
4.
5.
6.
System.out.println
(
"------------------"
);
bProperty.set
(-
20
);
bProperty.set
(-
30
);
bProperty.set
(-
40
);
bProperty.set
(-
50
);
bProperty.set
(-
60
);
Désormais, aucune exception ne sera levée ; en fait, c’est même A qui va prendre la valeur de B !
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
------------------
B changé : 60 -> -20
A changé : 60 -> -20
B changé : -20 -> -30
A changé : -20 -> -30
B changé : -30 -> -40
A changé : -30 -> -40
B changé : -40 -> -50
A changé : -40 -> -50
B changé : -50 -> -60
A changé : -50 -> -60
Les restrictions précédemment indiquées concernant les types acceptés par la méthode bind() s’appliquent également à la méthode bindBidirectionnal().
De la même manière, si jamais vous essayez de binder une propriété sur elle-même, une exception de type java.lang.IllegalArgumentException contenant le message « Cannot bind property to itself » sera levée.
III-B. La méthode unbindBidirectionnal()▲
Dans notre nouveau cas de figure, appeler la méthode unbind() de la propriété B ne suffit plus à casser la liaison :
2.
3.
4.
System.out.println
(
"------------------"
);
bProperty.unbind
(
);
aProperty.set
(
70
); // Changement sur A.
bProperty.set
(-
70
); // Changement sur B.
Les choses ne se passent pas comme prévu et nous obtenons :
2.
3.
4.
5.
------------------
A changé : -70 -> 70 // Changement sur A.
B changé : -70 -> 70
B changé : -60 -> -70 // Changement sur B.
A changé : -60 -> -70
Pour casser cette liaison, il nous faut désormais appeler la méthode unbindBidirectionnal() en lui passant la propriété A en paramètre :
2.
3.
4.
5.
6.
System.out.println
(
"------------------"
);
bProperty.unbindBidirectional
(
aProperty);
aProperty.set
(
80
);
bProperty.set
(-
80
);
System.out.printf
(
"Valeur de A : %d"
, aProperty.get
(
)).println
(
);
System.out.printf
(
"Valeur de B : %d"
, bProperty.get
(
)).println
(
);
Le résultat est désormais celui que nous attendions :
2.
3.
4.
A changé : -70 -> 80
B changé : -70 -> -80
Valeur de A : 80
Valeur de B : -80
Contrairement au binding simple, les liaisons ne sont pas cassées lors d’un rebinding utilisant les liaisons bidirectionnelles.
Ainsi si les propriétés A et B sont liées de manière bidirectionnelle, il est tout de même possible de lier les propriétés B et C bidirectionnellement !
Ce qui a pour résultat de propager les changements de valeur vers les trois propriétés :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
B changé : 10 -> -10 // A est à -10.
B changé : -10 -> 1000 // C est à 1000.
A changé : -10 -> 1000
A changé : 1000 -> 20 // A
B changé : 1000 -> 20
C changé : 1000 -> 20
B changé : 20 -> -20 // B
A changé : 20 -> -20
C changé : 20 -> -20
C changé : -20 -> 1001 // C
B changé : -20 -> 1001
A changé : -20 -> 1001
Attention, en aucun cas la propriété A n’est liée à la propriété C : les changements de valeur transitent toujours par B. Si la liaison entre A et B était rompue, alors la propriété A ne recevrait plus les changements de valeur de C et inversement.
IV. Ensemble ?▲
Les deux types de binding sont indépendants l’un de l’autre… donc si vous voulez vraiment avoir mal au crâne, vous pouvez faire le test suivant : A et B sont liés bidirectionnellement, A est liée sur C …
Ceci ne cause pas d’exception et affiche le résultat suivant :
2.
3.
4.
5.
6.
7.
8.
9.
B changé : 10 -> -10
------------------
A changé : -10 -> 1000
B changé : -10 -> 1000
A changé : 1000 -> 1001
B changé : 1000 -> 1001
Valeur de A : 1001
Valeur de B : 1001
Valeur de C : 1001
Tout changement de C est répercuté sur A et sur B.
Cependant, désormais, les limites imposées par le binding simple s’appliquent également à notre liaison bidirectionnelle et il est impossible d’appeler les méthodes set(), setValue() et bindBidirectionnal() de A (on peut toujours appeler bind()) une exception de type java.lang.RuntimeException contenant le message « A bound value cannot be set. ».
Appeler les méthodes set(), setValue(), bind() et bindBidirectionnal() de B est plus problématique :
bProperty.set
(
1002
);
Dans le JDK7, une exception bloquante de type java.lang.RuntimeException contenant le message « A bound value cannot be set. » est levée. Les changements de valeur effectués par un set() ou setValue() invoqué sur la propriété B s’appliquent, mais la propagation du changement échoue lorsqu’elle atteint la propriété A ce qui laisse les propriétés dans un état incohérent (A et B contiennent des valeurs différentes).
Dans le JDK8, une exception non bloquante de type java.lang.RuntimeException contenant le message « A bound value cannot be set. » est générée et imprimée sur la console. Le programme continue de s’exécuter, mais B ne change pas de valeur.
De plus l’ordre des liaisons de même que l’orientation de la liaison bidirectionnelle sont également importants et peuvent générer des résultats différents.
Par exemple ceci fonctionne :
2.
aProperty.bind
(
cProperty);
bProperty.bindBidirectional
(
aProperty);
Tandis que ceci génère immédiatement une exception bloquante de type java.lang.RuntimeException contenant le message « A bound value cannot be set. » :
aProperty.bind
(
cProperty);
aProperty.bindBidirectional
(
bProperty);
Ce genre de mélange est à éviter et à manier avec précaution donc.
V. Les expressions▲
Évidemment, même ainsi, les liaisons entre propriétés restent simples : il ne s’agit que de propagations de valeurs simples ce qui limite fortement la portée des propriétés que l’on peut définir. Fort heureusement, il existe la possibilité de définir des expressions !
Reprenons nos deux propriétés entières A et B ; nous souhaiterions leur adjoindre une troisième propriété, booléenne cette fois-ci, dont la valeur est équivalente aux lignes de pseudocode suivantes :
boolean
C =
(
A ==
B);
C’est tout à fait possible via la création de nouveaux bindings : des définitions d’expressions !
Nous avons utilisé la méthode isEqualTo() de la propriété A en passant la propriété B en paramètre pour créer un objet de type BooleanBinding. Cet objet n’est pas une propriété, c’est une expression. C’est aussi un ObservableValue<Boolean> ce qui veut donc dire que notre propriété C peut se « binder » dessus sans problème.
Initialement, notre propriété C a une valeur de false (puisqu’initialisée avec le constructeur par défaut) ; or, c’est aussi la valeur de notre binding, puisque A (-10) n’a pas la même valeur que B (10). Il est donc normal qu’aucun événement ne soit levé. Mais si nous faisons :
2.
bProperty.set
(-
10
);
bProperty.set
(
20
);
Alors, la valeur de C change correctement !
2.
C changé : false -> true
C changé : true -> false
Dans le cadre de propriétés numériques, un certain nombre de méthodes destinées à construire des expressions sont définies dans la classe parente abstraite NumberExpressionBase permettant de faire des tests ou même des opérations. Ainsi, il nous est tout à fait possible de définir une propriété D qui soit l’équivalent de la formule :
int
D =
(
A -
B);
À cet effet, il nous suffit de faire :
Désormais la valeur de D sera automatiquement mise à jour quand A ou B change :
2.
3.
D changé : 0 -> -30 // Binding (A = -10, B = 20)
D changé : -30 -> 30 // A (A = 50, B = 20)
D changé : 30 -> 70 // B (A = 50, B = -20)
Les expressions disponibles dépendent de chaque classe de base, par exemple, la classe StringExpression (ancêtre de StringProperty) dispose de la méthode isEqualToIgnoreCase() ou la classe ListExpression<V> (ancêtre de ListProperty<V>) permet d’utiliser l’expression valueAt() qui retourne un binding sur la valeur à un indice donné dans la liste. Pensez donc à bien lire la javadoc de chaque classe de propriété et d’expression.
VI. La classe Bindings▲
Il est possible de définir de nouvelles expressions plus complexes en utilisant les méthodes statiques de la classe javafx.beans.binding.Bindings. Il serait un peu trop long d’énumérer toutes les méthodes disponibles dans cette classe, aussi nous allons nous concentrer sur les plus fréquemment utilisées.
VI-A. La méthode convert()▲
Cette méthode prend en paramètre une instance de la classe ObservableValue et la convertit directement en StringExpression. Si le paramètre était déjà une StringExpression, il est directement retourné par la méthode. Son utilisation évidente est lorsque l’on veut rapidement afficher dans l’UI une valeur numérique ou la représentation textuelle d’un objet.
VI-B. La méthode concat()▲
Cette méthode prend en paramètres un nombre variable d’instances d’objets et les convertit en une StringExpression. Si au moins un des paramètres est une instance de ObservableValue, alors l’expression sera modifiée correctement lors de ses changements de valeur. De la même manière, cette expression est principalement utilisée pour afficher des messages dans l’UI.
VI-C. La méthode format()▲
Cette méthode prend en paramètre une chaine de caractères au format java.util.Formatter (format similaire à la fonction C printf()) et un nombre variable d’instances d’objets et les convertit en une StringExpression. Si au moins un des paramètres est une instance de ObservableValue, alors l’expression sera modifiée correctement lors de ses changements de valeur. Encore une fois, cette expression est principalement utilisée pour afficher des messages dans l’UI.
VI-D. La méthode isEmpty()▲
Cette méthode existe en trois variantes :
- une pour les instances d’ObservableList ;
- une pour les instances d’ObservableMap ;
- une pour les instances d’ObservableSet.
L’expression booléenne retournée aura une valeur à true si la collection observable est vide. Cette méthode est utilisée pour, par exemple, activer ou désactiver des portions d’UI en fonction des sélections de l’utilisateur dans une ListView ou une TableView.
VI-E. La méthode size()▲
Cette méthode existe en trois variantes :
- une pour les instances d’ObservableList ;
- une pour les instances d’ObservableMap ;
- une pour les instances d’ObservableSet.
L’expression numérique retournée contiendra la taille de la collection observable ciblée. Ici aussi, cette méthode est utilisée pour, par exemple, afficher des retours dans une UI en fonction des sélections de l’utilisateur dans une ListView ou une TableView.
VI-F. Les méthodes selectXXX()▲
Il peut arriver que l’on soit obligé de superviser ou de binder une propriété d’une propriété.
Imaginons que nous avons une propriété booléenne A et que nous souhaitons superviser la propriété disable d’une instance de la classe Button (celle de l’API SceneGraph, pas celle de AWT) qui est stockée dans une propriété B. Une première approche naïve serait de considérer que nous avons l’expression suivante :
boolean
A =
B.disable;
Déjà, c’est oublier que B est une propriété et donc qu’elle peut très bien avoir une valeur égale à null. Notre expression devient donc en fait :
boolean
A =
(
B ==
null
) ? false
: B.disable;
Et ensuite, comment récupérer la propriété d’une propriété ?
C’est ici que la méthode selectBoolean() de la classe Bindings vient nous aider. Cette méthode prend en paramètres la propriété source à surveiller (ici B) et ensuite le nom d’une ou plusieurs propriétés cibles. Dans notre exemple, cela se met en pratique comme suit :
Ce qui donnera :
2.
3.
4.
Valeur de A : false
---------------
--------------- // Pas de changement, c’est normal.
A changé : false -> true
On peut faire la même chose avec des types de bindings autres que des liaisons booléennes. Utilisons maintenant la méthode selectString() pour superviser la propriété text de notre propriété B.
Ce qui donne bien :
2.
C changé : null ->
C changé : -> Hello World!
Les méthodes selectXXX() prennent un nombre variable de noms de propriétés en paramètres. Il s’agit en fait du chemin à parcourir jusqu’à la propriété finale. Par exemple, si nous avons:
Bindings.selectBoolean
(
aProperty, "alpha"
, "beta"
, "gamma"
);
Cela reviendrait en fait à faire un appel à :
aProperty.getValue
(
).getAlpha
(
).getBeta
(
).isGamma
(
);
La valeur retournée dépendra de plusieurs facteurs :
- Est-ce que la valeur de A est null ou pas ?
- Est-ce que alpha est une propriété ou pas ?
- Est-ce que alpha est null ou pas ?
- Est-ce que beta est une propriété ou pas ?
- Est-ce que beta est null ou pas ?
- Est-ce que gamma est une propriété ou pas ?
- Est-ce que gamma est une propriété booléenne ou pas ?
- Est-ce que gamma est null ou pas ?
Et si ces critères ne sont pas remplis, la valeur retournée sera false. Pensez à bien lire la documentation pour savoir quelles sont les valeurs par défaut !
Note : étrangement, les JDK ont un fonctionnement un peu différent quand on utilise les méthodes selectXXX() sur des propriétés qui sont initialement à la valeur null :
- JDK 7 : rien à signaler, la liaison est établie silencieusement. Tout fonctionne comme décrit dans la documentation ;
- JDK 8, 8_5 : contrairement au JDK 7, établir une telle liaison sur une propriété contenant la valeur null imprime une exception sur la sortie d’erreur standard. Cette exception ne remonte pas dans le programme et n’empêche pas ce dernier de fonctionner ;
- JDK 8_20, 8_25 : un simple warning est affiché. Ici aussi, cet avertissement n’empêche pas le programme de fonctionner.
Dans tous les cas, même si vous apercevez une erreur ou un warning, il n’interfère en rien avec le bon fonctionnement du binding créé par la méthode selectXXX(). Le binding se comporte exactement comme décrit dans la javadoc et retourne donc une valeur par défaut quand la propriété observée est à null et la bonne valeur quand ce n’est pas le cas.
VI-G. La methode when()▲
Cette méthode est un peu plus complexe que les autres et elle ne produit pas directement une expression ; elle est l’équivalent de l’opérateur ternaire :
int
C =
TEST ? A : B;
Ou encore :
2.
3.
4.
5.
6.
int
C =
0
;
if
(
TEST) {
C =
A;
}
else
{
C =
B;
}
La méthode retourne un objet qui est une instance de la classe javafx.beans.binding.When. Il est possible de modifier le comportement de cette instance en appelant ses méthodes then() et otherwise() et de se binder sur l’objet retourné. Voici un exemple simple utilisant des constantes, mais il est tout à fait possible de passer des valeurs observables en paramètres de la condition ou des méthodes then() et otherwise() :
Le résultat est bien sûr que A prend la valeur -5 (puisque la valeur de notre test très simple est false) :
A changé : 0 -> -5
VII. Quelques considérations de performances▲
Comme vous l’avez sans doute remarqué, créer autant de liaisons, notamment lorsqu’on utilise des expressions, a tendance à créer beaucoup de petits objets en mémoire. Couplé à la gestion des événements d’invalidation et de changements nécessaires à la propagation des valeurs pour conserver l’intégrité des liaisons, cela peut entrainer une consommation de mémoire excessive ainsi qu’un drain de performances. Il ne faut donc pas en abuser.
De plus, les liaisons entre valeurs observables liées étant fortes, il faut faire le nécessaire pour casser ces références sous peine que le garbage collector ne puisse récupérer ces espaces mémoire. Il faudra donc penser à appeler les méthodes unbind() et unbindBidirectionnal() quand cela est nécessaire.
Sachez, cependant, qu’il vous est possible de définir vos propres expressions et liaisons en utilisant l’API de binding de bas niveau. De telles définitions sont par nature moins gourmandes que des agrégats d’expressions créées par des appels aux méthodes de la classe Bindings. Mais c’est là le sujet d’un autre tutoriel…
VIII. Conclusion▲
Nous avons fini notre tour d’horizon de ce bref aperçu des possibilités du binding. Désormais, vous savez comment lier des propriétés entre elles, et ce, que soit de manière unidirectionnelle ou bidirectionnelle. Vous avez également eu un aperçu des possibilités du binding par expression et vous savez que vous pouvez explorer les méthodes des classes de base ou de la classe Bindings pour créer de nouvelles expressions complexes.
IX. Remerciements▲
Je tiens à remercier toute l’équipe du forum Développez ainsi que Mickael Baron et Logan Mauzaize pour leurs suggestions et leur relecture du présent article. Je tiens également à remercier Claude Leloup pour ses corrections orthographiques.