Tutoriel pour le développement d'un client desktop en JavaFX - communication entre composants (partie 2)

Gérald Colin

Dans ce tutoriel, nous continuons le développement un client « lourd » en JavaFX. À titre d’exemple, nous développerons un logiciel de gestion de portefeuille de titres financiers.

Vu le vaste sujet, nous découperons ce guide en plusieurs parties et aborderons ici différents modes de communication entre composants.

Vous pouvez retrouver la première partie ici.

Bonne lecture !

Commentez

- Titre : Tutoriel pour le développement d'un client desktop en JavaFX - communication entre composants (partie 2)

- Auteur : Gérald Colin

- Parution : 13 décembre 2019

- Mise à jour : 13 décembre 2019

- Public visé :

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

Sommaire

- I. Introduction - partie 2

- II. Communication par liaison simple

- II-A. Communication entre la vue et le contrôleur

- II-B. Note sur l’affichage du numéro de build et du numéro de version avec Maven

- III. Communication par événements internes

- IV. Communication par événements entre composant

- IV-A. Préparation cosmétique de la barre de navigation latérale

- IV-B. Traitement des événements par Scope

- IV-C. Traitement par événements globaux

- V. Conclusion partie 2

- V-A. Remerciements

I - Introduction - partie 2

Pour faire suite à la partie 1, nous nous attaquons ici à la communication d'événements entre objets graphiques ou métiers. Cette communication est un élément important dans le développement d’interfaces graphiques car elle permet de découpler le code et d’avoir des composants indépendants et réutilisables tout cela aidé par l’application de patterns type MVC, MVP, MVVM et plein d’autres.

Comme expliqué dans la première partie, nous nous appuyons sur le framework MVVMFX qui nous apporte différents modes de communication. Il permet, par exemple, d'avoir une liaison (« binding ») des propriétés entre la vue et le contrôleur, de publier des notifications entre le contrôleur et la vue et enfin de publier des événements entre certains contrôleurs ou tous.

Pour rappel, l’initialisation du framework MVVMFX se fait dans la classe « main » en surchargeant MvvmfxGuiceApplication et non pas Application comme pour une application JavaFX classique. Le « Guice » ici fait référence au système d'injection des dépendances de Google. Il est possible d'utiliser d'autres implémentations d'injection de dépendances tel que JBoss Weld ou bientôt Spring. Cela sera utile pour la communication entre la vue et le contrôleur (injection du contrôleur dans la vue) ou encore la communication entre contrôleurs.

II - Communication par liaison simple

Il y a plusieurs niveau de communication entre la vue et le contrôleur et leur usage va dépendre des cas d’utilisation mais l’idée générale est que la vue traite l’interaction avec l’utilisateur alors que le contrôleur s’occupe des échanges avec les machines (services, base de données…).

II-A - Communication entre la vue et le contrôleur

Il s'agit d'un lien entre une propriété (de type « Property ») de la vue et une autre du contrôleur. Quand une propriété change, l'autre est automatiquement notifiée et mise à jour (observer pattern). Il y a soit un « binding » unidirectionnel (donc une notification que dans un sens) ou bien bidirectionnel. Nous privilégions généralement ce deuxième usage afin d'initialiser la valeur dans le contrôleur et de récupérer les mises à jour par la vue.

Pour illustrer ce cas, nous créons une pop-up About appelée depuis notre menu principal.

Dans le package gui, ajoutez un sous répertoire about qui contient les 3 fichiers de base d'un composant.

Comme vu dans la première partie, le fichier XML contient le descriptif de l’interface graphique, complété par la classe AboutView. Et la classe AboutViewModel fait le lien entre la présentation et la logique ou les données métier.

Dans la fenêtre about, nous souhaitons afficher le numéro de version et le numéro de build. La vue s'occupe d'afficher un texte donné par le contrôleur par liaison simple.

1. Dans le contrôleur, nous créons un objet de type « Property » qui contient le texte à afficher et qui est initialisé ici. Les types simples ont des propriétés déjà disponible comme SimpleStringProperty ou SimpleIntegerProperty et pour des types plus complexes nous utilisons un objet genérique de type Property<T>.

AboutViewModel

public class AboutViewModel implements ViewModel {
private StringProperty versionNumber = new SimpleStringProperty();

public void initialize() {
versionNumber.setValue("Version: " + App.APP_VERSION);
}

public StringProperty versionNumberProperty() {
return  versionNumber;
}
}

