Tutoriel sur le binding de haut niveau en JavaFX

Effectuez des liaisons entre propriétés et expressions en JavaFX.

Fabrice Bouyé

Cet article a pour but de vous expliquer comment faire du binding de haut niveau entre propriétés en JavaFX.

Pour réagir au contenu de cet article, un espace de dialogue vous est proposé sur le forum Commentez

- Titre : Tutoriel sur le binding de haut niveau en JavaFX

- Auteur : Fabrice Bouyé

- Parution : 11 février 2015

- Public visé :

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2015 Fabrice Bouyé. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. Liaison simple

- II-A. La méthode bind()

- II-B. La méthode unbind()

- II-C. Rebind

- III. Liaison bidirectionnelle

- III-A. La méthode bindBidirectionnal()

- III-B. La méthode unbindBidirectionnal()

- IV. Ensemble ?

- V. Les expressions

- VI. La classe Bindings

- VI-A. La méthode convert()

- VI-B. La méthode concat()

- VI-C. La méthode format()

- VI-D. La méthode isEmpty()

- VI-E. La méthode size()

- VI-F. Les méthodes selectXXX()

- VI-G. La methode when()

- VII. Quelques considérations de performances

- VIII. Conclusion

- IX. Remerciements

- X. Liens

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.

Figure 1 - Une liaison simple.

II-A - La méthode bind()

Supposons que nous ayons les deux propriétés suivantes, A et B :

1. IntegerProperty aProperty = new SimpleIntegerProperty(-10);  // A

2. IntegerProperty bProperty = new SimpleIntegerProperty(10);   // B

Ajoutons un ChangeListener sur B pour superviser ses changements de valeur :

Code JDK7

1. bProperty.addListener(new ChangeListener<Number>() {

3. @Override

4. public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {

5. System.out.printf("B changé : %d -> %d", oldValue, newValue).println();

6. }

7. });

Code JDK8

1. bProperty.addListener((ObservableValue<? extends Number> observable, Number oldValue, Number newValue) -> {

2. System.out.printf("B changé : %d -> %d", oldValue, newValue).println();

3. });

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 :

1. bProperty.bind(aProperty); // A est à -10.

Immédiatement, nous voyons s’afficher sur la console :

Sortie

1. B changé: 10 -> -10        // A est à -10.

De même, si nous continuons à modifier la valeur de A :

1. aProperty.set(20);

2. aProperty.set(30);

3. aProperty.set(40);

4. aProperty.set(50);

5. aProperty.set(60);

Nous obtiendrons alors :

Sortie

1. B changé : -10 -> 20

2. B changé : 20 -> 30

3. B changé : 30 -> 40

4. B changé : 40 -> 50

5. 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 :

1. bProperty.bind(aProperty);

2. aProperty.bind(bProperty); // Fermeture du cycle.

Ou :

1. bProperty.bind(aProperty);

2. cProperty.bind(bProperty);

3. dProperty.bind(cProperty);

4. eProperty.bind(dProperty);

5. fProperty.bind(eProperty);

6. 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 :

1. bProperty.unbind();

Et si l’on fait :

1. aProperty.set(70);

2. aProperty.set(80);

3. aProperty.set(90);

4. System.out.printf("Valeur de B : %d", bProperty.get()).println();

La seule chose qui sera affichée sera :

Sortie

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

Figure 2 - B dépend de A.

Cela a pour effet de casser la liaison entre A et B et d’établir une nouvelle liaison entre B et C.

Figure 3 - B dépend maintenant de C.

Code JDK7

1. IntegerProperty aProperty = new SimpleIntegerProperty(-10);

2. IntegerProperty bProperty = new SimpleIntegerProperty(10);

3. bProperty.addListener(new ChangeListener<Number>() {

4. @Override

5. public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {

6. System.out.printf("B changé : %d -> %d", oldValue, newValue).println();

7. }

8. });

9. IntegerProperty cProperty = new SimpleIntegerProperty(1000);

10. bProperty.bind(aProperty); // A est à -10.

11. aProperty.set(20);

12. bProperty.bind(cProperty); // C est à 1000.

13. cProperty.set(1001);

14. aProperty.set(30);

15. System.out.printf("Valeur de B : %d", bProperty.get()).println();

Code JDK8

1. IntegerProperty aProperty = new SimpleIntegerProperty(-10);

