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 graphiques dans une interface utilisateur. En effet chaque nœud parent, 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 tels 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. Liste observable en JavaFX▲
Nous allons effectuer un rapide rafraichissement sur la manière d’utiliser les listes observables. La classe ObservableList ainsi que les autres collections observables et les événements et classes utilitaires qui leur sont attachées font partie du module javafx.base et n’ont donc aucune dépendance graphique.
Contrairement aux listes Java classiques qui sont muettes lorsque leur contenu est modifié, une ObservableList de JavaFX émet 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.
Ces événements existent pour tenir notre programme informé de toutes modifications dans notre liste. C’est, bien sûr, très utile pour des interfaces graphiques que ce soit pour changer le contenu d’une liste déroulante ou encore pour rafraichir un graphe de scène et c’est bien pour cela que le cœur de la partie graphique de JavaFX est construit autour de ce concept. Cependant cela a aussi son utilité pour créer des programmes non graphiques pour, par exemple, réagir automatiquement à l’arrivée d’un nouveau message dans une file d’attente d’une couche business ou encore réagir au changement de statut d’un client connecté sur un serveur sans devoir tout coder à la main.
III. Utilisation normale▲
Nous allons maintenant créer un exemple simple permettant de tester les propagations d’événements lorsque nous manipulons une liste observable.
III-A. Modèle▲
Commençons par créer le type d’objets que nous allons stocker dans la liste observable. Ce même modèle nous servira également plus tard lorsque nous utiliserons des extracteurs.
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.
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.
III-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 :
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 :
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.
III-C. Écouteur▲
Nous connectons ensuite sur la liste un écouteur de type ListChangeListener<Person> et nous traitons l’événement de modification reçu :
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.
// É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.
for (int index = change.getFrom(); index < change.getTo(); index++) {
System.out.printf(pattern, index);
}
} else {
// Parcours inclusif.
for (int index = change.getFrom(); index <= change.getTo(); index++) {
System.out.printf(pattern, index);
}
}
}
}
});
Ou encore :
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.
// Écouteur.
persons.addListener (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));
}
}
});
III-D. Test▲
Nous pouvons ensuite tester les différents types d’opérations :
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.
// 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 ==");
Collections.sort(persons, new Comparator<Person>() {
@Override
public int compare(Person p1, Person 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);
Ou :
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 :
[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.
IV. 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.
IV-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.
IV-A-1. 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.
2.
3.
4.
5.
6.
final ObservableList<Person> persons = 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 !
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).
IV-A-2. Peuplement▲
La liste ainsi créée étant initialement vide, nous devons ensuite la peupler avec son contenu :
persons.setAll(
new Person("Dupond", "Valérie"), // NOI18N.
new Person("Higgins", "Clark"), // NOI18N.
new Person("Pantou", "Maurice"), // NOI18N.
new Person("Parmentier", "Yvette")); // NOI18N.IV-B. Test▲
Voilà, nous sommes désormais fins prêts pour effectuer à nouveau nos tests. Les tests de retrait, d’ajout, remplacement et permutation produisent bien sûr les mêmes résultats, mais lorsque nous arrivons aux tests de mise à jour :
2.
3.
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.
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 :
2.
3.
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 :
Element 1 was updated
Element 2 was updated
[Smith Anny, Langworth Andy, Cervantes Maurice, Parmentier Yvette]V. 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.
Note : le code suivant sera au format JDK10.
Cette interface de test se composera de trois parties :
- À gauche, une table (composant TableView) affiche une vue détaillée des utilisateurs. Un éditeur est placé sous elle et permet d’ajouter, supprimer ou de modifier un utilisateur. C’est cette table qui affichera notre liste observable, en effet, souvenez-vous qu’en JavaFX TableView est construite autour d’une liste observable d’objets et chaque colonne de la table représente une vue spécialisée de cette liste. Ainsi la colonne nom affiche une vue sur la propriété name d’un objet de type Person tandis que la colonne prénom affiche une vue sur la propriété surname de cette même classe.
- Au centre, une liste (composant ListView) se contente d’afficher les utilisateurs. Il s’agit d’une simple vue sur la liste d’objets qui sont affichés dans la table de manière à pouvoir rapidement contrôler les données.
- Enfin à droite nous avons un affichage du résumé des derniers traitements effectués sur nos utilisateurs. Le contenu de ce résumé sera modifié en fonction des événements levés par la liste observable affichée dans la table.
V-A. Montage de l’UI▲
Nous allons maintenant créer chacune des trois parties composant notre UI en commençant par la table et son éditeur. Il s’agit du genre de composants que vous serez amené à créer et utiliser dans vos propres formulaires et UI.
V-A-1. Table & éditeur▲
Commençons par initialiser la table graphique :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
// Colonne nom.
final var nameColumn = new TableColumn<Person, String>("Nom"); // NOI18N.
nameColumn.setCellValueFactory(feature -> feature.getValue().nameProperty());
// Ou :
//nameColumn.setCellValueFactory(new PropertyValueFactory<>("name")); // NOI18N.
// Colonne prénom.
final var surnameColumn = new TableColumn<Person, String>("Prénom"); // NOI18N.
surnameColumn.setCellValueFactory(feature -> feature.getValue().surnameProperty());
// Ou :
//surnameColumn.setCellValueFactory(new PropertyValueFactory<>("surname")); // NOI18N.
// Table.
final var personTableView = new TableView<Person>();
personTableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
personTableView.getColumns().addAll(nameColumn, surnameColumn);
personTableView.setItems(persons);
VBox.setVgrow(personTableView, Priority.ALWAYS);
Nous créons ensuite un éditeur permettant de créer une nouvelle personne, de changer le nom et le prénom ou encore d’effacer une personne existante. Pour rendre l’éditeur actif, nous allons placer un écouteur sur le modèle de sélection de la table, ce qui permettra de réagir lorsque l’utilisateur sélectionne une nouvelle personne avec la souris ou le clavier :
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.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
final var persons = personTableView.getItems();
// Nom.
final var nameField = new TextField();
GridPane.setConstraints(nameField, 1, 0);
GridPane.setHgrow(nameField, Priority.ALWAYS);
final var nameLabel = new Label("Nom"); // NOI18N.
nameLabel.setLabelFor(nameField);
GridPane.setConstraints(nameLabel, 0, 0);
// Prénom.
final var surnameField = new TextField();
GridPane.setConstraints(surnameField, 1, 1);
GridPane.setHgrow(surnameField, Priority.ALWAYS);
final var surnameLabel = new Label("Prénom"); // NOI18N.
surnameLabel.setLabelFor(surnameField);
GridPane.setConstraints(surnameLabel, 0, 1);
// Binding booléens.
final var hasNoName = nameField.textProperty().isEmpty();
final var hasNoSurame = surnameField.textProperty().isEmpty();
final var selectionIsNull = personTableView.getSelectionModel().selectedItemProperty().isNull();
// Ajout d'une personne.
final var addButton = new Button("Ajouter"); // NOI18N.
addButton.disableProperty().bind(hasNoName.or(hasNoSurame));
addButton.setOnAction(event -> {
final var name = nameField.getText().trim();
final var surname = surnameField.getText().trim();
final var person = new Person(name, surname);
persons.add(person);
personTableView.getSelectionModel().select(person);
});
// Mise à jour d'une personne.
final var updateButton = new Button("Modifier"); // NOI18N.
updateButton.disableProperty().bind(selectionIsNull.or(hasNoName.or(hasNoSurame)));
updateButton.setOnAction(event -> {
final var name = nameField.getText().trim();
final var surname = surnameField.getText().trim();
final var person = personTableView.getSelectionModel().getSelectedItem();
person.setName(name);
person.setSurname(surname);
});
// Suppression d'une personne.
final var removeButton = new Button("Supprimer"); // NOI18N.
removeButton.disableProperty().bind(selectionIsNull);
removeButton.setOnAction(event -> {
final int index = personTableView.getSelectionModel().getSelectedIndex();
persons.remove(index);
});
// Regroupement des boutons.
final var buttonHBox = new HBox();
buttonHBox.setSpacing(6);
buttonHBox.setAlignment(Pos.CENTER_RIGHT);
buttonHBox.getChildren().setAll(addButton, updateButton, removeButton);
GridPane.setConstraints(buttonHBox, 0, 3, 2, 1);
// Mettre à jour l'éditeur lors d'une sélection dans la table.
personTableView.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> {
// Retrait de l'ancien élément.
Optional.ofNullable(oldValue)
.ifPresent(value -> {
nameField.setText(null);
surnameField.setText(null);
});
// Mise en place du nouvel élément.
Optional.ofNullable(newValue)
.ifPresent(value -> {
nameField.setText(value.getName());
surnameField.setText(value.getSurname());
});
});
// Mise en place de la grille.
final var personEditor = new GridPane();
personEditor.setHgap(6);
personEditor.setVgap(6);
// Configuration des colonnes de la grille.
personEditor.getColumnConstraints().setAll(
IntStream.range(0, 1)
.mapToObj(column -> new ColumnConstraints())
.toArray(ColumnConstraints[]::new));
// Configuration des lignes de la grille.
personEditor.getRowConstraints().setAll(
IntStream.range(0, 2)
.mapToObj(row -> new RowConstraints())
.toArray(RowConstraints[]::new));
personEditor.getChildren().setAll(
nameLabel, nameField,
surnameLabel, surnameField,
buttonHBox);
VBox.setVgrow(personEditor, Priority.NEVER);
Et enfin nous pouvons assembler les deux contrôles dans notre UI :
2.
3.
4.
5.
// Panneau de gauche.
final var leftVBox = new VBox();
leftVBox.setSpacing(6);
leftVBox.setPadding(new Insets(6));
leftVBox.getChildren().setAll(personTableView, personEditor);
V-A-2. Liste▲
La mise en place de la liste graphique est beaucoup plus rapide, nous nous contentons de créer une ListView qui a pour source de données la liste même qui est contenue dans la table.
2.
3.
// Liste.
final var personListView = new ListView<Person>();
personListView.setItems(persons);
V-A-3. Zone de message▲
Ici, nous allons utiliser un composant TextFlow qui va nous permettre d’écrire des messages en utilisant du texte avec des mises en forme riches.
2.
3.
4.
5.
6.
final var messageFlow = new TextFlow();
AnchorPane.setTopAnchor(messageFlow, 0d);
AnchorPane.setLeftAnchor(messageFlow, 0d);
AnchorPane.setRightAnchor(messageFlow, 0d);
final var messageAnchor = new AnchorPane(messageFlow);
final var messageScroll = new ScrollPane(messageAnchor);
V-A-4. Scène▲
Et enfin nous allons monter notre UI dans la scène attachée à notre fenêtre en assemblant les trois composants :
2.
3.
4.
5.
6.
7.
// UI finale.
final var root = new SplitPane(leftVBox, personListView, messageScroll);
final var scene = new Scene(root, 600, 600);
primaryStage.setTitle("Gestion des utilisateurs"); // NOI18N.
primaryStage.setScene(scene);
primaryStage.show();
Platform.runLater(() -> root.setDividerPositions(0.33d, 0.66d));
V-B. Réaction aux modifications de la liste▲
En l’état notre UI fonctionne avec les modifications de notre éditeur qui sont reprises dans la liste graphique puisque nous avons créé une liste de données avec un extracteur. Cependant nous n’avons pas encore de retour dans l’onglet des messages. Pour régler ces soucis, nous devons mettre en place un écouteur sur la liste :
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 int added = 0;
private int removed = 0;
private int updated = 0;
[...]
persons.addListener((ListChangeListener<Person>.Change change) -> {
while (change.next()) {
int oldAdded = added;
int oldRemoved = removed;
int oldUpdated = updated;
if (change.wasPermutated()) {
} else if (change.wasReplaced()) {
} else if (change.wasAdded()) {
System.out.println("Was added.");
added++;
} else if (change.wasRemoved()) {
System.out.println("Was removed.");
removed++;
} else if (change.wasUpdated()) {
System.out.println("Was updated.");
updated++;
}
// Mise à jour du message.
updateMessage(oldAdded, oldRemoved, oldUpdated, added, removed, updated);
}
});
V-C. Publication du message▲
Lorsque la méthode updateMessage() est invoquée, nous recréons le contenu du TextFlow pour signaler les événements d’ajout, de retrait ou de modification :
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.
private void updateMessage(final int oldAdded, final int oldRemoved, final int oldUpdated, final int newAdded, final int newRemoved, final int newUpdated) {
final var bits = messageFlow.getChildren();
messageFlow.getChildren().clear();
if (newAdded > 0 || newRemoved > 0 || newUpdated > 0) {
bits.add(new Text("La liste a été modifiée :")); // NOI18N.
if (newAdded > 0) {
final var text = new Text("\n\tajout(s) : " + newAdded); // NOI18N.
if (oldAdded != newAdded) {
text.setFill(Color.RED);
}
bits.add(text);
}
if (newRemoved > 0) {
final var text = new Text("\n\tretrait(s) : " + newRemoved); // NOI18N.
if (oldRemoved != newRemoved) {
text.setFill(Color.RED);
}
bits.add(text);
}
if (newUpdated > 0) {
final var text = new Text("\n\tmodification(s) : " + newUpdated); // NOI18N.
if (oldUpdated != newUpdated) {
text.setFill(Color.RED);
}
bits.add(text);
}
}
}
VI. Code source▲
Le code source de cet article est disponible sur GitHub.com à l’URL https://github.com/fabricebouye/dvp-collection-extractor-javafx. Vous trouverez à cette adresse des projets pour l’IDE JetBrain Intellij IDEA. Certains de ces projets sont disponibles au format JDK7, JDK 8 ou JDK 10.
VII. 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 toute une tuyauterie complexe d’écouteurs sur chacune des propriétés observées.
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 Claude Leloup pour sa correction orthographique.








