IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Tutoriel sur les propriétés en JavaFX

De l'art et de la manière de définir des propriétés dans vos objets JavaFX

Cet article a pour but de vous expliquer comment définir et utiliser des propriétés JavaFX. Cet article fera un rapide tour d'horizon des différents types de propriétés définies dans l'API en montrant quelques cas particuliers d'utilisation où le programmeur doit faire attention à ce qu'il manipule. Cet article n'abordera pas le binding.

Pour réagir au contenu de cet article, un espace de dialogue vous est proposé sur le forum 5 commentaires Donner une note à l´article (5)

Article lu   fois.

L'auteur

Profil ProSite personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Introduction

Contrairement à d'autres langages tels que C#, Java ne possède toujours pas une manière implicite de définir une propriété; bien que le format JavaBeans pour déclarer des propriétés dans un objet soit le standard le plus suivi, la définition de propriétés en Java reste encore une affaire de programmation manuelle dans la quasi-majorité des cas. Les années passant, plusieurs projets et JSR ont vu le jour pour faciliter l'écriture et la validation des propriétés, mais, à ce jour, aucun d'entre eux n'a réellement abouti.

Or, au fil du temps, de nouveaux besoins sont apparus tels que la possibilité de faire du binding, c'est-à-dire de pouvoir propager automatiquement la valeur d'une propriété vers d'autres dès sa modification, sans avoir à recoder toute une gestion événementielle manuellement à chaque fois qu'on crée une nouvelle classe. JavaFX 2.x offre désormais un support des propriétés compatible avec les bases de JavaBeans tout en offrant une prise en charge de ces nouvelles fonctionnalités.

Il faut bien comprendre cependant que JavaFX ne règle pas pour autant le problème de la déclaration implicite des propriétés en Java. Cette problématique reste liée à des évolutions futures du langage et de la VM Java.

II. Conventions

Accéder à une propriété nécessite d'appeler une ou deux méthodes qui permettent d'obtenir la valeur de la propriété et de la redéfinir :

  • le getter permet de récupérer la valeur de la propriété. Cette méthode n'a pas de paramètres d'entrée ;
  • le setter permet de redéfinir la valeur de la propriété. Cette méthode est optionnelle: elle n'est pas présente quand la propriété est en lecture seule. Un setter ne prend qu'un seul paramètre, du même type que celui de la propriété.

Selon la convention, ces méthodes sont écrites sous la forme :

<préfixe><Nom de la propriété avec la première lettre en majuscule>

Pour les getters, on utilisera le préfixe get (verbe anglais signifiant obtenir) pour des valeurs littérales (entiers, flottants, etc.), chaines de caractères ou objet ou is (verbe anglais signifiant être) pour des valeurs booléennes. C'est assez rare, mais parfois certains getters des API Java et JavaFX utilisent le verbe get comme préfixe pour des valeurs booléennes.

Pour les setters on utilisera le préfixe set (verbe anglais signifiant définir).

Ainsi nous aurons :

 
Sé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.
// Propriété enabled.
private boolean enabled;            

// Getter.
public boolean isEnabled() {         
  return enabled;
}    

// Setter.
public void setEnabled(boolean value) { 
  enabled = value;
}

// Propriété age.
private int age;                    

// Getter.
public int getAge() {                
  return age;
}

// Setter.
public void setAge(int value) {        
  age = value;
}

// Propriété name.
private String name;                

// Getter.
public String getName() {            
  return name;
}

// Setter.
public void setName(String value) {     
  name = value;
}
// Propriété favoriteCar.
private Car favoriteCar;            

// Getter.
public Car getFavoriteCar() {        
  return favoriteCar;
}

// Setter.
public void setFavoriteCar(Car value) {
  favoriteCar = value;
}