2. IntegerProperty bProperty = new SimpleIntegerProperty(10);

3. bProperty.addListener((ObservableValue<? extends Number> observable, Number oldValue, Number newValue) -> {

4. System.out.printf("B changé : %d -> %d", oldValue, newValue).println();

5. });

6. IntegerProperty cProperty = new SimpleIntegerProperty(1000);

7. bProperty.bind(aProperty); // A est à -10.

8. aProperty.set(20);

9. bProperty.bind(cProperty); // C est à 1000.

10. cProperty.set(1001);

11. aProperty.set(30);

12. System.out.printf("Valeur de B : %d", bProperty.get()).println();

Le résultat est :

Sortie

1. B changé : 10 -> -10 // A est à -10.

2. B changé : -10 -> 20

3. B changé : 20 -> 1000 // C est à 1000.

4. B changé : 1000 -> 1001

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

Figure 4 - B, C et D dépendent de A.

1. bProperty.bind(aProperty);

2. cProperty.bind(aProperty);

3. dProperty.bind(aProperty);

4. [...]

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 !

Figure 5 - A et B dépendent l'un de l'autre.

III-A - La méthode bindBidirectionnal()

Reprenons notre exemple initial :

Code JDK7

1. IntegerProperty aProperty = new SimpleIntegerProperty(-10);  // A

2. IntegerProperty bProperty = new SimpleIntegerProperty(10);   // B

3. bProperty.addListener(new ChangeListener<Number>() {

4. @Override

5. public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {

6. System.out.printf("B changé : %d -> %d", oldValue, newValue).println();

7. }

8. });

Code JDK8

1. IntegerProperty aProperty = new SimpleIntegerProperty(-10);  // A

2. IntegerProperty bProperty = new SimpleIntegerProperty(10);   // B

3. bProperty.addListener((ObservableValue<? extends Number> observable, Number oldValue, Number newValue) -> {

4. System.out.printf("B changé : %d -> %d", oldValue, newValue).println();

5. });

Nous allons ajouter un ChangeListener sur la propriété A également :

Code JDK7

1. aProperty.addListener(new ChangeListener<Number>() {

2. @Override

3. public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {

4. System.out.printf("A changé : %d -> %d", oldValue, newValue).println();

5. }

6. });

Code JDK8

1. aProperty.addListener((ObservableValue<? extends Number> observable, Number oldValue, Number newValue) -> {

2. System.out.printf("A changé : %d -> %d", oldValue, newValue).println();

3. });

Et maintenant, lions la propriété B sur la propriété A en appelant sa méthode bindBidirectionnal() et recommençons nos tests :

1. bProperty.bindBidirectional(aProperty); // A est à -10.

2. System.out.println("------------------");

3. aProperty.set(20);

4. aProperty.set(30);

5. aProperty.set(40);

6. aProperty.set(50);

7. 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) :

Sortie

1. B changé : 10 -> -10

2. ------------------

3. A changé : -10 -> 20

4. B changé : -10 -> 20

5. A changé : 20 -> 30

6. B changé : 20 -> 30

7. A changé : 30 -> 40

8. B changé : 30 -> 40

9. A changé : 40 -> 50

10. B changé : 40 -> 50

11. A changé : 50 -> 60

12. B changé : 50 -> 60

Maintenant, essayons de faire ce qui ne fonctionnait pas dans la section précédente : modifions la valeur de B !

1. System.out.println("------------------");

2. bProperty.set(-20);

3. bProperty.set(-30);

4. bProperty.set(-40);

5. bProperty.set(-50);

6. 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 !

Sortie

1. ------------------

2. B changé : 60 -> -20

3. A changé : 60 -> -20

4. B changé : -20 -> -30

5. A changé : -20 -> -30

6. B changé : -30 -> -40

7. A changé : -30 -> -40

8. B changé : -40 -> -50

9. A changé : -40 -> -50

10. B changé : -50 -> -60

11. 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 :

1. System.out.println("------------------");

2. bProperty.unbind();

3. aProperty.set(70);  // Changement sur A.

4. bProperty.set(-70); // Changement sur B.

Les choses ne se passent pas comme prévu et nous obtenons :

Sortie

1. ------------------

2. A changé : -70 -> 70       // Changement sur A.

3. B changé : -70 -> 70