2. Dans le fichier XML, nous ajoutons le label avec un id, de type « fx:id » pour y accéder depuis le « code-behind », ie. la classe vue. Cet id a le même nom que la variable défini dans la classe vue (votre IDE peut vous générer la variable depuis le XML). Au passage, nous réduisons la taille de la fenêtre et ajoutons un bouton pour fermer la pop-up:

<AnchorPane xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="com.djay.gui.about.AboutView"
prefHeight="200.0" prefWidth="450.0">

<Label layoutX="20" layoutY="40" fx:id="version" />
<Button layoutX="200" layoutY="160" onAction="#close" text="Fermer" />

</AnchorPane>

3. Enfin dans la vue, nous effectuons la liaison entre le « Label » affiché et la « Property » de notre contrôleur :

public class AboutView implements FxmlView<AboutViewModel> {

public Label version;
private Stage aboutDialog;

@InjectViewModel
private AboutViewModel viewModel;

public void initialize() {
version.textProperty().bindBidirectional(viewModel.versionNumberProperty());
}

public void close() {
aboutDialog.close();
}

public void setStage(Stage dialogStage) {
this.aboutDialog = dialogStage;
}
}

Dans la méthode « initialize » de la vue, nous avons la liaison bidirectionnelle du contenu du label avec le contrôleur. Ainsi, les propriétés ont toujours la même valeur.

Enfin, pour faciliter l’appel du DialogAbout depuis le menu, nous ajoutons une classe « DialogHelper » dans un package « util » qui va créer une nouvelle scène et les éléments associés et lui appliquer la propriété «  APPLICATION_MODAL » :

DialogHelper

public class DialogHelper {

/**
*/
public static void initDialog(BooleanProperty openProperty, final Stage parentStage, Supplier<Parent> rootSupplier) {
final Stage dialogStage = new Stage(StageStyle.UTILITY);
dialogStage.initOwner(parentStage);
dialogStage.initModality(Modality.APPLICATION_MODAL);

openProperty.addListener((obs, oldValue, newValue) -> {
if (newValue) {
// when it is the first time the dialog is made visible (and therefore no scene exists) ...
if (dialogStage.getScene() == null) {
// ... we create a new scene and register it in the stage.
Scene dialogScene = new Scene(rootSupplier.get());
dialogScene.getStylesheets().add("/contacts.css");
dialogStage.setScene(dialogScene);
} else {
// ... otherwise we simple bring the dialog to front.
dialogStage.toFront();
}

dialogStage.sizeToScene();
dialogStage.show();
} else {
dialogStage.close();
}
});

// when the user clicks on the close button of the dialog window
// we want to set the property to false
dialogStage.setOnCloseRequest(event -> openProperty.set(false));
}

public static Stage showDialog(Parent view, Stage parentStage, StageStyle stageStyle, String... sceneStyleSheets) {
final Stage dialogStage = new Stage(StageStyle.UTILITY);
dialogStage.initOwner(parentStage);
dialogStage.initModality(Modality.APPLICATION_MODAL);
if ((stageStyle != null)) {
dialogStage.initStyle(stageStyle);
} else {
dialogStage.initStyle(StageStyle.DECORATED);
}
if (dialogStage.getScene() == null) {
// ... we create a new scene and register it in the stage.
Scene dialogScene = new Scene(view);
dialogScene.getStylesheets().addAll(sceneStyleSheets);
dialogStage.setScene(dialogScene);

dialogStage.sizeToScene();
dialogStage.show();
return dialogStage;
}
return null;
}
}

Du coup, dans la classe « MenuView » la méthode « about » appelée quand on click sur le menu, initialise les trois fichiers habituels du composant about (vue et controleur) et les passe au dialog helper précédent :

MenuView

public void about() {
ViewTuple<AboutView, AboutViewModel> aboutView = FluentViewLoader.fxmlView(AboutView.class).load();
aboutView.getView().getStylesheets().add("/css/dialog.css");
Stage dialogStage = DialogHelper.showDialog(aboutView.getView(), primaryStage, StageStyle.TRANSPARENT);
aboutView.getCodeBehind().setStage(dialogStage);
}

Afin de pouvoir fermer la boite de dialogue, nous passons le « stage » dans un setter :

aboutView.getCodeBehind().setStage(dialogStage);

Pour rappel, une application JavaFX est comme une représentation d’un spectacle : on retrouve la scène (Stage) puis l’estrade (Scene) et enfin les éléments qui la compose (Pane et autres nœuds).

