0

I am developing an app where I have a menu with 3 items: A, B, C

Each item leads to a flow of several screens. E.g. if A is clicked, then we get A1 -> A2 -> A3.

This is same for B and C.

For each Ai View(FXML) there is also a corresponding controller. Screens are created dynamically. I.e. A2 won't be created unless A1 is completed.

Please note that Ai and Bi controllers are instances of the same class.

I would like to have a one instance model injected to all controllers instances (all controllers instances created per a flow). E.g. One instance will be created and serve A1, A2, A3 controllers instances.

Is there any way to use Google Guice for this purpose or another framework for this kind of purpose ?

Thanks !

sfdcdev
  • 199
  • 4
  • 12
  • @James_D hi, I have read the post you have linked and it doesn't provide me a solution for my question. I would like to get different groups of controllers with different injected instance. – sfdcdev Jan 17 '17 at 23:52
  • I'm not sure I understand that question. In Spring, you control that by the `@Scope` (prototype: a new instance each time the bean is requested, singleton: the same instance each time the bean is requested). So typically your controllers are prototypes and your model/service singletons: but if you wanted a different injected instance for each, you would simply make each a prototype. I don't know Guice but this is pretty fundamental functionality for a DI framework, so it must have that built in. So use the controller factory to generate the controller from the DI, and configure DI as you need. – James_D Jan 17 '17 at 23:54
  • So in your scenario, your controllers for A1, A2, A3 are all prototypes. You create a model bean for that flow ("modelA"), make it a singleton, and inject it into each of A1, A2, A3. Doesn't that answer the question? – James_D Jan 17 '17 at 23:58
  • [Here](https://github.com/google/guice/wiki/Scopes) is a link to the equivalent for Guice. It looks basically the same, except that the default is prototype instead of singleton. – James_D Jan 18 '17 at 00:03
  • Ok thank you very much. I will look into it. – sfdcdev Jan 18 '17 at 00:04
  • So how can I make sure that same instance will be provided for A1, A2 and another one for B1, B2 ? – sfdcdev Jan 18 '17 at 00:05
  • Two singleton beans...? – James_D Jan 18 '17 at 00:06
  • Will it work also if A1 and B1 are instances of the same class? so are A2 and B2 – sfdcdev Jan 18 '17 at 00:13
  • That might take a bit of thought. There should be a way to do it. – James_D Jan 18 '17 at 00:13
  • The thing is that I have an hierarchy of classes that when I load A's FXML or B's FXML same hierarchy is instantiated. I.e. Instance per each class in this hierarchy. and i would like to have one Model instance injected to each group of instances. hope it's much more clear now :) – sfdcdev Jan 18 '17 at 00:18
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/133398/discussion-between-ron-daniel-and-james-d). – sfdcdev Jan 18 '17 at 00:22
  • So that looks a bit tricky. I can see how to do it without a D.I. framework, which is a bit ugly. Or with Spring but using a different application context for each "flow", which answers the question you posted but is problematic in general (can't share anything between the two flows, which you'd likely need to do too). You probably need to tag this question with the D.I. framework you want to use, if that's the way you want to go. – James_D Jan 18 '17 at 12:49
  • Thanks, could you please describe how would you do that without a DI framework? – sfdcdev Jan 18 '17 at 13:45
  • See answer (with updates). – James_D Jan 18 '17 at 17:52

2 Answers2

1

You can effect dependency injection into controllers using a controller factory. So in short, if you have some kind of model class, you can use a controller factory to pass values to the controller's constuctor:

Model model = ... ;

Callback<Class<?>, Object> controllerFactory = type -> {

    try {

        for (Constructor<?> c : type.getConstructors()) {
            if (c.getParameterCount() == 1 && c.getParameterTypes()[0] == BookingModel.class) {
                return c.newInstance(model);
            }
        }

        // no appropriate constructor: just use default:
        return type.newInstance(); 

    } catch (Exception exc) {
        throw new RuntimeException(exc);
    }

};

FXMLLoader loader = new FXMLLoader(getClass().getResource("/path/to/fxml"));
loader.setControllerFactory(controllerFactory);
Parent view = loader.load(); 

So in the case you describe, you would create one model for your "A" flow, create the controller factory from that model, and then use that controller factory when you load A1, A2, and A3. Then create another model instance, a controller factory from that second model instance, and use that controller factory to load B1, B2, and B3.

To make this more concrete, consider a hotel room booking application, which we can divide into three parts (for demonstration purposes): setting the arrival date, setting the departure date, and confirming the booking. The three pieces each need to access the same data, which would be kept in a model class. We can also use that model class to maintain the current state of the flow; e.g. which of the three booking steps (arrival, departure, confirm) we are in. Something like:

public class BookingModel {

    private final ObjectProperty<LocalDate> arrival = new SimpleObjectProperty<>();
    private final ObjectProperty<LocalDate> departure = new SimpleObjectProperty<>();
    private final BooleanProperty confirmed = new SimpleBooleanProperty();
    private final ObjectProperty<Screen> screen = new SimpleObjectProperty<>();

    public enum Screen {
        ARRIVAL, DEPARTURE, CONFIRMATION
    }

    public BookingModel() {
        arrival.addListener((obs, oldArrival, newArrival) -> {
            if (departure.get() == null || departure.get().equals(arrival.get()) || departure.get().isBefore(arrival.get())) {
                departure.set(arrival.get().plusDays(1));
            }
        });
    }

   // set/get/property methods for each property...

}

Each step has an FXML and a controller, and each controller needs access to the model instance shared by the steps in the same flow. So we can do:

public class ArrivalController {

    private final BookingModel model ;

    @FXML
    private DatePicker arrivalPicker ;

    @FXML
    private Button nextButton ;

