J'ai cherché à réaliser quelque chose qui s'est avéré difficile et je n'ai pas trouvé non plus d'aide sur le net pour faire ça. Comme j'ai la solution je la poste.

Injection d'un contrôleur d'un composant FXML inclus dans un autre FXML à l'aide de fx:include

Pré requis

Connaître les bases de Java FX et des FXML. Cours de rattrapage: http://fabrice-bouye.developpez.com/...ur-fxml-javafx

Position du problème (peut être omis en première lecture)

  1. Ma manière de réaliser l'injection de dépendances

    Je n'utilise jamais de setter (sauf rare contre-exemple) pour injecter les dépendances. J'évite les frameworks comme Spring. Bref, le code le plus simple possible, pas de fioritures (POJOs only), que des constructeurs (constructeurs privés + factory, avec des références final sur les coworkers/dépendances injectés).

  2. Injection d'un contrôleur custom

    Je cherchais à injecter un contrôleur custom à mes FXML. La motivation étant d'avoir un contrôleur dans lequel, je peux injecter des dépendances librement. Le FXMLLoader réalise l'injection des dépendances pour les Nodes Java FX déclarés dans le FXML avec des balises @FXML dans le contrôleur. Mais si on veux injecter d'autres types de dépendance ce n'est pas toujours facile avec des déclarations dans le fichier FXML.
    Pour cela l'API Java FX fournit FXMLLoader.setController avec laquelle on peut injecter un contrôleur sur mesure. Google "FXMLLoader setController" permet d'avoir plein d'exemples.

  3. Création de nested FXML

    Je cherche aussi à créer des composants FXML réutilisables. Même si je ne les réutilise pas, je créer des composants aussi petit que possible dédié à une fonction. Je crée des composants plus gros en utilisant des fx:include de sous composants. On évite les gros FXML de 3 kms. Google "Java FX fx:include" pour tout savoir sur le sujet.

  4. Injection d'un contrôleur custom pour un "nested"/include FXML

    C'est là où ça devient plus compliqué. J'utilise FXMLLoader setControllerFactory, mais il faut encore quelques subtilités en plus pour avoir un résultat vraiment probant.

    Voici la solution que je propose au travers d'un exemple:


Solution

On a les éléments suivants:

  • Main.java
  • MainGui.fxml
    FXML principal qui va contenir les sous FXML
  • MainGuiController.java
    Controlleur du FXML principal
  • SubGui.fxml
    Sous FXML inclus dans le FXML principal
  • SubGuiController.java
    Contrôleur du sous FXML
  • SubGuiView.java
    Node racine du fichier sous FXML


Main.java
Code : Sélectionner tout - Visualiser dans une fenêtre à part
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
55
 
package application;
 
import java.net.URL;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Callback;
 
public class Main extends Application {
	static SubGuiController subGuiController;
 
	@Override
	public void start(Stage primaryStage) {
		try {
			URL url = getClass().getResource("MainGui.fxml");
			FXMLLoader fxmlLoader = new FXMLLoader(url);
			MainGuiController mainGuiController = new MainGuiController("SomeDep");
			SubGuiController subGuiController1 = new SubGuiController("SomeSubDep_1");
			SubGuiController subGuiController2 = new SubGuiController("SomeSubDep_2");
			subGuiController = subGuiController1;
 
			fxmlLoader.setControllerFactory(new Callback<Class<?>, Object>() {
				@Override
				public Object call(Class<?> param) {
					if (param.equals(MainGuiController.class)) {
						System.out.println("Factory MainGuiController");
						return mainGuiController;
					} else if (param.equals(SubGuiController.class)) {
						System.out.println("Factory SubGuiController");
						SubGuiController subGuiControllerTmp = subGuiController;
						subGuiController = subGuiController2; // Bidouille non utilisable en vrai
						return subGuiControllerTmp;
					}
					return null;
				}
			});
 
			VBox root = fxmlLoader.load();
 
			Scene scene = new Scene(root, 400, 400);
			primaryStage.setScene(scene);
			primaryStage.show();
		}
		catch (Exception e) {
			e.printStackTrace();
		}
	}
 
	public static void main(String[] args) {
		launch(args);
	}
}
La fonction "setControllerFactory" permet de définir une factory qui va pouvoir renvoyer un contrôleur suivant chaque classe de contrôleur définie dans les fichiers FXML. Un des avantages de cette méthode est qu'elle permet de garder la balise fx:controller dans le fichier FXML et d'injecter un contrôleur FXML contrairement à setController où il faut supprimer la balise fx:controller. Ainsi on peut avoir les erreurs qui s'affichent dans un IDE si il y a des problèmes entre le contrôleur et le FXML.

