0

I am making a JavaFX application and which has four root controllers. Each of these controls the view on a tab. There is no controller for the tab selector as it has no functionality other than to select which of the four views to show. These controllers are required to have access to identical data and they can all modify this data.

The only way that I can see to achieve this in a way that is readable is with a static singleton which stores state that can be accessed and modified by all four controllers. For obvious reasons, this is not ideal.

I cannot use dependency injection as all controllers are initialised when the app is launched, they are not created by one another. I cannot use the observer patter either for the same reason. They have no way to obtain a reference to each other and thus they cannot observe each other. As far as I know, there is no broadcast notification system in Java which is annoying as it would be a solution. Is there another pattern that I could use or some way I could make one of these pattern work?

The fxml files for the tabs are loaded as content in the tab pane. It is done this way so that I can have a different controller for each tab. The start method then loads the tab pane fxml.

<TabPane prefHeight="200.0" prefWidth="200.0"   tabClosingPolicy="UNAVAILABLE" BorderPane.alignment="CENTER">
        <tabs>
          <Tab text="Data &amp; Statistics">
            <content>
              <fx:include fx:id="dataViewTabControl" source="DataViewTabControl.fxml" />
            </content>
          </Tab>
            . . .
Matt
  • 688
  • 5
  • 15
  • 1
    Possible duplicate of [Passing Parameters JavaFX FXML](http://stackoverflow.com/questions/14187963/passing-parameters-javafx-fxml) – fabian Sep 12 '16 at 07:11
  • These are controllers in the FXML-sense? Where are the four FXML files for the four tabs loaded? – James_D Sep 12 '16 at 08:28
  • They are loaded by the tab pane as content in the tabs like this: – Matt Sep 12 '16 at 08:31

1 Answers1

2

You can effectively use dependency injection by setting a controller factory on the FXMLLoader. This allows you to control how the controller instances are created for the FXML files that specify a controller class fx:controller attribute. The controller factory is used for included FXML files as well as for the main FXML file.

Thus you can create a model class with the application state, observe and modify that state from the controllers, and create a single instance which is shared among all the controllers.

Define your model:

public class Model { /* ... */ }

So if you define your DataViewTabControl.fxml file with a controller, such as:

<?xml version="1.0" encoding="UTF-8"?>

<!-- imports etc -->

<SomeRootPane fx:controller="com.example.DataViewTabController">

    <!-- ... -->

</SomeRootPane>

And define your DataViewTabController to take a Model parameter in the constructor:

public class DataViewTabController {

    private final Model model ;

    public DataViewTabController(Model model) {
        this.model = model ;
    }

}

Now when you load your main FXML file, and any other file that may have either controllers that take a model as a constructor parameter or <fx:include>s that do the same, use a controller factory that invokes the correct constructor:

Model model = new Model();

FXMLLoader loader = new FXMLLoader(getClass().getResource("/path/to/fxml/file"));

loader.setControllerFactory((Class<?> controllerType) -> {
    try {
        for (Constructor<?> con : controllerType.getConstructors()) {
            if (con.getParameterCount() == 1 && con.getParameterTypes()[0]==Model.class) {
                return con.newInstance(model);
            }
        }
        // no suitable constructor found: just return default instance
        return controllerType.newInstance();
    } catch (Exception e) {
        System.err.println("Warning: could not load controller");
        e.printStackTrace(System.err);
        return null ;
    }
});

Now all your controllers have access to the same Model instance, so you can use the usual MVC/Observer patterns to change data and respond to changes in data. This is particularly easy if your model class uses the JavaFX property API.

James_D
  • 201,275
  • 16
  • 291
  • 322
  • This is perfect. I had never even heard of a controller factory. – Matt Sep 12 '16 at 11:29
  • @Matt You can use this technique to combine JavaFX with a dependency injection framework too. E.g. if you use Spring, make all your controllers spring-managed beans (so you can inject your model instance in directly). Then given `ApplicationContext context;` you can just do `fxmlLoader.setControllerFactory(context::getBean);`. Then the FXMLLoader will request all controller instances from the spring application context. – James_D Sep 12 '16 at 12:17
  • I'll give it a try. Thanks for the help. – Matt Sep 12 '16 at 22:41