Développement de client en JavaFX (partie 3)

Attaquons nous maintenant à l'ajout de contenu avec nos vues, nos modèles, nos controllers, nos services...

Développement de client en JavaFX (partie 3)

Dans les partie 1 et partie 2, nous avons essentiellement vu la mise en place du projet. Nous allons maintenant voir comment ajouter du contenu.

TL;DR

En plus de JavaFX, le framework MVVMFX nous permet d'avoir du binding de propriétés entre la vue et le controller, de publier des notifications entre le controlleur et la vue et enfin de publier des évenements entre cerains controlleurs ou tous.

Vue et Controlleur

L'objectif de ce type de pattern est de découpler le code entre la classe qui traite la donnée et celle qui la présente. Un point clé est donc la communication des données entre classes sans créer de dépendance.
Nous utilisons MVVMFX que nous avons initialisé dans notre main en surchargeant MvvmfxGuiceApplication. Le "Guice" ici fait référence au sytème d'injection des dépendances de Google. Il est possible d'utiliser d'autres implementations d'injection de dépendances tel que JBoss Weld ou bientôt Spring.

Communication par binding simple

Il s'agit d'un lien entre une propriété (de type Property) de la vue et celle du controller. Ainsi quand l'un change, l'autre est automatiquement notifié et mis à jour.

A titre d'exemple, nous allons créer 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 :
client31

  • Le fichier xml contient le descriptif de l'interface graphique, complété par la classe AboutView (aussi appelé "Code behind"). Ces fichiers ne contiennent aucune logique à part celle de l'affichage.
  • La classe AboutViewModel (que l'on appelle ici controller par habitude) fait le lien entre la présentation et la logique ou les données métier.

Par exemple, nous affichons le numéro de version et le numéro de build dans notre dialog About. La vue s'occupe d'afficher un texte donné par le controller.

  1. Dans le controller, nous créons une Property qui contiendra le texte à afficher et qui sera chargé ici.
public class AboutViewModel implements ViewModel {
    private StringProperty versionNumber = new SimpleStringProperty();

    public void initialize() {
        versionNumber.setValue("Version: 1.0.0");
    }

    public StringProperty versionNumberProperty() {
        return  versionNumber;
    }
}
  1. Dans la classe vue, nous effectuons le binding entre le Label affiché et la Property de notre controller.
    public Label version;

    @InjectViewModel
    private AboutViewModel viewModel;

    public void initialize() {
        version.textProperty().bind(viewModel.versionNumberProperty());
    }
  1. Dans le fichier XML, nous ajoutons le Label avec un id, de type fx:id pour y accéder depuis le "code-behind". Cet id a le même nom que la variable de la class vue (votre IDE peut vous générer la variable depuis le XML).
<Label layoutX="20" layoutY="60" styleClass="about-text" fx:id="version" />

Nous ajoutons une classe DialogHelper pour faciliter l'appel du DialogAbout depuis notre commande du menu.
Cete dernière devient:

public void about(ActionEvent actionEvent) {
        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);
    }

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

Vous pouvez aussi "binder" des propriétés et ajouter un addListener((observable, oldValue, newValue) -> {}); du coté du controller et ainsi avoir les notifications de changement d'état par l'utilisateur par exemple.

Communication par events interne

Les events interne sont un système de notification que nous apporte MVVMFx pour notifier des changements entre la vue et le controller.

Tout d'abord, nous avons un lien entre la vue et le controlleur qui nous est donné par l'annotation @InjectViewModel qui est injectée dans la vue (comme déjà utilisé lors du binding de propriétés).

Depuis la vue, il est alors très simple d'obtenir des données du controlleur en appellant ses méthodes (exemple Refresh01 ci-dessous).

L'autre cas d'usage est de notifier la vue de changements de données depuis le controlleur (exemple Refresh02 ci-dessous). Dans ce cas, dans le controlleur nous utilisons la méthode publish(String messageName, Object... payload) et dans la vue nous souscrivons aux évenements avec la méthode subscribe().

Pour cet exemple, nous créons 2 boutons dans la vue pour simuler les 2 types de communication.
Le code de la vue:

public class DashboardView implements FxmlView<DashboardViewModel> {

    public StackPane stat01Pane;
    public Label nbActivePortfolioText;

    @InjectViewModel
    private DashboardViewModel viewModel;

    public void initialize() {
        JFXDepthManager.setDepth(stat01Pane, 1);
        viewModel.subscribe(viewModel.REFRESH_DATA, (k, payload) -> refreshFromController());
    }


    /**
     * Refresh le nombre de portfolios actifs par appel de la vue au controlleur
     * @param actionEvent
     */
    public void refresh01(ActionEvent actionEvent) {
        nbActivePortfolioText.setText(viewModel.getNbActivePortfolioProperty().get() + " portefeuilles actifs.");
    }

    public void refresh02(ActionEvent actionEvent) {
        viewModel.refresh02();
    }

    /**
     * Refresh le nombre de portfolios par notification du controlleur
     */
    private void refreshFromController() {
        nbActivePortfolioText.setText(viewModel.getNbActivePortfolioProperty().get() + " portefeuilles actifs.\n(Depuis le controlleur)");
    }
}

et le controlleur:

public class DashboardViewModel implements ViewModel {

    private IntegerProperty nbActivePortfolioProperty = new SimpleIntegerProperty();

    public static final String REFRESH_DATA = "refresh the view";

    public void initialize() {
    }

    public IntegerProperty getNbActivePortfolioProperty() {
        nbActivePortfolioProperty.set((int) (Math.random()*10));
        return nbActivePortfolioProperty; }

    public void refresh02() {
        publish(REFRESH_DATA);
    }
}

Communication entre controller via les scope

Autre moyen fourni par MVVMFX, les events par scope. L'objet Scope est utilisé uniquement au niveau des controllers. Nous en utilisons déjà pour notifier l'affichage de la barre de navigation latérale via le hamburger.

Prenons comme autre exemple, l'ajout de contenu par tabulation. Quand on click sur une des entrées de la barre latérale (Dashboard, Portfolio, Instrument), nous allons afficher le contenu correspondant dans un nouvel onglet. Pour cela, nous devons notifier le controller qui s'occupe de gérer les onglets (ContentViewModel).

Dans le NaviagationViewModel nous injectons le scope à utiliser. Il est possible d'utiliser différents scopes ce qui limite les broadcasts de messages à travers tous les controllers.
Avant la classe, nous ajoutons l'annotation @ScopeProvider avec les Scopes que nous utiliserons. Sans rentrer dans les détails, les scopes sont ensuite ajoutés en cascade dans les controllers fils et donc utilisables par ces derniers.
Puis nous ajoutons le scope avec l'annotation @InjectScope. Enfin nous notifierons les listeners du scope avec la méthode publish. Ce qui nous done:

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

    @InjectScope
    private MenuScope menuScope;

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

Ensuite, parmi les controlleurs du même scope (un controller peut appartenir à plusieurs scope), il faut implémenter un listener:

    @InjectScope
    private MenuScope menuScope;

    public void initialize() {
        // Event for adding a new component in the main view
        menuScope.subscribe(menuScope.MAIN_CONTENT, (k,v)->{
            if(v.length == 1 && v[0] instanceof String) {
                String contentType = (String) v[0];
                doSomething();
            }
        });
    }

Voilà pour cette partie. Prochainement, nous ajouterons de la donnée à partir d'un back-end.