Tutoriel sur les extracteurs en JavaFX

Comment recevoir des notifications de mise à jour des éléments contenus dans des listes observables

Cet article a pour but de vous expliquer comment utiliser des extracteurs et recevoir des notifications de mise à jour provenant des éléments qui sont contenus dans une liste observable.

Article lu   fois.

L'auteur

Profil ProSite personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Introduction

Les listes observables sont partout présentes dans JavaFX ; elles sont littéralement à la base du concept du SceneGraph qui permet d'afficher des nœuds graphique dans une interface utilisateur. En effet chaque nœud parents, groupe ou gestionnaire de mise en page dispose d'une liste observable de nœuds enfants. Au niveau des contrôles, ces listes sont également utilisées par les boites déroulantes et autres listes, arbres et tables graphiques.

De base, ces listes observables permettent de recevoir, via des écouteurs de type ListChangeListener, des événements de notification classiques telles que d'ajout, retrait, remplacement ou encore permutation des éléments contenus dans une de ces listes. Bref, tout ce qui se rapporte aux manipulations classiques d'une liste.

Cependant, il existe un autre type de notification : la notification de mise à jour qui permet d'être averti lorsque la propriété observable d'un élément de la liste est modifiée. Cette fonctionnalité peu connue permet d'être averti que l'état d'un des objets de la liste a changé ! Elle nécessite l'utilisation d'un nouveau concept : l'extracteur de propriétés.

II. Utilisation d'une liste observable

Nous allons effectuer un rapide rafraichissement sur la manière d'utiliser les listes observables. Contrairement aux listes Java classiques qui sont muettes lorsque leur contenu est modifié, une ObservableList de JavaFX émettre des évènements de type ListChangeListener.Change qui peuvent être reçus par un écouteur de type ListChangeListener. Cet événement peut avoir plusieurs états :

Type Description La taille de la liste change ? Les indices de la liste changent ? Méthodes
Added Ajout d'un élément dans la liste Oui Oui, décalage vers la droite à partir de l'indice d'ajout add();
Removed Suppression d'un élément dans la liste Oui Oui, décalage vers la gauche à partir de l'indice de suppression remove();
Replaced Remplacement d'un élément dans la liste par une valeur externe Non Non set();
Permutated Permutation de deux éléments dans la liste Non Non, les indices des deux valeurs sont cependant permutés sort();
Updated Mise à jour d'une propriété d'un élément de la liste Non Non n/a

À noter qu'un événement de replacement peut être vu comme une composition d'un retrait suivi d'un ajout. Tous les types d'événements, sauf celui de mise à jour, impliquent l'utilisation de méthodes définies dans l'interface java.util.List.

II-A. Modèle

Commençons par créer le type d'objets que nous allons stocker dans la liste observable. Nous allons créer une classe Person qui peut, par exemple, représenter un utilisateur tel que stocké dans une base de données. Cette personne disposera de deux propriétés observables, son nom (name) et son prénom (surname) que nous pourrons éditer dans une interface graphique.

 
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.
51.
52.
53.
54.
public final class Person {

    public Person(final String name, final String surname) {
        setName(name);
        setSurname(surname);
    }

    private final StringProperty name = new SimpleStringProperty(this, "name"); // NOI18N.

    public final String getName() {
        return name.get();
    }

    public final void setName(final String value) {
        name.set(value);
    }

    public final StringProperty nameProperty() {
        return name;
    }

    private final StringProperty surname = new SimpleStringProperty(this, "surname"); // NOI18N.

    public final String getSurname() {
        return surname.get();
    }

    public final void setSurname(final String value) {
        surname.set(value);
    }

    public final StringProperty surnameProperty() {
        return surname;
    }

    @Override
    public String toString() {
        String name = getName();
        name = (name == null) ? "" : name.trim(); // NOI18N.
        String surname = getSurname();
        surname = (surname == null) ? "" : surname.trim(); // NOI18N.
        final StringBuilder builder = new StringBuilder();
        if (!name.isEmpty()) {
            builder.append(name.trim());
        }
        if (!name.isEmpty() && !surname.isEmpty()) {
            builder.append(" ");
        }
        if (!surname.isEmpty()) {
            builder.append(surname.trim());
        }
        return builder.toString();
    }
}

Note : la méthode toString() est ici uniquement destinée à aider le débogage en faisant des affichages en mode console.

II-B. Création de la liste

Il nous est alors possible d'instancier puis d'ajouter des personnes dans une liste observable, d'installer un écouteur sur cette liste et puis de faire quelques tests sur son contenu :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
// Création de la liste.
final ObservableList<Person> persons = FXCollections.observableArrayList(
    new Person("Dupond", "Valérie"), // NOI18N.
    new Person("Higgins", "Clark"), // NOI18N.
    new Person("Pantou", "Maurice"), // NOI18N.
    new Person("Parmentier", "Yvette")); // NOI18N.