4. B changé : -60 -> -70      // Changement sur B.

5. 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 :

1. System.out.println("------------------");

2. bProperty.unbindBidirectional(aProperty);

3. aProperty.set(80);

4. bProperty.set(-80);

5. System.out.printf("Valeur de A : %d", aProperty.get()).println();

6. System.out.printf("Valeur de B : %d", bProperty.get()).println();

Le résultat est désormais celui que nous attendions :

Sortie

1. A changé : -70 -> 80

2. B changé : -70 -> -80

3. Valeur de A : 80

4. Valeur de B : -80

Contrairement au binding simple, les liaisons ne sont pas cassées lors d’un rebinding utilisant les liaisons bidirectionnelles.

Figure 6 - A et B dépendent l'un de l'autre.

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 !

Figure 7 - A et B dépendent l'un de l'autre ; B et C dépendent l'un de l'autre.

Code JDK7

1. IntegerProperty aProperty = new SimpleIntegerProperty(-10);

2. IntegerProperty bProperty = new SimpleIntegerProperty(10);

3. IntegerProperty cProperty = new SimpleIntegerProperty(1000);

4. aProperty.addListener(new ChangeListener<Number>() {

5. @Override

6. public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {

7. System.out.printf("A changé : %d -> %d", oldValue, newValue).println();

8. }

9. });

10. bProperty.addListener(new ChangeListener<Number>() {

11. @Override

12. public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {

13. System.out.printf("B changé : %d -> %d", oldValue, newValue).println();

14. }

15. });

16. cProperty.addListener(new ChangeListener<Number>() {

17. @Override

18. public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {

19. System.out.printf("C changé : %d -> %d", oldValue, newValue).println();

20. }

21. });

22. bProperty.bindBidirectional(aProperty); // A est à -10.

23. bProperty.bindBidirectional(cProperty); // C est à 1000.

24. aProperty.set(20); // A

25. bProperty.set(-20); // B

26. cProperty.set(1001); // C

Code JDK8

1. IntegerProperty aProperty = new SimpleIntegerProperty(-10);

2. IntegerProperty bProperty = new SimpleIntegerProperty(10);

3. IntegerProperty cProperty = new SimpleIntegerProperty(1000);

4. aProperty.addListener((ObservableValue<? extends Number> observable, Number oldValue, Number newValue) -> {

5. System.out.printf("A changé : %d -> %d", oldValue, newValue).println();

6. });

7. bProperty.addListener((ObservableValue<? extends Number> observable, Number oldValue, Number newValue) -> {

8. System.out.printf("B changé : %d -> %d", oldValue, newValue).println();

9. });

10. cProperty.addListener((ObservableValue<? extends Number> observable, Number oldValue, Number newValue) -> {

11. System.out.printf("C changé : %d -> %d", oldValue, newValue).println();

12. });

13. bProperty.bindBidirectional(aProperty); // A est à -10.

14. bProperty.bindBidirectional(cProperty); // C est à 1000.

15. aProperty.set(20); // A

16. bProperty.set(-20); // B

17. cProperty.set(1001); // C

Ce qui a pour résultat de propager les changements de valeur vers les trois propriétés :

Sortie

1. B changé : 10 -> -10      // A est à -10.

2. B changé : -10 -> 1000    // C est à 1000.

3. A changé : -10 -> 1000

4. A changé : 1000 -> 20     // A

5. B changé : 1000 -> 20

6. C changé : 1000 -> 20

7. B changé : 20 -> -20      // B

8. A changé : 20 -> -20

9. C changé : 20 -> -20

10. C changé : -20 -> 1001   // C

11. B changé : -20 -> 1001

12. 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 …

Figure 8 - A et B dépendent l'un de l'autre. A dépend de C.

Code JDK7

1. IntegerProperty aProperty = new SimpleIntegerProperty(-10);

2. IntegerProperty bProperty = new SimpleIntegerProperty(10);

3. aProperty.addListener(new ChangeListener<Number>() {

4. @Override

5. public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {

6. System.out.printf("A changé : %d -> %d", oldValue, newValue).println();

7. }

8. });

9. bProperty.addListener(new ChangeListener<Number>() {

10. @Override

11. public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {

12. System.out.printf("B changé : %d -> %d", oldValue, newValue).println();

13. }

14. });

