Tutoriel sur le binding de bas niveau en JavaFX

Créez de nouvelles expressions et valeurs observables en JavaFX.

Fabrice Bouyé

Cet article a pour but de vous montrer comment créer de nouvelles expressions grâce au binding de bas niveau 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 bas niveau en JavaFX

- Auteur : Fabrice Bouyé

- Parution : 5 mars 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. Cas de figure

- III. Première approche

- IV. Seconde approche

- V. Astuce

- VI. Conclusion

- VII. Remerciements

- VIII. Liens

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.

1. private final ObjectProperty<Date> date = new SimpleObjectProperty<>(this, "date", null);

3. public final Date getDate() {

4. return date.get();

5. }

7. public final void setDate(Date value) {

8. date.set(value);

9. }

11. public final ObjectProperty<Date> dateProperty() {

12. return date;

13. }

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

1. private final ObjectProperty<DateFormat> dateFormat = new SimpleObjectProperty<>(this, "dateFormat",  DateFormat.getDateInstance());

3. public final DateFormat getDateFormat() {

4. return dateFormat.get();

5. }

7. public final void setDateFormat(DateFormat value) {

8. dateFormat.set(value);

9. }

11. public final ObjectProperty<DateFormat> dateFormatProperty() {

12. return dateFormat;

13. }

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 :

JDK7

1. dateProperty().addListener(new ChangeListener<Date>() {

2. @Override

3. public void change(ObservableValue<? extends Date> observableValue, Date oldValue, Date newValue) {

4. updateDateLabels() ;

5. }

6. });

7. getApplication().getSettings().dateFormatProperty().addListener(new ChangeListener<DateFormat>() {

8. @Override

9. public void change(ObservableValue<? extends DateFormat> observableValue, DateFormat oldValue, DateFormat newValue) {

10. updateDateLabels() ;

11. }

12. });

14. [...]

16. private void updateDateLabels() {

17. Date date = getDate();

18. DateFormat format = getApplication().getSettings().getDateFormat();

19. String label = ((date == null) || (format == null)) ? null : format.format(date);

20. myDateLabel.setText(label);

21. }

JDK8

1. dateProperty().addListener((ObservableValue<? extends Date> observableValue, Date oldValue, Date newValue) -> updateDateLabels());

2. getApplication().getSettings().dateFormatProperty().addListener((ObservableValue<? extends DateFormat> observableValue, DateFormat oldValue, DateFormat newValue) -> updateDateLabels());

4. [...]

6. private void updateDateLabels() {

7. Date date = getDate();

8. DateFormat format = getApplication().getSettings().getDateFormat();

9. String label = ((date == null) || (format == null)) ? null : format.format(date);

10. myDateLabel.setText(label);

11. }

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 !

1. public final class DateLabelBinding extends StringBinding {

3. private ObjectProperty<Date> dateProperty;

4. private ObjectProperty<DateFormat> dateFormatProperty;

6. public DateLabelBinding(final ObjectProperty<Date> dateProperty, final ObjectProperty<DateFormat> dateFormatProperty) {

7. super();

8. this.dateProperty = dateProperty;

9. this.dateFormatProperty = dateFormatProperty;

10. // Enregistrement des propriétés sources.

11. bind(dateProperty, dateFormatProperty);

12. }

14. @Override

15. public void dispose() {

16. // Désenregistrement des propriétés sources.

17. unbind(dateProperty, dateFormatProperty);

18. dateProperty = null;

19. dateFormatProperty = null;

20. }

22. @Override

23. protected String computeValue() {

24. final Date date = (dateProperty == null) ? null : dateProperty.get();

25. final DateFormat dateFormat = (dateFormatProperty == null) ? null : dateFormatProperty.get();

26. // Calcul de la nouvelle valeur de la chaîne de texte.

27. return ((date == null) || (dateFormat == null)) ? null : dateFormat.format(date);

28. }

29. }

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 :

1. @Override

2. public ObservableList<?> getDependencies() {

3. return FXCollections.unmodifiableObservableList(FXCollections.observableArrayList(dateProperty, dateFormatProperty));

4. }

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 :

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

1. NumberBinding g = Bindings.add(Bindings.subtract(a.divide(2d), 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 !

1. final DoubleProperty a = [...]

2. final DoubleProperty b = [...]

3. final DoubleProperty c = [...]

4. final DoubleProperty d = [...]

5. final DoubleProperty e = [...]

6. final DoubleProperty f = [...]

7. final DoubleBinding g = new DoubleBinding() {

8. {

9. bind(a, b, c, d, e, f);

10. }

12. @Override

13. public void dispose() {

14. unbind(a, b, c, d, e, f);

15. }

17. @Override

18. protected double computeValue() {

19. final double aVal = a.getValue();

20. final double bVal = b.getValue();

21. final double cVal = c.getValue();

22. final double dVal = d.getValue();

23. final double eVal = e.getValue();

24. final double fVal = f.getValue();

25. return (aVal / 2d) - bVal + ((cVal * (dVal + eVal)) - fVal);

26. }

27. };

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.

VIII - Liens

- Using JavaFX Properties and Binding.