    public ArrivalController(BookingModel model) {
        this.model = model ;
    }

    public void initialize() {

        arrivalPicker.valueProperty().bindBidirectional(model.arrivalProperty());

        arrivalPicker.disableProperty().bind(model.confirmedProperty());

        nextButton.disableProperty().bind(model.arrivalProperty().isNull());
    }

    @FXML
    private void goToDeparture() {
        model.setScreen(BookingModel.Screen.DEPARTURE);
    }
}

and

public class DepartureController {
    private final BookingModel model ;

    @FXML
    private DatePicker departurePicker ;

    @FXML
    private Label arrivalLabel ;

    @FXML
    private Button nextButton ;

    public DepartureController(BookingModel model) {
        this.model = model ;
    }

    public void initialize() {
        model.setDeparture(null);

        departurePicker.setDayCellFactory(/* cell only enabled if date is after arrival ... */);

        departurePicker.valueProperty().bindBidirectional(model.departureProperty());

        departurePicker.disableProperty().bind(model.confirmedProperty());

        arrivalLabel.textProperty().bind(model.arrivalProperty().asString("Arrival date: %s"));

        nextButton.disableProperty().bind(model.departureProperty().isNull());

    }


    @FXML
    private void goToArrival() {
        model.setScreen(BookingModel.Screen.ARRIVAL);
    }

    @FXML
    private void goToConfirmation() {
        model.setScreen(BookingModel.Screen.CONFIRMATION);
    }
}

and

public class ConfirmationController {

    private final BookingModel model ;

    @FXML
    private Button confirmButton ;
    @FXML
    private Label arrivalLabel ;
    @FXML
    private Label departureLabel ;

    public ConfirmationController(BookingModel model) {
        this.model = model ;
    }

    public void initialize() {

        confirmButton.textProperty().bind(Bindings
                .when(model.confirmedProperty())
                .then("Cancel")
                .otherwise("Confirm"));

        arrivalLabel.textProperty().bind(model.arrivalProperty().asString("Arrival: %s"));
        departureLabel.textProperty().bind(model.departureProperty().asString("Departure: %s"));
    }

    @FXML
    private void confirmOrCancel() {
        model.setConfirmed(! model.isConfirmed());
    }

    @FXML
    private void goToDeparture() {
        model.setScreen(Screen.DEPARTURE);
    }
}

Now we can create a "booking flow" with

private Parent createBookingFlow() {
    BookingModel model = new BookingModel() ;
    model.setScreen(Screen.ARRIVAL);
    ControllerFactory controllerFactory = new ControllerFactory(model);
    BorderPane flow = new BorderPane();

    Node arrivalScreen = load("arrival/Arrival.fxml", controllerFactory);
    Node departureScreen = load("departure/Departure.fxml", controllerFactory);
    Node confirmationScreen = load("confirmation/Confirmation.fxml", controllerFactory);

    flow.centerProperty().bind(Bindings.createObjectBinding(() -> {
        switch (model.getScreen()) {
            case ARRIVAL: return arrivalScreen ;
            case DEPARTURE: return departureScreen ;
            case CONFIRMATION: return confirmationScreen ;
            default: return null ;
        }
    }, model.screenProperty()));

    return flow ;
}

private Node load(String resource, ControllerFactory controllerFactory) {
    try {
        FXMLLoader loader = new FXMLLoader(getClass().getClassLoader().getResource(resource));
        loader.setControllerFactory(controllerFactory);
        return loader.load() ;
    } catch (IOException exc) {
        throw new UncheckedIOException(exc);
    }
}

with the ControllerFactory defined following the pattern at the beginning of the answer:

public class ControllerFactory implements Callback<Class<?>, Object> {

    private final BookingModel model ;

    public ControllerFactory(BookingModel model) {
        this.model = model ;
    }

    @Override
    public Object call(Class<?> type) {
        try {

            for (Constructor<?> c : type.getConstructors()) {
                if (c.getParameterCount() == 1 && c.getParameterTypes()[0] == BookingModel.class) {
                    return c.newInstance(model);
                }
            }

            // no appropriate constructor: just use default:
            return type.newInstance(); 

        } catch (Exception exc) {
            throw new RuntimeException(exc);
        }
    }

}

and this will work if we need multiple "flows":

public class BookingApplication extends Application {

    @Override
    public void start(Stage primaryStage) {

        SplitPane split = new SplitPane();
        split.getItems().addAll(createBookingFlow(), createBookingFlow());
        split.setOrientation(Orientation.VERTICAL);

        Scene scene = new Scene(split, 600, 600);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private Parent createBookingFlow() {
        // see above...
    }

    private Node load(String resource, ControllerFactory controllerFactory) {
        // see above...
    }

    public static void main(String[] args) {
        launch(args);
    }
}

Complete example as a gist.

It's not clear to me how to easily set this up using a Dependency Injection framework such as Spring. The issue is controlling the granularity of the creation of the booking model: you don't want it to have singleton scope (because different flows would need different models), but you don't want prototype scope either (because different controllers within the same flow need the same model). In a sense you would need something akin to "session" scope, though a session here is not a HttpSession but would be a custom session tied to the "flow". As far as I know there is no way in Spring to generalize the definition of a session; though others with more Spring expertise may have a way, and users of other DI frameworks may know if this is possible in those frameworks.

James_D
  • 201,275
  • 16
  • 291
  • 322
  • Thank you very much for your help. I will spend the next hours and learn the different approaches. Thanks James_D ! – sfdcdev Jan 19 '17 at 12:09
1

Have you looked into Guice custom scopes?

It seems that built in ones (Session and Singleton) will not meet you requirements so you will have to manually enter and exit the scope. Provided that you can manage them then the framework will guarantee a unique instance of the scoped dependency per scope

Luigi
  • 99
  • 5