Et voilà, quand vous ouvrez le dialog About, la valeur affichée provient de votre contrôleur. Si celle-ci est modifiée, la valeur sera automatiquement changée dans la vue.

II-B - Note sur l’affichage du numéro de build et du numéro de version avec Maven

Dans cette exemple, nous affichons le numéro de version et de build qui nous sont donné par une méthode statique de notre classe main. Nous souhaitons ne pas dupliquer cette donnée et estimons que le changement doit se faire uniquement dans le pom.xml.

Le problème est alors de récupérer cette information dans un fichier projet depuis notre instance.

Une façon de résoudre ce problème est de créer un fichier version.txt dans vos ressources et qui contient :

version=${pom.version}
build.date=${timestamp}

Dans le fichier pom.xml, il faut ajouter la propriété « timestamp » utilisée  qui se base sur l’instruction « maven.build » et nous ajoutons une instruction de formatage du build juste en dessous. Enfin, il ne faut pas oublier d’ajouter l’instruction de « filtering » à true pour le répertoire de reources afin que maven parse le fichier :

<properties>
<!-- autres propriétés ici -->

<timestamp>${maven.build.timestamp}</timestamp>
<maven.build.timestamp.format>yyyy-MM-dd HH:mm:ss</maven.build.timestamp.format>

</properties>

<build>
<!-- use resource files (fxml, css) from the java directory -->
<resources>
<resource>
<directory>src/main/java</directory>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>

<!-- Autres instructions de build à la suite →
</build>

A l’exécution du goal « compile » de Maven, ce dernier va générer le fichier version.txt avec les données du pom.xml

III - Communication par événements internes

Le framework MVVMFX nous apporte un système de notification entre la vue et le contrôleur. Cela est utilisé quand des traitements sont effectués depuis le contrôleur, comme des appels à des traitements asynchrones, et que nous souhaitons ensuite notifier la vue.

Nous venons de voir que pour mettre à jour des données nous pouvons avoir la liaison par propriété. Maintenant si nous souhaitons faire un traitement plus important dans la vue, comme une actualisation complète, nous pouvons notifier la vue par des messages depuis le contrôleur.

Tout d'abord, nous avons un lien entre la vue et le contrôleur qui nous est donné par l'annotation « @InjectViewModel ». Cette annotation permet d’avoir une référence au contrôleur depuis la vue.

Il est alors possible d'obtenir des données du contrôleur en appelant ses méthodes (exemple RefreshPositions ci-dessous). Mais dans ce cas, la vue est couplée aux méthodes du contrôleur car elle doit connaître ses méthodes.

L'autre cas d'usage est de notifier la vue des changements de données depuis le contrôleur (exemple RefreshInstruments ci-dessous). Dans ce cas, le contrôleur publie un message que la vue écoute via la methode « subscribe » pour ensuite lancer un traitement.

Pour illustrer ces deux cas, nous ajoutons une page centrale « Dashboard » avec les trois fichiers habituels (vue, fxml et contrôleur) et qui contient deux panneaux avec des statistiques sur les positions et instruments. Nous ajoutons deux boutons dans la vue pour simuler les deux types de communication.

Le code de la vue:

DashboardView

public class DashboardView implements FxmlView<DashboardViewModel> {

public StackPane statsPositionsPane;
public StackPane statsInstrumentsPane;
public Label nbActivePositionText;
public Label nbActiveInstrumentText;

@InjectViewModel
DashboardViewModel viewModel;

public void initialize() {
// Souscrit aux messages provenant du controleur
viewModel.subscribe(viewModel.REFRESH_DATA, (k, payload) -> {
Integer nbInstruments = (Integer) payload[0];
if (nbInstruments!=null) {
refreshFromController(nbInstruments);
}
});

// Effet visuel sur les panneaux de stats pour les faire resortir par defaut et les changer quand la souris passe dessus
// pour illustrer un autre moyen de le faire qu'en css
JFXDepthManager.setDepth(statsPositionsPane, 1);
JFXDepthManager.setDepth(statsInstrumentsPane, 1);
statsPositionsPane.hoverProperty().addListener((observable, oldValue, newValue) -> {
if (newValue)
changeVisualEffectPane(statsPositionsPane, 0);
else
changeVisualEffectPane(statsPositionsPane, 1);
});
statsInstrumentsPane.hoverProperty().addListener((observable, oldValue, newValue) -> {
if (newValue)
changeVisualEffectPane(statsInstrumentsPane, 0);
else
changeVisualEffectPane(statsInstrumentsPane, 1);
});
}

private void refreshFromController(Integer nbInstruments) {
// Le controlleur vient de notifier la vue qu'un changement a eu lieu.
// Il est aussi possible de passer la donnée dans le contenu du message.
nbActiveInstrumentText.setText(nbInstruments + " instruments utilisés");
}

public void refreshPostions(ActionEvent actionEvent) {
nbActivePositionText.setText(viewModel.getOpenPositions());
}

public void refreshInstruments(ActionEvent actionEvent) {
// Appelle le controleur pour simuler des changements de données
viewModel.refreshInstrumentSimulation();
}

private void changeVisualEffectPane(StackPane pane, int level) {
JFXDepthManager.setDepth(pane, level);
}
}

