I've been through a number of tutorials on integrating Spring DI with JavaFx but I've hit a wall that the simple examples dont cover (and I cant figure out).
I want clean separation between the view and presentation layers. I would like to use fxml to define composable views and Spring to wire it all together. Here's a concrete example:
Dashboard.fxml:
<GridPane fx:id="view"
fx:controller="com.scrub.presenters.DashboardPresenter"
xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml">
<children>
<TransactionHistoryPresenter fx:id="transactionHistory" />
</children>
</GridPane>
Main.java:
public void start(Stage primaryStage) throws Exception{
try {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppFactory.class);
SpringFxmlLoader loader = context.getBean(SpringFxmlLoader.class);
primaryStage.setScene(new Scene((Parent)loader.load("/views/dashboard.fxml")));
primaryStage.setTitle("Hello World");
primaryStage.show();
} catch(Exception e) {
e.printStackTrace();
}
}
SpringFxmlLoader.java:
public class SpringFxmlLoader {
@Autowired
ApplicationContext context;
public Object load(String url) {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource(url));
loader.setControllerFactory(new Callback<Class<?>, Object>() {
@Override
public Object call(Class<?> aClass) {
return context.getBean(aClass);
}
});
return loader.load();
} catch(Exception e) {
e.printStackTrace();
throw new RuntimeException(String.format("Failed to load FXML file '%s'", url));
}
}
}
So when DashboardPresenter gets loaded the SpringFxmlLoader correctly injects the controller with the loader.setControllerFactory.
However, the custom TransactionHistoryPresenter control is loaded with a new instance and not from the spring context. It must be using its own FXMLLoader?
Any ideas how to make custom controls play nice with Spring? I really dont want to go down the path of having the controllers / presenters manually wiring them up.
The main problem here, is make sure that Spring is initialized on the same thread of the JavaFX application. This usually means that Spring code must be executed on the JavaFX application thread; other time-consuming jobs can of course be executed on their own thread.
This is the solution I put together using this tutorial and my own knowledge of Spring Boot:
This class is both a JavaFX application entry point and a Spring Boot initialization entry point, hence the passing around of varargs. Importing an external configuration file makes it easier to keep the main class uncluttered while getting other Spring-related stuff ready (i.e. setting up Spring Data JPA, resource bundles, security...)
On the JavaFX "start" method, the main ApplicationContext is initialized and lives. Any bean used at this point must be retrieved via ApplicationContext.getBean(), but every other annotated bean (provided it is in a descendant package of this main class) will be accessible as always.
In particular, Controllers are declared in this other class:
You can see any Controller (I have just one, but it can be many) is annotated with @Bean and the whole class is a Configuration.
Finally, here is MainPaneController.
This Controller is declared as a @Bean, so it can be @Autowired with and from any other @Beans (or Services, Components, etc.). Now for example you can have it answer to a button press and delegate logic performed on its fields to a @Service. Any component declared into the Spring-created Controllers will be managed by Spring and thus aware of the context.
It is all quite easy and straightforward to configure. Feel free to ask if you have any doubts.