Reputation: 36703
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'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);
}
});
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;
}
}
Upvotes: 2
Views: 4008
Reputation: 92
Also, can be add "prototype" at the begining of class.
@Component
@Scope("prototype")
public class ExampleController implements Initializable {
Upvotes: 0
Reputation: 61
It can be done little simpler, without using recursive function:
public class GuiContext {
private static final ApplicationContext applicationContext = new AnnotationConfigApplicationContext(GuiAppConfiguration.class);
public Object loadFxml(final String fileName) throws IOException {
return FXMLLoader.load(App.class.getResource(fileName), null, null, applicationContext::getBean);
}
}
Upvotes: 1
Reputation: 36703
The best I've managed to date...
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();
}
Upvotes: 0
Reputation: 209330
I don't see a way to do this completely cleanly. As you've noticed, the only information a controller factory gets is the controller class, so there is no way to distinguish between instances. You can use the FXML to provide further values to those controllers, but those values will be set during the FXML initialization phase, which will happen after the Spring initialization phase. (I think this is actually why your @PostConstruct
won't work: the @PostConstruct
method(s) will be invoked after construction, but before FXML injection.) Spring-injected controllers should always have prototype
scope, imho, since you would never want different FXML-loaded elements to share the same controller.
I think the way I would do this would be to inject model instances into the nested controllers using a prototype
scope for the model (so each controller gets its own model), and then configure those models from the top-level controller. This still involves some wiring "by hand" (you can't use Spring to inject the values you want directly into the model), but it feels a bit cleaner than the approach you outlined above. Not sure if it will actually work for your real use-case, of course.
So:
application-context.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="model" class="application.Model" scope="prototype" />
<bean id="nestedController" class="application.NestedController" scope="prototype" autowire="byName"/>
<bean id="topController" class="application.TopLevelController" scope="prototype" />
</beans>
Model.java:
package application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class Model {
private final StringProperty text = new SimpleStringProperty();
public final StringProperty textProperty() {
return this.text;
}
public final String getText() {
return this.textProperty().get();
}
public final void setText(final String text) {
this.textProperty().set(text);
}
}
NestedController.java:
package application;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
public class NestedController {
@FXML
private Label label ;
private Model model ;
public Model getModel() {
return model ;
}
public void setModel(Model model) {
this.model = model ;
}
public void initialize() {
label.textProperty().bindBidirectional(model.textProperty());
}
}
Nested.fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.Tab?>
<BorderPane xmlns:fx="http://javafx.com/fxml/1" fx:controller="application.NestedController">
<center>
<Label fx:id="label"/>
</center>
</BorderPane>
TopLevel.fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.control.TabPane?>
<?import javafx.scene.control.Tab?>
<BorderPane xmlns:fx="http://javafx.com/fxml/1"
fx:controller="application.TopLevelController">
<center>
<TabPane>
<tabs>
<Tab text="First">
<content>
<fx:include fx:id="firstTab" source="Nested.fxml" />
</content>
</Tab>
<Tab>
<content>
<fx:include fx:id="secondTab" source="Nested.fxml" />
</content>
</Tab>
</tabs>
</TabPane>
</center>
</BorderPane>
The TopLevelController
has to wire values into the models, which is a bit of a pain. I'm not sure if this is cleaner than wiring the entire model by hand or not...
package application;
import javafx.fxml.FXML;
public class TopLevelController {
@FXML
private NestedController firstTabController ;
@FXML
private NestedController secondTabController ;
public void initialize() {
firstTabController.getModel().setText("First Tab");
secondTabController.getModel().setText("Second Tab");
}
}
The application assembly is fairly straightforward:
package application;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class NestedControllersSpring extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
try (ClassPathXmlApplicationContext context
= new ClassPathXmlApplicationContext("application-context.xml")) {
FXMLLoader loader = new FXMLLoader(getClass().getResource("TopLevel.fxml"));
loader.setControllerFactory(cls -> context.getBean(cls));
primaryStage.setScene(new Scene(loader.load(), 600, 400));
primaryStage.show();
}
}
public static void main(String[] args) {
launch(args);
}
}
Upvotes: 4