Ou encore :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
// Création de la liste.
final ObservableList<Person> persons = FXCollections.observableArrayList();
persons.setAll(
    new Person("Dupond", "Valérie"), // NOI18N.
    new Person("Higgins", "Clark"), // NOI18N.
    new Person("Pantou", "Maurice"), // NOI18N.
    new Person("Parmentier", "Yvette")); // NOI18N.

II-C. Écouteur

Nous connectons ensuite sur la liste un écouteur de type ListChangeListener<Person> et nous traitons l'événement de modification reçu :

 
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.
// Écouteur.
persons.addListener(new ListChangeListener<Person>() {
    @Override
    public void onChanged(ListChangeListener.Change<? extends Person> change) {
       // Rappel : plusieurs modifications peuvent être agrégées dans un seul événement.
        while (change.next()) {
            String changeLabel = "?"; // NOI18N.
            if (change.wasReplaced()) {
                changeLabel = "replaced"; // NOI18N.
            } else if (change.wasAdded()) {
                changeLabel = "added"; // NOI18N.
            } else if (change.wasRemoved()) {
                changeLabel = "removed"; // NOI18N.
            } else if (change.wasPermutated()) {
                changeLabel = "permutated"; // NOI18N.
            } else if (change.wasUpdated()) {
                changeLabel = "updated"; // NOI18N.
            }
            final String pattern = String.format("Element %s was %s%n", "%d", changeLabel); // NOI18N.
            if (change.wasAdded() || change.wasReplaced() || change.wasUpdated()) {
                // Parcours exclusif.
                IntStream.range(change.getFrom(), change.getTo())
                        .forEach(index -> System.out.printf(pattern, index));
            } else {
                // Parcours inclusif.
                IntStream.rangeClosed(change.getFrom(), change.getTo())
                        .forEach(index -> System.out.printf(pattern, index));
            }
        }
    }
});

II-D. Test

Nous pouvons ensuite tester les différents types d'opération :

 
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.
// Opérations.
System.out.println(persons);
// Retrait.
System.out.println("== Remove ==");
persons.remove(0);
System.out.println(persons);
// Ajout.
System.out.println("== Add ==");
persons.add(0, new Person("Dupond", "Valérie")); // NOI18N.
System.out.println(persons);
// Remplacement.
System.out.println("== Replace ==");

persons.set(0, new Person("Langworth", "Cathy")); // NOI18N.
System.out.println(persons);
// Tri -> permutation.
System.out.println("== Permute ==");
persons.sort((p1, p2) -> {
    int result = p1.getName().compareTo(p2.getName());
    if (result == 0) {
        result = p1.getSurname().compareTo(p2.getSurname());
    }
    return result;
});
System.out.println(persons);
// Mise à jour.
System.out.println("== Update ==");
persons.get(0).setName("Smith"); // NOI18N.
persons.get(0).setSurname("Anny"); // NOI18N.
System.out.println(persons);

Ce qui nous permet d'obtenir le résultat suivant :

 
Sélectionnez
[Dupond Valérie, Higgins Clark, Pantou Maurice, Parmentier Yvette]
== Remove ==
Element 0 was removed
[Higgins Clark, Pantou Maurice, Parmentier Yvette]
== Add ==
Element 0 was added
[Dupond Valérie, Higgins Clark, Pantou Maurice, Parmentier Yvette]
== Replace ==
Element 0 was replaced
[Langworth Cathy, Higgins Clark, Pantou Maurice, Parmentier Yvette]
== Permute ==
Element 0 was permutated
Element 1 was permutated
Element 2 was permutated
Element 3 was permutated
Element 4 was permutated
[Higgins Clark, Langworth Cathy, Pantou Maurice, Parmentier Yvette]
== Update ==
[Smith Anny, Langworth Cathy, Pantou Maurice, Parmentier Yvette]

Nous voyons bien que tous les types de modifications ont été répercutés sauf l'événement de mise à jour.

III. Suivi des mises à jour

Le suivi des évènements de mise à jour est en effet optionnel, comme spécifié dans la documentation de la classe ListChangeListener.Change, et la plupart des listes observables ne permettent pas de l'effectuer.

III-A. Création de la liste

Nous allons donc changer la manière dont nous créons une instance de liste observable. Nous allons cette fois-ci utiliser la variante de la méthode de fabrique observableArrayList()(1) de la classe utilitaire FXCollections qui prend en paramètre un extracteur de valeurs observables… c'est-à-dire un objet qui pour chaque valeur contenue dans la liste va retourner ses propriétés d'intérêt.

III-B. Extracteur

Dans notre cas, le type de l'extracteur est Callback<Person, Observable[]> : un objet dont nous devons surcharger la méthode call(), qui prend une instance de la classe Person en paramètre tandis qu'elle retournera un tableau des valeurs observables qui ne seront autres que les propriétés intéressantes de notre classe Person, ses propriétés name et surmane.

Code JDK7
Sélectionnez
1.
2.
3.
4.
5.
6.
final ObservableList<Person> items = FXCollections.observableArrayList(new Callback<Person, Observable[]>() {
    @Override
    public Observable[] call(final Person person) {
        return new ObservableValue[]{person.nameProperty(), person.surnameProperty()};
    }
});

