1

I would like to integrate JavaFX and CDI. There are some good examples over the web about that like these ones:

https://dzone.com/articles/fxml-javafx-powered-cdi-jboss http://fxapps.blogspot.com.br/2017/10/using-cdi-20-in-javafx-application.html

However, all the examples I have seen don't work in a real world because they are not able to inject more than one Stage (the primaryStage), if they are, I don't know how.

So I would like to know if it's possible in a JavaFX/CDI project to inject more than one Stage (to use in modal windows for example...)

thank's!

  • 1
    Seems strange to inject a stage into anything - neither of the links you provided use the DI framework to manage the stage. I can't see any reason that any component would have a dependency on a stage. Usually you use dependency injection frameworks in JavaFX to inject models and services into the controllers, and establish other dependencies among the models and services. – James_D Dec 01 '17 at 13:48
  • James_D, thank's for replying! The problem is that to show modal dialogs I need a new Stage for them, thus, the question is how could I get that Stage? If I call **new Stage()** the CDI beans won't get injected into the controller of that Stage. – Rafael A P Nascimento Dec 01 '17 at 16:54
  • Just create a new stage. Why do you need the DI framework to create it? – James_D Dec 01 '17 at 16:55
  • *"If I call `new Stage()` the CDI beans won't get injected into the controller of that Stage."*. This doesn't make sense. Controllers are not created when you call `new Stage()`, they are (typically) created when you load the FXML file. Just instruct the `FXMLLoader` to create controllers via CDI in the usual way. – James_D Dec 01 '17 at 17:05
  • "_they are (typically) created when you load the FXML file_" yes! So how could I inject any CDI bean (such as services for example) into this controller - IF - its FXML represents a modal window with its own Stage? – Rafael A P Nascimento Dec 01 '17 at 17:14
  • *"how could I inject any CDI bean (such as services for example) into this controller"* The same way as it's done in the first link in your question. There is absolutely nothing specific to the `primaryStage` in the way the `FXMLLoader` is used there. – James_D Dec 01 '17 at 17:15
  • I tried that way, but the CDI beans won't get injected, i.e., they remain null! I guess that's because a modal window requires a new stage for it... – Rafael A P Nascimento Dec 01 '17 at 17:25
  • Your guess is probably wrong. It's much more likely that you just have an error in your code. Again, a controller cares nothing about which stage the UI it is controlling is displayed in. – James_D Dec 01 '17 at 17:25
  • ok, thanks, James! I will check my code up. I will be back with the code if couldn't figure out what's wrong – Rafael A P Nascimento Dec 01 '17 at 17:39
  • Posted an answer, containing a complete example which has multiple stages and uses CDI to share a model between two controllers. You should be able to copy, paste, and run this under JDK 1.8.0 and weld-se 2.4.5. – James_D Dec 01 '17 at 20:28

1 Answers1

8

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.

James_D
  • 201,275
  • 16
  • 291
  • 322