Issue
I'm implementing a large application using JavaFX but unsure how to deal with nested controllers and Spring.
The FXML has already been provided by the design team and has up to 3 levels of nested FXML using the include mechanism.
Models will be defined in Spring
- I need to inject the models into all the nested controllers
- There are multiple instances of controllers of the same type. i.e. parts of the screen are identical but powered by different instances of the same model type.
My work so far
I've read Stephen Chin's blog - JavaFX in Spring Day 2 – Configuration and FXML and other SO questions however these only deals with top level controllers.
I experimented with FXMLLoader.setControllerFactory() mechanism and defining the controllers in the application context, however this only gives the class of the controller to create, which means there is no way to differentiate the two controllers of the same type but with different data.
Problem with using controller factory:
loader.setControllerFactory(new Callback<Class<?>, Object>() {
@Override
public Object call(Class<?> param) {
// OK but doesn't work when multiple instances controller of same type
return context.getBean(param);
}
});
- My best approach so far is for the top level controller to be wired up using Stephen Chin approach.
- For the case with multiple bean instances the parent controller will get references to the specific beans via @Autowire/@Qualifer and then set on the corresponding controller.
- The next level of controllers can be wired up too by exposing them on the top level controller and calling autowire()
Specific questions
- Is there anyway of using controller factory mechanism so I can define the controllers in the spring context instead so that it is easier to wire them up ?
- Is there some other method I can use?
Example
Spring context
<context:annotation-config />
<bean id="modelA" class="org.example.Model">
<property name="value" value="a value" />
</bean>
<bean id="modelB" class="org.example.Model">
<property name="value" value="b value" />
</bean>
Top level
<HBox maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.example.TopLevelController">
<children>
<TabPane tabClosingPolicy="UNAVAILABLE">
<tabs>
<Tab text="A">
<content>
<fx:include fx:id="a" source="nested.fxml" />
</content>
</Tab>
<Tab text="B">
<content>
<fx:include fx:id="b" source="nested.fxml" />
</content>
</Tab>
</tabs>
</TabPane>
</children>
</HBox>
Nested level
<HBox alignment="CENTER" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="200.0" prefWidth="200.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.example.NestedController">
<children>
<TextField fx:id="value" />
</children>
</HBox>
Application main
public class NestedControllersSpring extends Application {
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage stage) throws Exception {
ClassPathXmlApplicationContext context =
new ClassPathXmlApplicationContext("/app-context.xml");
FXMLLoader loader = new FXMLLoader(getClass().getResource("/top-level.fxml"));
stage.setScene(new Scene(loader.load()));
TopLevelController top = loader.getController();
context.getAutowireCapableBeanFactory().autowireBean(top);
context.getAutowireCapableBeanFactory().autowireBean(top.getAController());
context.getAutowireCapableBeanFactory().autowireBean(top.getBController());
top.init(); // needed because autowire doesn't call @PostConstruct
stage.show();
}
}
Top level controller
public class TopLevelController {
@FXML
private NestedController aController;
@FXML
private NestedController bController;
@Autowired
@Qualifier("modelA")
private Model a;
@Autowired
@Qualifier("modelB")
private Model b;
@PostConstruct
public void init() {
aController.setModel(a);
bController.setModel(b);
}
public NestedController getAController() {
return aController;
}
public NestedController getBController() {
return bController;
}
}
Solution
The best I've managed to date...
- Recursively go through controller wherever an FXML annotation present and not a Node
- Use autowireBean to hookup @Inject / @Autowire fields
- Use initializeBean() to call @PostConstruct if present.
Code
public void recursiveWire(ClassPathXmlApplicationContext context, Object root) throws Exception {
context.getAutowireCapableBeanFactory().autowireBean(root);
context.getAutowireCapableBeanFactory().initializeBean(root, null);
for (Field field : root.getClass().getDeclaredFields()) {
if (field.isAnnotationPresent(FXML.class) &&
! Node.class.isAssignableFrom(field.getType())) {
// <== assume if not a Node, must be a controller
recursiveWire(context, field.get(root));
}
}
}
Usage
@Override
public void start(Stage stage) throws Exception {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("/app-context.xml");
FXMLLoader loader = new FXMLLoader(getClass().getResource("/top-level.fxml"));
stage.setScene(new Scene(loader.load()));
recursiveWire(context, loader.getController());
stage.show();
}
Answered By - Adam