Malheureusement si l'on a plusieurs instance du sous FXML comme dans notre cas, il est difficille d'injecter un contrôleur dédié à chaque instance du sous FXML. Ici on a fait une bidouille pour injecter des contrôleurs différents à chaque appel de la factory. Mais ce n'est pas portable, on fait ça à l'aveugle car on ne sait pas vraiment sur quelle instance du sous FXML on agit. Par contre, il est très facile de donner au choix soit
1. une même instance du contrôleur à chaque sous FXML (renvoyer toujours le même contrôleur dans la factory)
ou soit
2. une instance différente pour chacun. (instancier un nouveau contrôleur à chaque appel de la factory)

Il nous manque donc un moyen de différencier les différentes instances du sous FXML et de leur contrôleur. En effet dans le contrôleur, on ne sait pas à quel sous FXML on a à faire. Pour cela on va créer un identifiant pour chaque sous FXML en définissant la classe SubGuiView.java


MainGui.fxml
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 
<?xml version="1.0" encoding="UTF-8"?>
 
<?import java.lang.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.layout.VBox?>
 
 
<VBox xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/8.0.40"
    fx:controller="application.MainGuiController">
   <children>
      <Button onAction="#btnAction1" text="Button1" />
      <Button onAction="#btnAction2" text="Button2" />
      <fx:include fx:id="subGui1" source="SubGui.fxml" subGuiIdx="1" />
      <fx:include fx:id="subGui2" source="SubGui.fxml" subGuiIdx="2" />
   </children>
</VBox>
Ici, on crée les deux instances du sous FXML avec deux indexes différents (subGuiIdx = 1 ou 2)


MainGuiController.java
Code : Sélectionner tout - Visualiser dans une fenêtre à part
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
 
package application;
 
import javafx.fxml.FXML;
 
public class MainGuiController {
 
    String dep;
 
    @FXML
    private SubGuiView subGui1;
    @FXML
    private SubGuiController subGui1Controller;
    @FXML
    private SubGuiView subGui2;
    @FXML
    private SubGuiController subGui2Controller;
 
    public MainGuiController(String dep) {
        this.dep = dep;
    }
 
    @FXML public void btnAction1() {
        System.out.println("Main 1 " + dep + " -- " + subGui1Controller.getDepVal() + " -- sub Idx ctrl = " + subGui1Controller.getSelfIdx());
    }
 
    @FXML public void btnAction2() {
        System.out.println("Main 2 " + dep + " -- " + subGui2Controller.getDepVal() + " -- sub Idx view = " + subGui2.getSubGuiIdx());
    }
}
Rien de spécial, on a les différentes instances des sous FXML.


SubGui.fxml
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 
<?xml version="1.0" encoding="UTF-8"?>
 
<?import java.lang.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import application.SubGuiView?>
 
 
<SubGuiView xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/8.0.40"
    fx:controller="application.SubGuiController"
    fx:id="view">
   <children>
      <Label text="Label" />
      <Button onAction="#btnAction" text="ButtonSub" />
   </children>
</SubGuiView>
La racine du sous FXML est de type SubGuiView (étend HBox), ce qui va permettre d'identifier les différentes instances. On lui attribue le fx:id "view".


SubGuiController.java
Code : Sélectionner tout - Visualiser dans une fenêtre à part
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
 
package application;
 
import javafx.fxml.FXML;
 
public class SubGuiController {
 
    String dep;
    @FXML SubGuiView view;
 
    public SubGuiController(String dep) {
        this.dep = dep;
    }
 
    @FXML public void btnAction() {
        System.out.println("Sub " + dep + " -- " + view.getSubGuiIdx());
    }
 
    public String getDepVal() {
        return dep;
    }
 
    public int getSelfIdx() {
        return view.getSubGuiIdx();
    }
}
Dans le contrôleur du sous FXML on a "SubGuiView view", qui permet d'identifier dans quelle instance définie dans le FXML main on se trouve.


SubGuiView.java
Code : Sélectionner tout - Visualiser dans une fenêtre à part
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
 
package application;
 
import javafx.scene.layout.HBox;
 
public class SubGuiView extends HBox {
 
    private int subGuiIdx;
    private boolean idxSet;
 
    public SubGuiView() {
        idxSet = false;
    }
 
    public int getSubGuiIdx() {
        return subGuiIdx;
    }
 
    public void setSubGuiIdx(int idx) {
        if (idxSet) {
            return;//Or throw error
        }
        this.subGuiIdx = idx;
        idxSet = true;
    }
}

"subGuiIdx" permet de définir un id grâce à l'attribut subGuiIdx dans la balise fx:include du FXML main. On peut généraliser le principe pour passer d'autres paramètres si besoin. Le boolean idxSet est juste une autre bidouille pour rendre SubGuiView "immutable".



Voilà ça fait beaucoup. L'avantage de la méthode proposée ici est de construire et d'injecter des contrôleurs sur mesure dont toutes les dépendances peuvent être passées à la construction. Ainsi tout est fonctionnel, après l'appel à FXMLLoader.load(), les contrôleurs n'ont pas besoin d'avoir leur dépendances initialisées.