15. bProperty.bindBidirectional(aProperty); // A est à -10.

16. System.out.println("------------------");

17. IntegerProperty cProperty = new SimpleIntegerProperty(1000);

18. aProperty.bind(cProperty);

19. cProperty.set(1001);

20. System.out.printf("Valeur de A : %d", aProperty.get()).println();

21. System.out.printf("Valeur de B : %d", bProperty.get()).println();

22. System.out.printf("Valeur de C : %d", cProperty.get()).println();

Code JDK8

1. IntegerProperty aProperty = new SimpleIntegerProperty(-10);

2. IntegerProperty bProperty = new SimpleIntegerProperty(10);

3. aProperty.addListener((ObservableValue<? extends Number> observable, Number oldValue, Number newValue) -> {

4. System.out.printf("A changé : %d -> %d", oldValue, newValue).println();

5. });

6. bProperty.addListener((ObservableValue<? extends Number> observable, Number oldValue, Number newValue) -> {

7. System.out.printf("B changé : %d -> %d", oldValue, newValue).println();

8. });

9. bProperty.bindBidirectional(aProperty); // A est à -10.

10. System.out.println("------------------");

11. IntegerProperty cProperty = new SimpleIntegerProperty(1000);

12. aProperty.bind(cProperty);

13. cProperty.set(1001);

14. System.out.printf("Valeur de A : %d", aProperty.get()).println();

15. System.out.printf("Valeur de B : %d", bProperty.get()).println();

16. System.out.printf("Valeur de C : %d", cProperty.get()).println();

Ceci ne cause pas d’exception et affiche le résultat suivant :

Sortie

1. B changé : 10 -> -10

2. ------------------

3. A changé : -10 -> 1000

4. B changé : -10 -> 1000

5. A changé : 1000 -> 1001

6. B changé : 1000 -> 1001

7. Valeur de A : 1001

8. Valeur de B : 1001

9. 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 :

1. 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 :

1. aProperty.bind(cProperty);

2. 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 :

Pseudocode

1. boolean C = (A == B);

C’est tout à fait possible via la création de nouveaux bindings : des définitions d’expressions !

Code JDK7

1. IntegerProperty aProperty = new SimpleIntegerProperty(-10);

2. IntegerProperty bProperty = new SimpleIntegerProperty(10);

3. BooleanProperty cProperty = new SimpleBooleanProperty();

4. cProperty.addListener(new ChangeListener<Boolean>(){

6. @Override

7. public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {

8. System.out.printf("C changé : %b -> %b", oldValue, newValue).println();

9. }

10. });

11. cProperty.bind(aProperty.isEqualTo(bProperty));   // C = (A == B)

Code JDK8

1. IntegerProperty aProperty = new SimpleIntegerProperty(-10);

2. IntegerProperty bProperty = new SimpleIntegerProperty(10);

3. BooleanProperty cProperty = new SimpleBooleanProperty();

4. cProperty.addListener((ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) -> {

5. System.out.printf("C changé : %b -> %b", oldValue, newValue).println();

6. });

7. cProperty.bind(aProperty.isEqualTo(bProperty));   // C = (A == B)

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 :

1. bProperty.set(-10);

2. bProperty.set(20);

Alors, la valeur de C change correctement !

Sortie

1. C changé : false -> true

2. 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 :

Pseudocode

1. int D = (A - B);

À cet effet, il nous suffit de faire :

Code JDK7

1. IntegerProperty dProperty = new SimpleIntegerProperty();

2. dProperty.addListener(new ChangeListener<Number>(){

4. @Override

5. public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {

6. System.out.printf("D changé : %d -> %d", oldValue.intValue(), newValue.intValue()).println();

7. }

8. });

9. dProperty.bind(aProperty.subtract(bProperty));   // D = A - B

10. aProperty.setValue(50);  // A

11. bProperty.setValue(-20); // B

Code JDK8

1. IntegerProperty dProperty = new SimpleIntegerProperty();

2. dProperty.addListener((ObservableValue<? extends Number> observable, Number oldValue, Number newValue) -> {

3. System.out.printf("D changé : %d -> %d", oldValue.intValue(), newValue.intValue()).println();

4. });

5. dProperty.bind(aProperty.subtract(bProperty));   // D = A - B

6. aProperty.setValue(50);  // A