Dans la partie graphique nous utilisons un « JFXMasonryPane » de la bibliothèque Jfoenix.

Dashboard vue FXML

<JFXMasonryPane fx:id="masonryPane" HSpacing="20" VSpacing="20">
<StackPane fx:id="statsPositionsPane" prefHeight="220" prefWidth="220" styleClass="stat-pane">
<VBox>
<children>
<Button fx:id="refresh1" onAction="#refreshPostions" text="MàJ Positions">
<VBox.margin>
<Insets top="10.0" />
</VBox.margin>
</Button>
<Label fx:id="nbActivePositionText" text="0 position active">
<VBox.margin>
<Insets top="50.0" />
</VBox.margin>
</Label>
</children>
<padding>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
</padding>
</VBox>

</StackPane>
<StackPane fx:id="statsInstrumentsPane" prefHeight="220" prefWidth="220" styleClass="stat-pane">
<VBox>
<children>
<Button fx:id="refresh2" onAction="#refreshInstruments" text="MàJ Instruments">
<VBox.margin>
<Insets top="10.0" />
</VBox.margin>
</Button>
<Label fx:id="nbActiveInstrumentText" text="0 instrument utilisé">
<VBox.margin>
<Insets top="50.0" />
</VBox.margin>
</Label>
</children>
<padding>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
</padding>
</VBox>

</StackPane>
<padding>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
</padding>
</JFXMasonryPane>

Et enfin le contrôleur qui simule des traitements :

Dashboard VM

public class DashboardViewModel implements ViewModel {

public static String REFRESH_DATA = "Refresh data message";

public void refreshInstrumentSimulation() {
// Nous simulons par l'appel à cette methode un traitement long
// Une fois terminé, nous envoyons une notification à la vue
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
publish(REFRESH_DATA, new Random().nextInt(100) + 10);
}

public String getOpenPositions() {
Integer simulatedNb = new Random().nextInt(20);
return simulatedNb + " positions actives.";
}
}

Pour afficher le dashboard par défaut dans le contenu principal de l’application, nous modifions la « MainView » pour inclure la vue du dashboard par programmation :

MainView

public class MainView implements FxmlView<MainViewModel> {
public JFXTabPane tabPane;

public void initialize() {
final ViewTuple dashboard = FluentViewLoader.fxmlView(DashboardView.class).load();
tabPane.getTabs().add(new Tab("Dashboard", dashboard.getView()));
}
}

Ici on remarque l’apport du framework MVVMFX qui facilite le chargement d’une vue avec la classe « FluentViewLoader ».

A ce stade, vous devriez avoir ceci et en cliquant sur les boutons vous simulez des traitements et une communication entre la vue et le contrôleur :

IV - Communication par événements entre composant

Comme autre moyen fourni par MVVMFX pour la communication entre composant, nous avons les events par scope. L'objet Scope est utilisé uniquement au niveau des contrôleurs. Pour illustrer cet usage, le click sur le « hamburger » (l’icône avec les trois barres horizontales) va afficher la barre de navigation latérale. Le click et le résultat sont dans des composants différents.

IV-A - Préparation cosmétique de la barre de navigation latérale

Nous devons commencer par modifier le composant centrale pour avoir une barre de navigation latérale rétractable et un contenu centrale en onglets. L’icône hamburger qui ouvre ou ferme la barre de navigation se trouve dans le menu.

Nous modifions le menu pour y ajouter l’icône et encapsulons le menu existant dans une « vbox » :

MenuView.fxml

