You don't need to use CDI to manage the stages: stages themselves simply have a Scene
; they do not have any dependencies on any other objects you need to manage. All you need to do is ensure that the FXMLLoader
has a controllerFactory
that retrieves controller instances from the DI framework.
Here is a quick example (caveat: I have never used CDI/Weld before, so I might not have the optimal way of doing things here).
First, it's probably a good idea to expose a controller factory that gets the appropriate controllers:
package app;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Instance;
import javax.inject.Inject;
import javafx.util.Callback;
@ApplicationScoped
public class CDIControllerFactory implements Callback<Class<?>, Object> {
@Inject
private Instance<Object> instance ;
@Override
public Object call(Class<?> type) {
Object controller = instance.select(type).get();
return controller;
}
}
Here is a model class we want to share with all the controllers. Since we only want one instance, we make it @ApplicationScoped
:
package app;
import javax.enterprise.context.ApplicationScoped;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
@ApplicationScoped
public class Model {
private final ObservableList<String> names = FXCollections.observableArrayList();
public ObservableList<String> getNames() {
return names ;
}
public void addName(String name) {
names.add(name);
}
}
The test application will just have a list view (with a list of names) and a button for adding a new name from a dialog. Here is the main controller:
package app;
import java.io.IOException;
import javax.inject.Inject;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.control.ListView;
import javafx.stage.Modality;
import javafx.stage.Stage;
public class MainController {
@Inject
private Model model ;
@Inject
private CDIControllerFactory controllerFactory ;
@FXML
private ListView<String> listView ;
@FXML
private void initialize() {
listView.setItems(model.getNames());
}
@FXML
private void showAddDialog() throws IOException {
FXMLLoader loader = new FXMLLoader(AddNameController.class.getResource("AddNameDialog.fxml"));
loader.setControllerFactory(controllerFactory);
Scene scene = new Scene(loader.load());
Stage stage = new Stage();
stage.initModality(Modality.APPLICATION_MODAL);
stage.setScene(scene);
stage.show();
}
}
Note how it uses the controller factory on the FXMLLoader
. The stage can just be created "by hand".
Here's the controller for the dialog that is used to add new names. Note how it has a reference to the same model instance, via CDI:
package app;
import javax.enterprise.inject.Default;
import javax.inject.Inject;
import javafx.fxml.FXML;
import javafx.scene.control.TextField;
@Default
public class AddNameController {
@Inject
private Model model ;
@FXML
private TextField nameField ;
@FXML
private void submit() {
model.addName(nameField.getText());
close();
}
@FXML
private void close() {
nameField.getScene().getWindow().hide();
}
}
Here are the two FXML files (they are both in the app
package: the only real requirement with the way I coded these is that they should be in the same package as their corresponding controller classes).
Main.fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.control.ListView?>
<?import javafx.scene.layout.HBox?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<BorderPane xmlns:fx="http://javafx.com/fxml/1" fx:controller="app.MainController">
<center>
<ListView fx:id="listView" />
</center>
<bottom>
<HBox alignment="CENTER">
<padding>
<Insets top="5" right="5" left="5" bottom="5" />
</padding>
<Button text="Add..." onAction="#showAddDialog" />
</HBox>
</bottom>
</BorderPane>
AddNameDialog.fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.control.ListView?>
<?import javafx.scene.layout.HBox?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<BorderPane xmlns:fx="http://javafx.com/fxml/1" fx:controller="app.MainController">
<center>
<ListView fx:id="listView" />
</center>
<bottom>
<HBox alignment="CENTER">
<padding>
<Insets top="5" right="5" left="5" bottom="5" />
</padding>
<Button text="Add..." onAction="#showAddDialog" />
</HBox>
</bottom>
</BorderPane>
Here's the application class:
package app;
import java.io.IOException;
import org.jboss.weld.environment.se.Weld;
import org.jboss.weld.environment.se.WeldContainer;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class Main extends Application {
private Weld weld ;
private WeldContainer container ;
@Override
public void init() {
weld = new Weld();
container = weld.initialize();
}
@Override
public void stop() {
weld.shutdown();
}
@Override
public void start(Stage primaryStage) throws IOException {
FXMLLoader loader = new FXMLLoader(MainController.class.getResource("Main.fxml"));
loader.setControllerFactory(container.select(CDIControllerFactory.class).get());
Scene scene = new Scene(loader.load(), 600, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
and of course the CDI configuration class, META-INF/beans.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd"
bean-discovery-mode="all">
</beans>
If you really want to let CDI provide your stages, you can, but I don't really see there's much to gain by it. But, e.g. you can do something like:
package app;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.inject.Qualifier;
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD,
ElementType.TYPE, ElementType.PARAMETER})
public @interface ModalStage { }
which lets you provide modal and non-modal stages:
package app;
import javax.enterprise.inject.Produces;
import javafx.stage.Modality;
import javafx.stage.Stage;
public class StageProducer {
@Produces
public Stage stage() {
return new Stage();
}
@Produces
@ModalStage
public Stage modalStage() {
Stage stage = stage();
stage.initModality(Modality.APPLICATION_MODAL);
return stage ;
}
}
And then your MainController
might look like
public class MainController {
@Inject
private Model model ;
@Inject
private CDIControllerFactory controllerFactory ;
@Inject
@ModalStage
private Stage addNameDialogStage ;
@FXML
private ListView<String> listView ;
@FXML
private void initialize() {
listView.setItems(model.getNames());
}
@FXML
private void showAddDialog() throws IOException {
FXMLLoader loader = new FXMLLoader(AddNameController.class.getResource("AddNameDialog.fxml"));
loader.setControllerFactory(controllerFactory);
Scene scene = new Scene(loader.load());
addNameDialogStage.setScene(scene);
addNameDialogStage.show();
}
}
There are other facilities you could easily build into this, e.g. providing a class for loading FXML from a resource name, which incorporates the controller factory already, etc etc.