Les beans offrent donc une notion simple de propriété avec des accesseurs permettant de lire et modifier les valeurs de ces propriétés. Le fait d'avoir une convention de nommage permet d'identifier rapidement les propriétés accessibles dans une classe en lisant sa page de documentation ou en en listant ses méthodes via la reflection (mécanisme d'introspection).

Les propriétés JavaFX restent compatibles avec ces conventions, donc un objet disposant de propriétés JavaFX pourra être utilisé comme un bean normal.

II-A. Limitation des Java Beans

Cependant, il n'est pas vraiment possible de se mettre à l'écoute des changements des valeurs, car ce support est complètement optionnel. Il est en effet possible de rajouter un support des java.bean.PropertyChangeListener sur la classe pour permettre à des listeners (écouteurs) de s'y enregistrer, mais ceci doit être fait à la main pour chaque nouvelle classe créée. Chaque lancement d'événement dans une propriété doit également être codé manuellement.

De plus, il n'est pas du tout possible de faire du binding, c'est-à-dire la capacité de « nouer» aisément des propriétés entre elles pour avoir une propagation automatique des modifications d'une propriété à une autre.

C'est ici que les propriétés JavaFX entrent en jeu: elles apportent une infrastructure prête à l'emploi supportant la propagation d'événements ainsi que le binding.

II-B. Et en JavaFX?

Voici très rapidement ce que donne la propriété age si on l'écrit avec les propriétés JavaFX. Nous entrerons dans les détails des différents types de propriétés dans les sections ultérieures.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
// Propriété age.
private final IntegerProperty age = new SimpleIntegerProperty();

// Getter.
public final int getAge() {                
  return age.get();
}

// Setter.
public final void setAge(int value) {        
  age.set(value);
}

// Accès à la propriété.
public final IntegerProperty ageProperty() {        
  return age;
}

Tout d'abord, on peut voir que, plutôt que de stocker directement la valeur dans la classe, on utilise une propriété JavaFX qui va empaqueter la valeur: il s'agit des classes IntegerProperty et SimpleIntegerProperty. On peut accéder à la valeur de la propriété et la modifier par des accesseurs classiques: get(), getValue(), set(), setValue(). De plus, cette classe se chargera de produire les événements appropriés lorsque la valeur change. Comme cette classe est également une expression, il est possible de faire du binding de haut niveau. L'initialisation de la propriété se fait en appelant une classe concrète SimpleIntegerProperty avec le constructeur par défaut, ce qui mettra la propriété à une valeur par défaut :

  • false pour les booléens ;
  • 0 pour les nombres ;
  • null pour les chaines ou les objets.

Il existe cependant d'autres constructeurs qui permettent de passer une référence au bean parent, un nom textuel à la propriété ainsi qu'une valeur initiale. Pensez à vous référer à la documentation de chaque classe concrète lorsque vous créerez vos propriétés.

Ensuite, nous avons rajouté une 3e méthode en plus du getter et du setter: il s'agit de l'accesseur public à la propriété. Via cette méthode, on peut désormais depuis l'extérieur de la classe, récupérer une référence sur l'objet propriété et rajouter des listeners sur la propriété, faire du binding et aussi directement retirer ou définir la valeur de la propriété via les méthodes set(), get(), setValue() et getValue().

La signature de cette méthode prend toujours la forme :

public final <type de la propriété> <nom de la propriété>Property();

Soit ici :

 
Sélectionnez
1.
2.
3.
public final IntegerProperty ageProperty() {
  [...]
}

Utiliser cette convention permettra à l'accesseur d'être facilement retrouvé si on use de la reflection.

Enfin, toutes les propriétés et les méthodes sont déclarées final ce qui veut dire qu'elles ne peuvent être ni modifiées, ni surchargées par les classes filles de notre classe. C'est quelque chose de TRÈS important, car cela permet de préserver l'intégrité de la propriété. Si le programmeur pouvait surcharger n'importe laquelle de ces méthodes dans une classe fille, il casserait complètement le mécanisme de la propriété, par exemple en retournant une valeur fixe dans le getter ou en évitant de la stocker dans l'objet propriété dans le setter ou encore en établissant dans le setter une restriction qu'on peut contourner en accédant directement à la propriété. Ainsi, un objet qui accèderait directement à la propriété obtiendrait des valeurs différentes de celles retournées par le getter et le setter… Bref, cela causerait de gros soucis dans le programme, donc mieux vaut ne pas le permettre en interdisant toute surcharge des méthodes.

III. Les bases

JavaFX propose donc toute une série de classes et d'interfaces dédiées à la définition des propriétés qu'elles soient en lecture seule, en lecture-écriture ou encore pour empaqueter des propriétés venant de beans Java normaux ou même des valeurs immutables.

III-A. Interfaces communes

Les interfaces qui sont ancêtres de toutes ces classes sont :

  • javafx.beans.Observable - cette interface définit la possibilité d'ajouter et de retirer des écouteurs de type InvalidationListener sur la propriété ;
  • javafx.beans.ObservableValue<T> - cette interface définit la possibilité d'ajouter et de retirer des écouteurs de type ChangeListener<T> sur la propriété. Cette interface définit également la méthode getValue() qui permet de récupérer la valeur contenue dans l'objet propriété.

III-B. Hiérarchie des classes

Ces interfaces sont elles-mêmes dérivées en expressions qui contiennent des méthodes destinées à faciliter les expressions liées binding, puis en propriété lecture seule (des classes concrètes où seul le getter est implémenté), et enfin en propriété lecture-écriture avec une implémentation du setter (la méthode setValue()) ainsi que le support du binding et du binding bidirectionnel. Enfin une implémentation concrète prête à l'emploi nous est fournie. Ceci nous donne quelque chose de similaire au schéma suivant, en omettant quelques classes intermédiaires au passage pour les propriétés stockant des valeurs entières :

Image non disponible
Figure 1 - Hiérarchie des classes.

Il existe également d'autres interfaces et classes mineures annexes qui rajoutent des méthodes de convenance destinées à faciliter la programmation comme, par exemple ObservableIntegerValue qui définit une méthode get() retournant un int ou WritableIntegerValue qui définit une méthode set() prenant un int en paramètre.

III-C. Écouteurs

III-C-1. InvalidationListener

Cet écouteur permet d'être tenu au courant des événements d'invalidation des propriétés. En effet, lorsque la valeur d'une propriété change, elle devient invalide. Par la même occasion, elle provoque l'invalidation de toute expression qui lui est liée par binding. L'expression restera invalide tant qu'il n'y a pas d'accès à sa valeur par un getter. Cela veut dire que si la propriété change quand on fait quelque chose comme cela :

JDK7
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
ageProperty().addListener(new InvalidationListener() {
  @Override
  public void invalidated(Observable o) {
    [...]
  }
});
setAge(5);             // événement d'invalidation ici. 
setAge(6);
setAge(7);
JDK8
Sélectionnez
1.
2.
3.
4.
5.
6.
ageProperty().addListener((Observable o) -> {
  [...]
});
setAge(5);             // événement d'invalidation ici. 
setAge(6);
setAge(7);

On aura bien un événement d'invalidation au premier appel au setter, mais ensuite plus du tout jusqu'au dernier appel au setter. C'est au moment où l'accès au getter se fait que la valeur de la propriété est réévaluée et que la propriété redevient valide. Cela permet de minimiser le nombre d'événements produits et d'empêcher que des propriétés bindées entre elles ne soient réévaluées trop souvent quand cela n'est pas nécessaire.

Attention: si un ChangeListener et un InvalidationListener sont placés sur la même propriété, les événements d'invalidation seront lancés à chaque changement de valeur.

III-C-2. ChangeListener

Cet écouteur permet d'être tenu au courant des changements de valeur des propriétés. Lorsque la valeur d'une propriété change, la propriété, son ancienne valeur et sa nouvelle valeur sont transmises à l'écouteur pour traitement immédiat.

JDK7
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
ageProperty().addListener(new ChangeListener<Number>() {
  @Override
  public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
    [...]
  }
}); 
setAge(5);             // événement de changement ici. 
setAge(6);             // événement de changement ici. 
setAge(7);             // événement de changement ici. 
int newAge = getAge(); // retourne 7.
setAge(8);             // événement de changement ici.
JDK8
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
ageProperty().addListener((ObservableValue<? extends Number> observable, Number oldValue, Number newValue) -> {
  [...]
}); 
setAge(5);             // événement de changement ici. 
setAge(6);             // événement de changement ici. 
setAge(7);             // événement de changement ici. 
int newAge = getAge(); // retourne 7.
setAge(8);             // événement de changement ici.

Étant donné que les événements sont plus fréquents et transportent plus d'informations, ces listeners sont plus gourmands en ressource et en mémoire.