<VBox AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0"
fx:id="menuPane">
<children>
<MenuBar AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0"
AnchorPane.topAnchor="0.0">
<menus>
<Menu mnemonicParsing="false" text="%menu.file">
<items>
<MenuItem mnemonicParsing="false" text="%menu.quit" onAction="#quit" />
</items>
</Menu>
<Menu mnemonicParsing="false" text="%menu.help">
<items>
<MenuItem mnemonicParsing="false" onAction="#about" text="%menu.about" />
</items>
</Menu>
</menus>
</MenuBar>
<JFXToolbar>
<leftItems>
<JFXRippler maskType="CIRCLE" style="-fx-ripple-color:WHITE;">
<StackPane fx:id="titleBurgerContainer" onMouseClicked="#hamburgerClicked">
<JFXHamburger fx:id="titleBurger">
<HamburgerSlideCloseTransition/>
</JFXHamburger>
</StackPane>
</JFXRippler>
<Label text=""></Label>
</leftItems>
</JFXToolbar>
</children>
</VBox>

Nous ajoutons le triplet de fichier (Vue xml, la classe de vue et son contrôleur) dans un package content, avec les onglets du main actuel et nous modifions les 3 fichiers du main ainsi :

- nous remplaçons les onglets par un objet de type drawer qui va construire une barre latérale et un contenu centrale

<!-- Contenu principale -->
<center>
<JFXDrawer fx:id="drawer" defaultDrawerSize="180" direction="LEFT" styleClass="body">
</JFXDrawer>
</center>

- La vue initialise l’objet Drawer et va écouter le contrôleur pour ouvrir ou fermer la barre latérale :

public class MainView implements FxmlView<MainViewModel> {
public JFXDrawer drawer;

@InjectViewModel
private MainViewModel viewModel;

public void initialize() {
final ViewTuple navigationView = FluentViewLoader.fxmlView(NavigationView.class).load();
drawer.setSidePane(navigationView.getView());
drawer.setOverLayVisible(false);
// traitement des messages du controller
viewModel.subscribe(MainViewModel.SIDE_PANE, (k,v) -> openCloseSidePane());
// Affichage du conteneur qui contient le contenu
final ViewTuple contentView = FluentViewLoader.fxmlView(ContentView.class).load();
drawer.setContent(contentView.getView());
}

private void openCloseSidePane() {
if (drawer.isClosed() || drawer.isClosing()) {
drawer.open();
} else {
drawer.close();
}
}
}

- Pour l’instant le contrôleur ne fait rien mais c’est lui qui recevra les notifications par « scope »

public class MainViewModel implements ViewModel {

public static final String SIDE_PANE = "Show/Hide side pane";

}

IV-B - Traitement des événements par Scope

Nous ajoutons un objet scope spécifique qui implémente l’interface « Scope » que nous fournit le framework MVVMFX. Le principe est que les événements sont transmis par contexte et donc à une partie de l’application. Il faut garder en tête que les composants sont en structure d’arbre et que le contexte sera disponible pour le nœud et tous ses fils. N’hésitez pas à consulter le wiki des scopes.

Scope

import de.saxsys.mvvmfx.Scope;

public class MenuScope implements Scope {
public static final String SIDE_PANE = "SIDE_PANE";
public static final String MAIN_CONTENT = "ADD TAB IN MAIN CONTENT";
}

Nous déclarons deux String pour identifier les messages.

Puis dans MainViewModel, qui est un des plus haut nœud de l’aplication, nous ajoutons la déclaration de scope à l’aide de l’annotation « @ScopeProvider » et nous l’injectons pour pouvoir l’utiliser dans l’initialize qui écoute les evenements du scope et qui notifie la vue ensuite :

MainViewModel

@ScopeProvider(scopes = {MenuScope.class})
public class MainViewModel implements ViewModel {

@InjectScope
MenuScope menuScope;

public static final String SIDE_PANE = "Show/Hide side pane";

public void initialize() {
menuScope.subscribe(MenuScope.SIDE_PANE, (k,v) -> publish(SIDE_PANE));
}

}

Il reste à envoyer un evenement sur le scope lors du click sur le hamburger. Dans MenuView, on appelle une méthode du controleur et dans ce dernier, nous injectons le scope puis publions l’evenement :

MenuView

public void hamburgerClicked(MouseEvent mouseEvent) {
menuViewModel.notifyShowHideSidePane();
}

Et le controleur :

MenuViewModel