Ce que nous pouvons, bien évidemment, grandement simplifier en écrivant une expression lambda avec le JDK8 : une simple fonction qui prend une personne en paramètre et sort un tableau contenant ses propriétés observables en retour !

Code JDK8
Sélectionnez
final ObservableList<Person> persons = FXCollections.observableArrayList(person -> new ObservableValue[]{person.nameProperty(), person.surnameProperty()});

Lorsqu'une nouvelle personne est ajoutée dans la liste observable, cette dernière va invoquer l'extracteur qui va retourner les propriétés name et surname de la personne. La liste créera alors le câblage nécessaire pour surveiller l'état de ces propriétés et propagera leurs modifications de valeur si besoin. L'extracteur sera utilisé de manière similaire lorsque l'élément est retiré de la liste pour supprimer toute trace des écouteurs des propriétés (et éviter ainsi une fuite mémoire).

III-C. Peuplement

La liste ainsi créée étant initialement vide, nous devons ensuite la peupler avec son contenu :

 
Sélectionnez
persons.setAll(
        new Person("Dupond", "Valérie"), // NOI18N.
        new Person("Higgins", "Clark"), // NOI18N.
        new Person("Pantou", "Maurice"), // NOI18N.
        new Person("Parmentier", "Yvette")); // NOI18N.

III-D. Test

Voilà, nous sommes désormais fin prêts pour effectuer à nouveau nos tests. Les tests de retrait, d'ajout, remplacement et permutation produisent bien sur les même résultats mais lorsque nous arrivons aux tests de mise à jour :

 
Sélectionnez
persons.get(0).setName("Smith"); // NOI18N.
persons.get(0).setSurname("Anny"); // NOI18N.
System.out.println(persons);

Cette fois-ci la sortie montre bien que nous recevons des événements de modification sur l'élément à l'indice 0.

 
Sélectionnez
Element 0 was updated
Element 0 was updated
[Smith Anny, Langworth Cathy, Pantou Maurice, Parmentier Yvette]

Ici, nous n'avons pas modifié le contenu de la liste, nous avons modifié les valeurs des propriétés d'un des objets de la liste ce qui a provoqué des événements de mise à jour pour cet indice.

Et si nous modifions les propriétés de deux éléments distincts :

 
Sélectionnez
persons.get(1).setSurname("Andy"); // NOI18N.
persons.get(2).setName("Cervantes"); // NOI18N.
System.out.println(persons);

Nous avons alors des notifications pour chacun des deux éléments impactés :

 
Sélectionnez
Element 1 was updated
Element 2 was updated
[Smith Anny, Langworth Andy, Cervantes Maurice, Parmentier Yvette]

IV. Exemple

Nous allons maintenant créer une petite interface de saisie pour nous permettre d'éditer les personnes. Nous allons conserver l'exemple simple mais cela devrait vous donner une idée de ce qu'il est possible de faire avec les extracteurs. Il est tout à fait possible de faire sans extracteur mais cela demande bien plus de code, d'écouteurs ou de binding sur des propriétés.

Cette interface de test se composera de 3 parties :

Image non disponible
  • À gauche, une table affiche une vue détaille des utilisateurs. Un éditeur est placé sous elle et permet d'ajouter, supprimer ou de modifier un utilisateur.
  • Au centre, une liste se contente d'afficher les utilisateurs.
  • Enfin à droite nous avons un affichage du résumé des derniers traitements effectués sur nos utilisateurs.

IV-A. Éditeur

Commençons par initialiser la table graphique :

 
Sélectionnez
// Colonne nom.
final TableColumn<Person, String> nameColumn = new TableColumn<>("Nom"); // NOI18N.
nameColumn.setCellValueFactory(feature -> feature.getValue().nameProperty());
// Ou :
//nameColumn.setCellValueFactory(new PropertyValueFactory<>("name")); // NOI18N.
// Colonne prénom.
final TableColumn<Person, String> surnameColumn = new TableColumn<>("Prénom"); // NOI18N.
surnameColumn.setCellValueFactory(feature -> feature.getValue().surnameProperty());
// Ou :
//surnameColumn.setCellValueFactory(new PropertyValueFactory<>("surname")); // NOI18N.
// Table.
final TableView<Person> personTableView = new TableView<>();
personTableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
personTableView.getColumns().addAll(nameColumn, surnameColumn);
personTableView.setItems(persons);
VBox.setVgrow(personTableView, Priority.ALWAYS);

V. Conclusion

Dans cet article nous nous sommes penchés sur l'utilisation des extracteurs qui permettent d'ajouter un nouveau niveau de fonctionnalité aux listes observables : nous pouvons désormais être notifiés lorsque l'état d'un objet contenu dans une liste observable est modifié. Et ceci peut être fait sans devoir pour autant coder tout une tuyauterie complexe d'écouteurs sur chacune des propriétés observées.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   


Il est également possible d'utiliser la variante de la méthode de fabrique observableList() de la classe utilitaire FXCollections qui prend un paramètres une liste préexistante et un extracteur pour créer une telle liste observable sur une collection qui existe déjà.

  

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