7. bProperty.setValue(-20); // B

Désormais la valeur de D sera automatiquement mise à jour quand A ou B change :

Sortie

1. D changé : 0 -> -30    // Binding (A = -10, B = 20)

2. D changé : -30 -> 30   // A (A = 50, B = 20)

3. 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 :

Pseudocode

1. 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 :

Pseudocode

1. 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 :

Code JDK7

1. ObjectProperty<Button> bProperty = new SimpleObjectProperty<>();

2. BooleanProperty aProperty = new SimpleBooleanProperty();

3. aProperty.addListener(new ChangeListener<Boolean>() {

4. @Override

5. public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {

6. System.out.printf("A changé : %b -> %b", oldValue, newValue).println();

7. }

8. });

9. aProperty.bind(Bindings.selectBoolean(bProperty, "disable"));

10. System.out.printf("Valeur de A : %b", aProperty.get()).println();

11. System.out.println("---------------");

12. bProperty.setValue(new Button());      // disable est a false sur une nouvelle instance.

13. System.out.println("---------------"); // Pas de changement, c’est normal.

14. bProperty.get().setDisable(true);

Code JDK8

1. ObjectProperty<Button> bProperty = new SimpleObjectProperty<>();

2. BooleanProperty aProperty = new SimpleBooleanProperty();

3. aProperty.addListener((ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) -> {

4. System.out.printf("A changé : %b -> %b", oldValue, newValue).println();

5. });

6. aProperty.bind(Bindings.selectBoolean(bProperty, "disable"));

7. System.out.printf("Valeur de A : %b", aProperty.get()).println();

8. System.out.println("---------------");

9. bProperty.setValue(new Button());      // disable est a false sur une nouvelle instance.

10. System.out.println("---------------"); // Pas de changement, c’est normal.

11. bProperty.get().setDisable(true);

Ce qui donnera :

Sortie

1. Valeur de A : false

2. ---------------

3. --------------- // Pas de changement, c’est normal.

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

Code JDK7

1. StringProperty cProperty = new SimpleStringProperty();

2. cProperty.addListener(new ChangeListener<String>() {

3. @Override

4. public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {

5. System.out.printf("C changé : %s -> %s", oldValue, newValue).println();

6. }

7. });

8. cProperty.bind(Bindings.selectString(bProperty, "text")); // Le texte du bouton est initialement égal à "".

9. bProperty.get().setText("Hello World!");

Code JDK8

1. StringProperty cProperty = new SimpleStringProperty();

2. cProperty.addListener((ObservableValue<? extends String> observable, String oldValue, String newValue) -> {

3. System.out.printf("C changé : %s -> %s", oldValue, newValue).println();

4. });

5. cProperty.bind(Bindings.selectString(bProperty, "text")); // Le texte du bouton est initialement égal à "".

6. bProperty.get().setText("Hello World!");

Ce qui donne bien :

Sortie

1. C changé : null ->

2. 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:

1. Bindings.selectBoolean(aProperty, "alpha", "beta", "gamma");

Cela reviendrait en fait à faire un appel à :

1. 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 :

Pseudocode

1. int C = TEST ? A : B;

Ou encore :

Pseudocode

1. int C = 0;

2. if (TEST) {

3. C = A;

4. } else {

5. C = B;

6. }

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() :

Code JDK7

1. IntegerProperty aProperty = new SimpleIntegerProperty();

2. aProperty.addListener(new ChangeListener<Number>() {

3. @Override

4. public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {

5. System.out.printf("A changé : %d -> %d", oldValue, newValue).println();

6. }

7. });

8. When when = Bindings.when(new SimpleBooleanProperty(false));

9. aProperty.bind(when.then(5).otherwise(-5));

Code JDK8

1. IntegerProperty aProperty = new SimpleIntegerProperty();

2. aProperty.addListener((ObservableValue<? extends Number> observable, Number oldValue, Number newValue) -> {

3. System.out.printf("A changé : %d -> %d", oldValue, newValue).println();

4. });

5. When when = Bindings.when(new SimpleBooleanProperty(false));

6. aProperty.bind(when.then(5).otherwise(-5));

Le résultat est bien sûr que A prend la valeur -5 (puisque la valeur de notre test très simple est false) :

Sortie

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

X - Liens

- Using JavaFX Properties and Binding.