public class MenuViewModel implements ViewModel {

@InjectScope
MenuScope menuScope;

public void notifyShowHideSidePane() {
menuScope.publish(MenuScope.SIDE_PANE);
}
}

Et voilà, vous pouvez maintenant ouvrir ou ferme le menu latérale en cliquant sur l’icône « hamburger ».

IV-C - Traitement par événements globaux

Pour ajouter des onglets au contenu principal quand on clique sur un des menus latérale, nous utilisons des événements globaux disponible via l’objet « NotificationCenter » fourni par MVVMFX. Il s’agit d’un mécanisme similaire aux Scopes sauf qu’il n’y a pas de déclaration de scope à faire.

Dans la vue du menu latérale, nous ajoutons l’appel de la méthode du contrôleur :

NavigationView

public class NavigationView implements FxmlView<NavigationViewModel> {

@InjectViewModel
private NavigationViewModel viewModel;

public void showDashboard() {
viewModel.addMainContent(NavigationViewModel.DASHBOARD);
}

public void showPortfolios() {
viewModel.addMainContent(NavigationViewModel.PORTFOLIOS);
}

public void showInstruments() {
viewModel.addMainContent(NavigationViewModel.INSTRUMENTS);
}
}

Puis dans le contrôleur, nous envoyons le message via le NotificationCenter que nous avons injecté:

NavigationViewModel

public class NavigationViewModel implements ViewModel {
@Inject
private NotificationCenter notificationCenter;

public static final String MAIN_CONTENT = "ADD TAB IN MAIN CONTENT";

public static final String DASHBOARD = "Dashboard";
public static final String PORTFOLIOS = "Portfolios";
public static final String INSTRUMENTS = "Instruments";

public void addMainContent(String tabType) {
notificationCenter.publish(MAIN_CONTENT, tabType);
}
}

Il faut créer le contenu des 2 onglets manquants en ajoutant pour chacun nos 3 fichiers d’un composant (Vue FXML, Vue Java et VueModel).

Dans le « Content » qui gère l’ajout d’onglet dans le conteneur principale, nous ajoutons un écouteur d’événement sur le « NotificationCenter » qui va ensuite prévenir la vue d’ajouter un onglet d’un type passé en paramètre dans l’événement.

ContentViewModel

public class ContentViewModel implements ViewModel {
@Inject
NotificationCenter notificationCenter;

public static final String ACTION_ADD_COMPONENT ="Add component in the view";

public void initialize() {
notificationCenter.subscribe(NavigationViewModel.MAIN_CONTENT, (k, v) -> {
if (v.length==1 && v[0] instanceof String) {
String tabType = (String) v[0];
addTab(tabType);
}
});
}

private void addTab(String tabType) {
publish(ACTION_ADD_COMPONENT, tabType);
}
}

Et la vue écoute l’événement du contrôleur :

ContentView

public class ContentView implements FxmlView<ContentViewModel> {
public JFXTabPane tabPane;

@InjectViewModel
private ContentViewModel viewModel;

public void initialize() {
// Charge un onglet par defaut
final ViewTuple dashboard = FluentViewLoader.fxmlView(DashboardView.class).load();
tabPane.getTabs().add(new Tab("Dashboard", dashboard.getView()));

// Ecoute l"evenement pour ajouter un nouvel onglet
viewModel.subscribe(ContentViewModel.ACTION_ADD_COMPONENT, (key, payload) -> {
String tabType = (String) payload[0];
Parent tabView = null;
switch (tabType) {
case NavigationViewModel.DASHBOARD:
tabView = FluentViewLoader.fxmlView(DashboardView.class).load().getView();
break;
case NavigationViewModel.INSTRUMENTS:
tabView = FluentViewLoader.fxmlView(InstrumentsView.class).load().getView();
break;
case NavigationViewModel.PORTFOLIOS:
tabView = FluentViewLoader.fxmlView(PortfolioView.class).load().getView();
break;
}
if (tabView!=null) {
Parent finalTabView = tabView;
Platform.runLater(()-> {
Tab newTab = new Tab(tabType, finalTabView);
tabPane.getTabs().add(newTab);
tabPane.getSelectionModel().select(newTab);
});
}
}
);
}
}

V - Conclusion partie 2

À ce stade, vous devriez avoir une application qui ressemble à ceci :

V-A - Remerciements

Je tiens à remercier l'équipe de Developpez.com et plus particulièrement Mickael Baron pour son aide et son accompagnement et escartefigue pour sa relecture attentive.