III-C-2-a. ChangeListener sur les nombres

Concernant les propriétés contenant des littéraux numériques, on serait tenté lorsqu'on veut enregistrer des écouteurs de faire la chose suivante :

JDK7
Sélectionnez
1.
2.
3.
4.
5.
6.
ageProperty().addListener(new ChangeListener<Integer>(){
  @Override
  public void changed(ObservableValue<? extends Integer> observable, Integer oldValue, Integer newValue) {
    [...]
  }
};
JDK8
Sélectionnez
1.
2.
3.
ageProperty().addListener((ObservableValue<? extends Integer> observable, Integer oldValue, Integer newValue) -> {
  [...]
};

Or cela ne fonctionnera pas, car IntegerProperty et SimpleIntegerProperty héritent de ObservableValue<Number> au lieu de ObservableValue<Integer>; il faudra donc faire à la place :

JDK7
Sélectionnez
1.
2.
3.
4.
5.
6.
ageProperty().addListener(new ChangeListener<Number>(){
    @Override
  public void changed(ObservableValue<? extends Number > observable, Number oldValue, Number newValue) {
    [...]
  }
});
JDK8
Sélectionnez
1.
2.
3.
ageProperty().addListener((ObservableValue<? extends Number > observable, Number oldValue, Number newValue) -> {
  [...]
});

C'est également le cas de toutes les classes de propriété qui permettent de stocker des nombres.

III-D. Threading

Tels quels, il semble que les propriétés et événements JavaFX pourront fonctionner depuis n'importe quel thread.

Attention, lorsqu'elles font partie de nœuds graphiques du SceneGraph, les propriétés sont généralement prévues pour être lancées et manipulées depuis le JavaFX Application Thread. Si des nœuds sont attachés à une scène, manipuler des propriétés dans un thread autre que le JavaFX Application Thread provoquera une exception. Ce thread est habituellement démarré au lancement d'une Application JavaFX ou lors de l'utilisation d'un JFXPanel (pour afficher du JavaFX dans du Swing) ou d'un JFXCanvas (pour afficher du JavaFX dans du SWT).

IV. Mise en œuvre

IV-A. Booléens, littéraux et chaines de caractères

  • javafx.beans.property.BooleanProperty - permet de stocker des valeurs booléennes.
  • javafx.beans.property.IntegerProperty - permet de stocker des valeurs entières.
  • javafx.beans.property.LongProperty - permet de stocker des valeurs entières longues.
  • javafx.beans.property.FloatProperty - permet de stocker des valeurs flottantes.
  • javafx.beans.property.DoubleProperty - permet de stocker des valeurs flottantes en précision double.
  • javafx.beans.property.StringProperty - permet de stocker des chaines de caractères.

Il n'existe pas de stockage dédié pour les littéraux de type byte, char et short. Il faudra donc utiliser des IntegerProperty ou une ObjectProperty (voir plus bas) pour stocker de telles valeurs.

Chacun de ces types dispose de classes parentes qui ne permettent que la lecture seule (ex. : ReadOnlyIntegerProperty) et de classes concrètes filles prêtes à l'emploi permettant la lecture-écriture (ex. : SimpleIntegerProperty).

Comme montré précédemment, une implémentation de la propriété age peut se faire comme suit :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
// Propriété age.
private final IntegerProperty age = new SimpleIntegerProperty();

// Getter.
public final int getAge() {                
  return age.get();
}

// Setter.
public final void setAge(int value) {        
  age.set(value);
}

// Accès à la propriété.
public final IntegerProperty ageProperty() {        
  return age;
}

IV-B. Objets

  • javafx.beans.property.ObjectProperty<T> - permet de stocker tout autre type d'objets. Ici T est bien entendu le type de la valeur de la propriété.

Cette classe est dédiée au stockage des objets au sens large. Une implémentation de la propriété favoriteCar peut se faire comme suit :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
// Propriété favoriteCar.
private final ObjectProperty<Car> favoriteCar = new Simple ObjectProperty<>();

// Getter.
public final Car getFavoriteCar() {                
  return favoriteCar.get();
}

// Setter.
public final void setFavoriteCar(Car value) {        
  favoriteCar.set(value);
}

// Accès à la propriété.
public final ObjectProperty<Car> favoriteCarProperty() {        
  return favoriteCar;
}

IV-B-1. Listes observables

  • javafx.beans.property.ListProperty<V> - permet de stocker des ObservableList<V>. Ici V est le type des objets contenus dans la liste observable.

Les listes observables (ObservableList) sont une part importante de l'API JavaFX. Elles sont énormément utilisées dans les contrôles tels que ComboBox, ListView, TableView ou les différents types de graphes et elles sont au cœur même des nœuds parents (des nœuds qui contiennent d'autres nœuds et servent de base aux layouts et aux groupes).

Imaginons que désormais nous avons non plus une, mais plusieurs voitures préférées. Nous serions tentés d'écrire quelque chose comme cela :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
// Propriété favoriteCars.
private final ObjectProperty<ObservableList<Car>> favoriteCars = new SimpleObjecProperty<>();

// Getter.
public final ObservableList<Car> getFavoriteCars() {            
  return favoriteCars.get();
}

// Setter.
public final void setFavoriteCars(ObservableList<Car> value) {    
  favoriteCars.set(value);
}

// Accès à la propriété.
public final ObjecProperty<ObservableList<Car>> favoriteCarsProperty() {        
  return favoriteCars;
}

Malheureusement, l'implémentation d'ObjectProperty pose quelques problèmes avec l'utilisation des listes observables. Tout d'abord, quand on manipule des listes observables, on est moins intéressé par les InvalidationListener et ChangeListener qui sont lancés lorsque la propriété change de valeur (c'est-à-dire lorsque la référence sur la liste dans la propriété change : on a remplacé la liste A par une liste B dans la propriété) que par les changements opérés sur le contenu même de la liste. C'est donc un ListChangeListener qui nous intéresse : on veut savoir quand un ou plusieurs éléments de liste sont retirés ou ajoutés (un remplacement de la liste A par la liste B pouvant être considéré comme un remplacement complet, dans la propriété, des éléments de la liste A par ceux de la liste B).

Sans rentrer dans les détails d'implémentation, maintenir un ChangeListener<ObservableList<T>> sur une ObjectProperty<ObservableList<T>> dans le seul but de désinstaller (sur l'ancienne liste) et de réinstaller (sur la nouvelle liste) un ListChangeListener<T> s'avère vite fastidieux à mettre en place et ne fonctionne pas bien finalement: il y a des cas dans lesquels les événements de changement de valeur de la propriété ne sont pas lancés correctement et donc notre écouteur de liste peut ne pas être correctement installé sur la nouvelle liste de stockage.

Il existe donc un type de propriété spécialement destiné à stocker les ObservableList, il s'agit de ListProperty. La définition de notre propriété peut alors s'écrire :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
// Propriété favoriteCar.
private final ListProperty<Car> favoriteCars = new SimpleListProperty<>();

// Getter.
public final ObservableList<Car> getFavoriteCars() {            
  return favoriteCars.get();
}

// Setter.
public final void setFavoriteCars(ObservableList<Car> value) {    
  favoriteCars.set(value);
}

// Accès à la propriété.
public final ListProperty<Car> favoriteCarsProperty() {        
  return favoriteCars;
}

Bon, après il faut quand même faire attention qu'ici la propriété est initialisée à une valeur égale à null. Et donc un l'appel suivant provoquera une NullPointerException :

 
Sélectionnez
1.
myObject.getFavoriteCars().add(new, Car());

Il faudra donc songer à utiliser le constructeur de SimpleListProperty qui permet de passer en argument une valeur par défaut pour la propriété, par exemple une ObservableList<Car> vide créée grâce à la classe utilitaire FXCollections.

Par contre, tout ListChangeListener placé sur la propriété sera correctement transmis si l'instance de la liste change; ainsi :

JDK7
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
myObject.favoriteCarsProperty().addListener(new ListChangeListener<Car>() {
  @Override
  public void onChanged(ListChangeListener.Change<? extends Car> change) {
    while (change.next()) {
      for (Car removed : change.getRemoved()) {
        System.out.printf("%s removed.", removed).println();
      }
      for (Car added : change.getAddedSubList()) {
        System.out.printf("%s added.", added).println();
      }
    }
  }
});
System.out.println(myObject.getFavoriteCars());       // Liste A.
myObject.setFavoriteCars(FXCollections.<Car>observableArrayList());
myObject.getFavoriteCars().addAll(new, Car(), new, Car(), new, Car());
System.out.println(myObject.getFavoriteCars());       // Liste B.
myObject.setFavoriteCars(FXCollections.<Car>observableArrayList());
System.out.println(myObject.getFavoriteCars());       // Liste C.
JDK8
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
myObject.favoriteCarsProperty().addListener((ListChangeListener.Change<? extends Car> change) -> {
  while (change.next()) {
    change.getRemoved().forEach((Car removed) -> {
      System.out.printf("%s removed.", removed).println();
    }
    change.getAddedSubList().forEach((Car added) -> {
      System.out.printf("%s added.", added).println();
    }
  }
});
System.out.println(myObject.getFavoriteCars());       // Liste A.
myObject.setFavoriteCars(FXCollections.<Car>observableArrayList());
myObject.getFavoriteCars().addAll(new, Car(), new, Car(), new, Car());
System.out.println(myObject.getFavoriteCars());       // Liste B.
myObject.setFavoriteCars(FXCollections.<Car>observableArrayList());
System.out.println(myObject.getFavoriteCars());       // Liste C.

Produira une sortie similaire à :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
null                                                                     // Liste A.
test.Main$Car@6046f6e4 added.
test.Main$Car@52fb241d added.
test.Main$Car@6600167a added.
[test.Main$Car@6046f6e4, test.Main$Car@52fb241d, test.Main$Car@6600167a] // Liste B.
test.Main$Car@6046f6e4 removed.
test.Main$Car@52fb241d removed.
test.Main$Car@6600167a removed.
[]                                                                       // Liste C.

On voit donc bien que l'écouteur placé sur la propriété alors que la liste était à une valeur null (liste A) est réutilisé sur la liste B et C et que le contenu de la liste est supprimé de la propriété lorsqu'on remplace la liste B (pleine) par la liste C (vide).

IV-B-2. Tables observables

  • javafx.beans.property.MapProperty<K, V> - permet de stocker des ObservableMap<K, V>. Ici, K est le type des clés et V est le type des valeurs contenues dans la table observable.

Même si elles sont moins répandues dans l'API, de la même manière que pour les listes observables, les tables observables, ObservableMap, nécessitent l'utilisation d'une classe de stockage appropriée: MapProperty qui permet la gestion automatique des MapChangelistener. Ainsi, une propriété qui contient les voitures par plaque d'immatriculation serait définie comme :

 
Sélectionnez
1.
private final MapProperty<String, Car> carMap = new SimpleMapProperty<>();

IV-B-3. Ensembles observables

  • javafx.beans.property.SetProperty<V> - permet de stocker des ObservableSet<V>. Ici V est le type des objets contenus dans l'ensemble observable.

À l'identique, les ensembles observables, ObservableSet peuvent être stockés dans des SetProperty avec une gestion automatique des SetChangeListener.

 
Sélectionnez
1.
private final SetProperty<String> namePool = new SimpleSetProperty<>();

IV-C. Et les Java Beans ?

L'API fournit tout un tas de classes prévues pour transformer les propriétés des Java Beans en propriétés JavaFX :

  • javafx.beans.property.adapter.JavaBeanBooleanProperty - permet d'empaqueter une propriété contenant des valeurs booléennes ;
  • javafx.beans.property.adapter.JavaBeanIntegerProperty - permet d'empaqueter une propriété contenant des valeurs entières ;
  • javafx.beans.property.adapter.JavaBeanLongProperty - permet d'empaqueter une propriété contenant des valeurs longues ;
  • javafx.beans.property.adapter.JavaBeanFloatProperty - permet d'empaqueter une propriété contenant des valeurs flottantes ;
  • javafx.beans.property.adapter.JavaBeanDoubleProperty - permet d'empaqueter une propriété contenant des valeurs flottantes en double précision ;
  • javafx.beans.property.adapter.JavaBeanStringProperty - permet d'empaqueter une propriété contenant des valeurs textuelles ;
  • javafx.beans.property.adapter.JavaBeanObjectProperty<T> - permet d'empaqueter une propriété contenant des valeurs de type objet.

Il n'est pas possible d'instancier directement une instance de ces types, il faut passer par des builders appropriés pour chaque type. Attention des exceptions peuvent être levées lors de la construction entre autres si les getters et les setters ne sont pas trouvés sur le bean (si les conventions Java Bean n'ont pas été respectées par exemple). Cependant le builder est assez flexible pour accepter des références sur des méthodes en paramètre si besoin est.

Supposons que nous ayons un bean très simple dans le genre :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
public class Person {

  private int age = 30;

  public int getAge() {
    return age;
  }

  public void setAge(int age) {
    this.age = age;
  }
}

Nous allons empaqueter (wrapper) sa propriété age comme suit :

 
Sélectionnez
1.
2.
3.
4.
Person person = new Person();
JavaBeanIntegerProperty personAge = JavaBeanIntegerPropertyBuilder.create()
                                    .bean(person).name("age")
                                    .build();

Profitons-en pour mettre un InvalidationListener et un ChangeListener sur la propriété et faisons quelques tests :

JDK7
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
personAge.addListener(new InvalidationListener() {

  @Override
  public void invalidated(Observable o) {
    System.out.println("age invalid.");
  }
});
personAge.addListener(new ChangeListener<Number>(){

  @Override
  public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
    System.out.printf("age %d -> %d", oldValue, newValue).println();
  }                
});
System.out.println(personAge.get());
// Changement direct sur le bean.
person.setAge(31); 
System.out.println(personAge.get());
// Changement direct sur la propriete.
personAge.set(32);
System.out.println(personAge.get());
System.out.println(person.getAge());
JDK8
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
personAge.addListener((Observable o) -> {
  System.out.println("age invalid.");
});
personAge.addListener((ObservableValue<? extends Number> observable, Number oldValue, Number newValue) -> {
  System.out.printf("age %d -> %d", oldValue, newValue).println();
});
System.out.println(personAge.get());
// Changement direct sur le bean.
person.setAge(31); 
System.out.println(personAge.get());
// Changement direct sur la propriete.
personAge.set(32);
System.out.println(personAge.get());
System.out.println(person.getAge());

Cela nous donne :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
30
31
age invalid.
age 30 -> 32
32
32

Comme nous pouvons le voir, il n'y a pas de miracles : les changements opérés directement sur le bean ne provoquent pas la levée d'événements par la propriété JavaFX. Rien dans la classe du bean ne permet en effet de suivre les changements de valeur de la propriété age. Par contre, les changements opérés sur la propriété JavaFX provoquent la levée d'événements d'invalidation et de modification ainsi que la répercussion du changement de valeur vers le bean.

Reprenons notre bean, et écrivons-le correctement cette fois; c'est-à-dire avec un support pour accepter des écouteurs de type PropertyChangeListener, ce qui est la manière Java Bean de notifier des modifications de propriétés :

 
Sé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 Person {
  
  private int age = 30;
  
  public int getAge() {
      return age;
  }
  
  public void setAge(int age) {
      int oldAge = this.age;
      this.age = age;
      listenerList.firePropertyChange("age", oldAge, age);
  }
  private PropertyChangeSupport listenerList = new PropertyChangeSupport(this);
  
  public void addPropertyChangeListener(PropertyChangeListener listener) {
      listenerList.addPropertyChangeListener(listener);
  }
  
  public void removePropertyChangeListener(PropertyChangeListener listener) {
      listenerList.removePropertyChangeListener(listener);
  }
  
  public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) {
      listenerList.addPropertyChangeListener(propertyName, listener);
  }
  
  public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) {
      listenerList.removePropertyChangeListener(propertyName, listener);
  }
}

Et lors de son exécution, maintenant notre test nous retourne :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
30
age invalid.
age 30 -> 31
31
age invalid.
age 31 -> 32
age invalid.
32
32

Désormais, les événements de changement de valeur de la propriété du bean sont correctement répercutés vers la propriété JavaFX et provoquent l'appel des écouteurs qui sont placés dessus. L'empaquetage des propriétés d'un bean Java fonctionne donc parfaitement, pour peu que le programmeur ait fait l'effort de rendre ses propriétés observables dans le bean lui-même en programmant toute la tuyauterie nécessaire !

V. Utilisation avancée

V-A. Propriétés en lecture seule

Pour créer une propriété en lecture seule, le moyen le plus simple est de commencer par supprimer le setter ou par le rendre inaccessible depuis l'extérieur de la classe. Vous pouvez cependant en conserver un private ou protected selon les besoins. Il faut ensuite transformer le type de retour de la méthode d'accès à la propriété pour ce que type retourne ne permette pas les modifications.

V-A-1. Cacher le type réel

On serait tenté d'écrire tout simplement le code suivantqui ne causera aucun souci à la compilation et fonctionnera parfaitement :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
// Propriété errorCount.
private final IntegerProperty errorCount = new SimpleIntegerProperty();
    
// Getter.
public final int getErrorCount() {
  return errorCount.get();
}

// Accès à la propriété.
public final ReadOnlyIntegerProperty errorCountProperty() {
  return errorCount;
}

Cependant, ce n'est pas du tout recommandé puisque n'importe qui peut alors caster la propriété retournée dans un type qui accepte l'écriture et ainsi casser la protection.

 
Sélectionnez
1.
(IntegerProperty)(errorReport.errorCountProperty()).set(0);

V-A-2. Utiliser des wrappers

Dans l'API, tous les types Simple présentés précédemment exposent des classes filles permettant d'empaqueter des valeursfacilement et de créer ensuite des propriétés en lecture seule :

  • javafx.beans.property.ReadOnlyBooleanWrapper - pour les valeurs booléennes ;
  • javafx.beans.property.ReadOnlyIntegerWrapper - pour les valeurs entières ;
  • javafx.beans.property.ReadOnlyLongWrapper - pour les valeurs longues ;
  • javafx.beans.property.ReadOnlyFloatWrapper - pour les valeurs flottantes ;
  • javafx.beans.property.ReadOnlyDoubleWrapper - pour les valeurs flottantes en double précision ;
  • javafx.beans.property.ReadOnlyObjectWrapper<T> - pour les objets de manière générale ;
  • javafx.beans.property.ReadOnlyListWrapper<V> - pour les listes observables ;
  • javafx.beans.property.ReadOnlyMapWrapper<K, V> - pour les tables observables ;
  • javafx.beans.property.ReadOnlySetWrapper<V> - pour les ensembles observables.

Il est donc plus recommandé d'utiliser un wrapperpour rendre la propriété non modifiable :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
// Propriété errorCount.
private final ReadOnlyIntegerWrapper errorCount = new ReadOnlyIntegerWrapper();
    
// Getter.
public final int getErrorCount() {
  return errorCount.get();
}

// Accès à la propriété.
public final ReadOnlyIntegerProperty errorCountProperty() {
  return errorCount.getReadOnlyProperty();
}

L'appel à la méthode getReadOnlyProperty() de la propriété produit un nouvel objet sur lequel on ne peut pas appeler les méthodes set() ou setValue(). Cette méthode a cependant le défaut de créer au moins deux propriétés qui sont synchronisées entre elles: la propriété interne qui est modifiable et une ou plusieurs propriétés publiques qui ne sont pas modifiables; elle est donc plus gourmande en mémoire.

V-A-3. Étendre le type de base

Une autre méthode consiste à étendre le type de base de la propriété, ici ReadOnlyIntegerPropertyBase.

 
Sé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.
private class MyReadOnlyIntegerProperty extends ReadOnlyIntegerPropertyBase {

  private int value;
  private String name;

  public MyReadOnlyIntegerProperty(String name) {
    this.name = name;
  }

  @Override
  public final int get() {
    return value;
  }

  private void set(int newValue) {
    value = newValue;
    fireValueChangedEvent();
 }

 @Override
 public Object getBean() {
   return <Classe parente>.this;
 }

 @Override
 public String getName() {
   return name;
 }
}

// Propriété errorCount.
private final MyReadOnlyIntegerProperty errorCount = new MyReadOnlyIntegerProperty("errorCount");
    
// Getter.
public final int getErrorCount() {
  return errorCount.get();
}

// Accès à la propriété.
public final ReadOnlyIntegerProperty errorCountProperty() {
  return errorCount;
}

Cette manière de faire permet de produire un résultat totalement sûr: la propriété n'offre aucun accès public à la méthode set(). Elle a par contre le gros désavantage d'obliger à redéfinir manuellement tous les types de toutes les propriétés utilisées dans votre code, ce qui alourdit considérablement la tâche du programmeur.

V-B. Restriction des valeurs

Il peut arriver qu'on soit obligé de mettre des restrictions sur les valeurs d'entrée d'une propriété. Dans ce cas, il est plus aisé de faire ces tests et validations de valeur saisie dans le setter de la propriété, pour, par exemple, faire des tests de sécurité, corriger la valeur saisie par l'utilisateur ou au besoin lever une exception. Cependant, dans ce cas, il est important de faire en sorte que l'accesseur de la propriété retourne une propriété en lecture seule. Si ce n'était pas le cas, il serait possible de contourner la validation effectuée dans le setter par un appel direct aux méthodes set() ou setValue() de la propriété.

On peut de nouveau utiliser un wrapper pour mettre en œuvre cette solution :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
// Propriété chapterCount.
private final ReadOnlyIntegerWrapper chapterCount = new ReadOnlyIntegerWrapper();

// Getter.
public final int getChapterCount() {
  return chapterCount.get();
}

// Setter.
public final void setChapterCount(int value) {
  // Correction de la valeur saisie.
  chapterCount.set(value < 0 ? 0 : value);
}

// Accès à la propriété... en lecture seule.
public final ReadOnlyIntegerProperty chapterCountProperty() {
  return chapterCount.getReadOnlyProperty();
}

V-C. Optimisation

Un défaut important de l'utilisation des propriétés est qu'elles consomment plus de mémoire que de simples valeurs décrites dans l'objet. Si dans la majorité des cas, ce n'est pas un souci, cela peut s'avérer gênant lors de l'utilisation d'objets qui contiennent plusieurs dizaines de propriétés, comme, par exemple, les nœuds de l'API graphique SceneGraph.

V-C-1. Initialisation différée

Pour pallier ce problème, en interne, l'API JavaFX utilise des initialisations différées : les références des propriétés sont initialement à null et les getters retournent des valeurs par défaut. Seuls les accès au setter et à l'accesseur initialisent la propriété à sa vraie valeur.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
// Propriété background.
private ObjectProperty<Color> background = null;

// Getter.
public final Color getBackground() {
  return (background == null) ? Color.BLACK : background.get();
}

// Setter.
public final void setBackground(Color value) {
  backgroundProperty().set(value);
}

// Accès à la propriété.
public final ObjectProperty<Color> backgroundProperty() {
  if (background == null) {
    background = new SimpleObjectProperty<>(this, "background", Color.BLACK);
  }
  return background;
}

V-C-2. Externalisation des propriétés

Les propriétés les moins utilisées peuvent être regroupées et externalisées dans une classe de support à l'extérieur de l'objet. Ce dernier n'a plus alors qu'à initialiser une référence vers cette classe de support quand un accès est fait à une de ces propriétés. Cela permet de réduire dans notre classe principale la quantité de code à écrire pour ces propriétés et de n'avoir également à maintenir qu'une seule référence : celle vers l'objet de support.

L'API SceneGraph utilise également cette méthode d'optimisation pour les propriétés les moins utilisées des nœuds ou encore la plupart des propriétés liées au positionnement dans la 3e dimension puisque les nœuds sont principalement utilisés dans des interfaces graphiques traditionnelles en 2D.

V-C-3. Gestion des liaisons

Toute liaison sur une propriété, que ce soit le fait d'y attacher un écouteur ou d'effectuer une liaison dessus grâce au binding peut empêcher le garbage collector de réclamer l'espace mémoire occupé par cet objet. De ce fait, il est recommandé de désinstaller les InvalidationListener et ChangeListener qui ont été enregistrés sur la propriété et de casser les liaisons en appelant les méthodes unbind() ou unbindBidirectionnal() suivant le type de binding utilisé.

V-C-3-a. Classes anonymes

Nous conservons une référence sur l'instance de notre classe anonyme pour permettre de la désinstaller quand la liaison n'est plus nécessaire :

JDK7
Sé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.
private final IntegerProperty myErrorCount = new SimpleIntegerProperty();

// Classe anonyme implémentant le listener.
private ChangeListener<Number> errorCountChangeListener = new ChangeListener<Number>() {
  @Override
  public void change(ObservableValue<? Extends Number> observableValue, Number oldValue, Number newValue) {
    [...]
  }
};

private final IntegerProperty myChapterCount = new SimpleIntegerProperty();

[...]

// Installation du listener.
anObject.errorCountProperty().addListener(errorCountChangeListener);
// Binding.
myChapterCount.bind(anObject.chapterCountProperty());

[...]

//Lorsqu'on veut libérer la référence :
// Désinstallation du listener.
anObject.errorCountProperty().removeListener(errorCountChangeListener);
errorCountChangeListener = null;
// Suppression du binding.
myChapterCount.unbind();
V-C-3-b. Lambda

Le JDK8 permet de faire la même chose avec des expressions lambda :

JDK8 avec lambda
Sé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.
private final IntegerProperty myErrorCount = new SimpleIntegerProperty();

// Lambda implémentant le listener.
private ChangeListener<Number> errorCountChangeListener = (ObservableValue<? Extends Number> observableValue, Number oldValue, Number newValue) -> {
  [...]
};

private final IntegerProperty myChapterCount = new SimpleIntegerProperty();

[...]

// Installation du listener.
anObject.errorCountProperty().addListener(errorCountChangeListener);
// Binding.
myChapterCount.bind(anObject.chapterCountProperty());

[...]

//Lorsqu'on veut libérer la référence :
// Désinstallation du listener.
anObject.errorCountProperty().removeListener(errorCountChangeListener);
errorCountChangeListener = null;
// Suppression du binding.
myChapterCount.unbind();
V-C-3-c. Références de méthodes

En JDK 8, on peut aussi utiliser une référence de méthode :

JDK8 avec références de méthodes
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
private final IntegerProperty myErrorCount = new SimpleIntegerProperty();

// Méthode implémentant le listener.
private void impl_errorCountChangeListener(ObservableValue<? Extends Number> observableValue, Number oldValue, Number newValue) {
    [...]
}

private final IntegerProperty myChapterCount = new SimpleIntegerProperty();

[...]

// Installation du listener.
anObject.errorCountProperty().addListener(this:: impl_errorCountChangeListener);
// Binding.
myChapterCount.bind(anObject.chapterCountProperty());

[...]

// Suppression du binding.
myChapterCount.unbind();

Attention ! Bien qu'il soit possible d'appeler la méthode removeListener() en passant en paramètre un écouteur qui soit une référence de méthode, cela n'a aucun effet à la fin (avec le JDK 8 b121 actuellement disponible). Ainsi, si nous faisons :

JDK8 avec référence de méthode
Sélectionnez
1.
2.
3.
//Lorsqu'on veut libérer la référence :
// Désinstallation du listener.
anObject.errorCountProperty().removeListener(this:: impl_errorCountChangeListener);

Notre écouteur continuera malgré tout à recevoir des notifications de changement de propriété !

Pour contourner le problème, il faut éviter de référencer directement la méthode dans les méthodes addListener() et removeListener(). À la place, il convient de la stocker dans un membre de la classequi sera passé en paramètre de ces méthodescomme pour une classe anonyme ou une expression lambda :

JDK8 avec référence de méthode
Sélectionnez
private final IntegerProperty myErrorCount = new SimpleIntegerProperty();

// Méthode implémentant le listener.
private void impl_errorCountChangeListener(ObservableValue<? Extends Number> observableValue, Number oldValue, Number newValue) {
    [...]
}

// Référence vers la méthode  impl_errorCountChangeListener()
private ChangeListener<Number> errorCountChangeListener = this:: impl_errorCountChangeListener;

private final IntegerProperty myChapterCount = new SimpleIntegerProperty();

[...]

// Installation du listener.
anObject.errorCountProperty().addListener(errorCountChangeListener);
// Binding.
myChapterCount.bind(anObject.chapterCountProperty());

[...]

//Lorsqu'on veut libérer la référence :
// Désinstallation du listener.
anObject.errorCountProperty().removeListener(errorCountChangeListener);
errorCountChangeListener = null;
// Suppression du binding.
myChapterCount.unbind();

V-C-4. Liaisons faibles

Il est possible de faciliter la gestion des listeners en utilisant des WeakInvalidationListener et WeakChangeListener pour éviter de devoir appeler manuellement les méthodes d'enregistrement, mais cela implique de devoir conserver une référence au listener d'origine tant qu'on en a besoin sous peine de voir la liaison faible disparaitre si le garbage collector décide de libérer cet espace mémoire.

V-C-4-a. Classe anonyme
JDK7
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
private final IntegerProperty myErrorCount = new SimpleIntegerProperty();

// Classe anonyme implémentant le listener.
private ChangeListener<Number> errorCountChangeListener = new ChangeListener<Number>() {
  @Override
  public void change(ObservableValue<? Extends Number> observableValue, Number oldValue, Number newValue) {
    [...]
  }
};

[...]

// Installation du listener avec une référence faible.
anObject.errorCountProperty().addListener(new WeakChangeListener<>(errorCountChangeListener));

[...]

// Lorsqu'on veut libérer la référence :
errorCountChangeListener = null;
V-C-4-b. Lambda
JDK8 avec lambda
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
private final IntegerProperty myErrorCount = new SimpleIntegerProperty();

// Lambda implémentant le listener.
private ChangeListener<Number> errorCountChangeListener = (ObservableValue<? Extends Number> observableValue, Number oldValue, Number newValue) -> {
  [...]
};

[...]

// Installation du listener avec une référence faible.
anObject.errorCountProperty().addListener(new WeakChangeListener<>(errorCountChangeListener));

[...]

// Lorsqu'on veut libérer la référence :
errorCountChangeListener = null;
V-C-4-c. Référence de méthode

Il n'y a pas vraiment d'équivalent pour libérer explicitement la référence lorsqu'on utilise des références de méthode directement :

JDK8 avec références de méthode
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
private final IntegerProperty myErrorCount = new SimpleIntegerProperty();

// Méthode implémentant le listener.
private void impl_errorCountChangeListener(ObservableValue<? Extends Number> observableValue, Number oldValue, Number newValue) {
    [...]
}

[...]

// Installation du listener avec une référence faible.
anObject.errorCountProperty().addListener(new WeakChangeListener<Number>(this::impl_errorCountChangeListener));

[...]

// Il n'y a pas d'équivalent en JDK 8+références de méthode pour libérer explicitement cette référence.

Par contre, il est toujours possible d'utiliser un membre qui contient la référence à la méthode :

JDK8 avec référence de méthode
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
private final IntegerProperty myErrorCount = new SimpleIntegerProperty();

// Méthode implémentant le listener.
private void impl_errorCountChangeListener(ObservableValue<? Extends Number> observableValue, Number oldValue, Number newValue) {
    [...]
}

// Référence vers la méthode  impl_errorCountChangeListener()
private ChangeListener<Number> errorCountChangeListener =  this::impl_ errorCountChangeListener;

[...]

// Installation du listener avec une référence faible.
anObject.errorCountProperty().addListener(new WeakChangeListener<>(errorCountChangeListener));

[...]

// Lorsqu'on veut libérer la référence :
errorCountChangeListener = null;

V-D. Empaqueter une valeur d'un POJO

Pour diverses raisons, il peut être nécessaire d'empaqueter des valeurs dans un objet de type ObservableValue. Ainsi, lorsque par exemple,on utilise TableView avec des objets simples Java (POJO = Plain Old Java Object), il est nécessaire de faire une telle transformation, car TableView ne fonctionne qu'avec des instances d'ObservableValue. C'est très simple : il suffit de créer au vol des propriétés dans lesquelles on mettra les valeurs.

Supposons que nous avons une requête vers une base de données qui nous renvoie des objets sous une forme de structure toute simple :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
public classs Dog {
  public String name;
  public int age;
  
  public Dog(String name, int age) {
    this.name = name;
    this.age = age;
  }
}

et que nous souhaitons afficher les résultats de cette requête dans une TableView<Dog> contenant une colonne pour le nom et une pour l'âge.

Comme c'est un objet Java simple, sans propriétés JavaFX, TableView n'a absolument aucune idée de comment trouver les valeurs à afficher dans ses lignes pour chaque colonne. Pour chacune des colonnes, il va donc falloir spécifier une CellValueFactory qui, à partir d'un objet de type Dog présent sur une ligne donnée, va nous retourner un ObservableValue du type approprié pour cette colonne :

JDK7
Sé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.
// Colonne name.
TableColumn<Dog, String> nameColumn = TableColumnBuilder.<Dog, String>create().text("Name").build();
// Permet à la colonne name de récupérer la valeur dans l'objet.
nameColumn.setCellValueFactory(new Callback<CellDataFeatures<Dog, String>, ObservableValue<String>>(){

  @Override
  public  ObservableValue<String> call(CellDataFeatures<Dog, String> feature) {
    Dog rintintin = feature.getValue();
    return new SimpleStringProperty(rintintin.name);
  }
});
// Colonne age.
TableColumn<Dog, Integer> ageColumn = TableColumnBuilder.<Dog, Integer>create().text("Age").build();
// Permet à la colonne age de récupérer la valeur dans l'objet.
ageColumn.setCellValueFactory(new Callback<CellDataFeatures<Dog, Integer>, ObservableValue<Integer>>(){

  @Override
  public  ObservableValue<Integer> call(CellDataFeatures<Dog, Integer> feature) {
    Dog rintintin = feature.getValue();
    return new SimpleObjectProperty<>(rintintin.age);
  }
});
// Initialisation de la table.
ObservableList<Dog> doggies = FXCollections.observableArrayList(new Dog("Rex", 3), new Dog("Lassie", 7));
TableView<Dog> tableView = new TableView<Dog>();
tableView.getColumns().setAll(nameColumn, ageColumn);
tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
tableView.setItems(doggies);
JDK8
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
// Colonne name.
// Les builders sont dépréciés dans le JDK8 et disparaitront dans le JDK9.
TableColumn<Dog, String> nameColumn = new TableColumn<>("Name");
// Permet à la colonne name de récupérer la valeur dans l'objet.
nameColumn.setCellValueFactory((CellDataFeatures<Dog, String> feature) -> {
  Dog rintintin = feature.getValue();
  return new SimpleStringProperty(rintintin.name);
});
// Colonne age.
TableColumn<Dog, Integer> ageColumn = new TableColumn<>("Age");
// Permet à la colonne age de récupérer la valeur dans l'objet.
ageColumn.setCellValueFactory((CellDataFeatures<Dog, Integer> feature) -> {
  Dog rintintin = feature.getValue();
  return new SimpleObjectProperty<>(rintintin.age);
});
// Initialisation de la table.
ObservableList<Dog> doggies = FXCollections.observableArrayList(new Dog("Rex", 3), new Dog("Lassie", 7));
TableView<Dog> tableView = new TableView<Dog>();
tableView.getColumns().setAll(nameColumn, ageColumn);
tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
tableView.setItems(doggies);

On peut remarquer ici que j'ai utilisé un SimpleObjectProperty<Integer> au lieu d'un SimpleIntegerProperty. C'est malheureusement nécessaire puisque, comme indiqué plus haut, SimpleIntegerProperty hérite de l'ObservableValue<Number> au lieu de l'ObservableValue<Integer>.

Et voilà c'est magique, la TableView va pouvoir afficher les bonnes informations. Cela marcherait également pour un objet plus complexe comme une valeur retirée en appelant un getter sur un JavaBean.

Image non disponible
Figure 2 - La table affiche les valeurs en provenance de nos objets.

VI. Conclusion

Voilà, nous avons fait un rapide tour pour poser les bases de l'écriture des propriétés en JavaFX. Nous avons vu que, dans la plupart des cas, la mise en place est plus rapide puisque ne nécessitant pas de devoir manuellement écrire toute la pile événementielle destinée à propager les changements de propriété.

VII. Liens

VIII. Remerciements

Je tiens à remercier toute l'équipe du forum Développez ainsi que Mickael Baron pour ses suggestions et sa relecture du présent article. Je tiens également à remercier Philippe Duval et Malick Seck pour leurs corrections orthographiques.

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 © 2013 Fabrice Bouyé. Aucune reproduction, même partielle, ne peut être faite de ce site ni 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.