FXML & JavaFX powered by CDI & JBoss Weld
Some people already blogged on how to proceed to have Guice injection [1] working with JavaFX & FXML.
Now it's my turn to provide a way to empower JavaFX with CDI using Weld as the implementation.
My goal was to have CDI just working, no matter how I was using JavaFX by directly coding in plain Java or using FXML.
Ready? let's go !!!
Bootstrap JavaFX & Weld/CDI
The launcher class will be the only place where we will have Weld specific code, all the rest will be totally CDI compliant.The only trick here is to make the application parameters available as a CDI compliant object so that we can reuse them afterwards.
Notice also that we use the CDI event mechanism to startup our real application code.
public class WeldJavaFXLauncher extends Application {
/**
* Nothing special, we just use the JavaFX Application methods to boostrap
* JavaFX
*/
public static void main(String[] args) {
Application.launch(WeldJavaFXLauncher.class, args);
}
@SuppressWarnings("serial")
@Override
public void start(final Stage primaryStage) throws Exception {
// Let's initialize CDI/Weld.
WeldContainer weldContainer = new Weld().initialize();
// Make the application parameters injectable with a standard CDI
// annotation
weldContainer.instance().select(ApplicationParametersProvider.class).get().setParameters(getParameters());
// Now that JavaFX thread is ready
// let's inform whoever cares using standard CDI notification mechanism:
// CDI events
weldContainer.event().select(Stage.class, new AnnotationLiteral<StartupScene>() {}).fire(primaryStage);
}
}
Start our real JavaFX application
Here starts our real application code. We're just listening the previously fired event (containing the Scene object to render into) and start showing our application.In the following example, we load an FXML GUI but it might have been any node created in any way.
public class LoginApplicationStarter {
// Let's have a FXMLLoader injected automatically
@Inject FXMLLoader fxmlLoader;
// Our CDI entry point, we just listen to an event providing the startup scene
public void launchJavaFXApplication(@Observes @StartupScene Stage s) {
InputStream is = null;
try {
is = getClass().getResourceAsStream("login.fxml");
// we just load our FXML form (including controler and so on)
Parent root = (Parent) fxmlLoader.load(is);
s.setScene(new Scene(root, 300, 275));
s.show(); // let's show the scene
} catch (IOException e) {
throw new IllegalStateException("cannot load FXML login screen", e);
} finally {
// omitted is cleanup
}
}
}
But what about the FXML controller?
First let's have a look at the controller we want to use inside our application.
It is a pure POJO class annoted with both JavaFX & CDI annotations.
It is a pure POJO class annoted with both JavaFX & CDI annotations.
// Simple application controller that uses injected fieldsIn order to have injection working inside the FXML controller we need to intrument JavaFX so that controller objects are created by CDI.
// to delegate login process and to get default values from the command line using: --user=SomeUser
public class LoginController implements Initializable {
// Standard FXML injected fields
@FXML TextField loginField;
@FXML PasswordField passwordField;
@FXML Text feedback;
// CDI Injected service
@Inject LoginService loginService;
// Default application parameters retrieved using CDI
@Inject Parameters applicationParameters;
@FXML protected void handleSubmitButtonAction(ActionEvent event) {
feedback.setText(loginService.login(loginField.getText(), passwordField.getText()));
}
@Override
public void initialize(URL location, ResourceBundle resources) {
loginField.setText(applicationParameters.getNamed().get("user"));
}
}
As we are in a CDI environment we can also have the FXMLLoader classes injected, that's exactly what we did in the previous LoginApplicationStarter class.
How can we achieve this?
We just have to provide a Producer class which responsibility will be to create FXMLLoader instances able to load FXML GUIs and instanciate controllers using CDI.
The only thing a little bit tricky there is that the controller instanciation depends on the required class or interface (using fx:controller in your fxml file). In order to have such a runtime injection/resolution available we use a
CDI Instance Object.
public class FXMLLoaderProducer {
@Inject Instance<Object> instance;
@Produces
public FXMLLoader createLoader() {
FXMLLoader loader = new FXMLLoader();
loader.setControllerFactory(new Callback<Class<?>, Object>() {
@Override
public Object call(Class<?> param) {
return instance.select(param).get();
}
});
return loader;
}
}
I hope you found the article interesting and of course do not hesitate to comment if you see some errors or possible enhancements...
Finally if you are interrested you can find the full source code here.
[1] http://andrewtill.blogspot.be/2012/07/creating-javafx-controllers-using-guice.htm