Bela Baboso
Bela Baboso

Reputation: 61

JavaFX: Need help understanding setControllerFactory

I set up multiple custom controllers during the creation of an app and would need some help in organizing these controllers with setControllerFactory in JavaFX.

I'm fairly inexperienced with JavaFX but invested quite some time in creating a small app with Scenebuilder and JavaFX.

Background of the app The app consists of: - a map (implemented as an imageView) - a sidebar with buttons and icons for drag and drop events. - the map also has separate layers as the target for the drag and drop of different icon types.

As a prototype of my drag and drop event I used the instructions of Joel Graff (https://monograff76.wordpress.com/2015/02/17/developing-a-drag-and-drop-ui-in-javafx-part-i-skeleton-application/). He writes "in order for an object to be visible beyond a container’s edges, it must be a child of a parent or other ancestral container – it must belong to a higher level of the hierarchy. In the case of our drag-over icon, this means we had to add it as a child to the RootLayout’s top-level AnchorPane." and he uses dynamic roots for his project.

To teach myself how to use custom control with FXML I used Irina Fedortsova's tutorial https://docs.oracle.com/javafx/2/fxml_get_started/custom_control.htm. And to learn how to set up multiple screens I used the video https://www.youtube.com/watch?v=5GsdaZWDcdY and associating code from https://github.com/acaicedo/JFX-MultiScreen.

After building my app the logic tier of my app got more and more entangled with the presentation tier, and I feel as if my code would benefit greatly from some refactoring. My problem seems to be a lack in the understanding of the load and initialize process of controller classes. Since the drag icons and the RootLayout have to be loaded from the beginning, it is a mystery to me how I can load these classes in a way that I can call them again at a later time.

When I was looking for further solutions, I repeatedly came across the method setControllerFactory. Unfortunately I can't find a good explanation for how to use it properly and what it's specific purpose is. The only tutorial I found was: https://riptutorial.com/javafx/example/8805/passing-parameters-to-fxml---using-a-controllerfactory, unfortunately it seems to be a bit insufficient for my purpose.

I feel as if I would benefit the most from a methode/class with which I could organize all my custom controllers, load and initialize them at the appropriate time and then later access them again (similar to the interface and superclass in the video for JFX-MultiScreen).

Upvotes: 3

Views: 3241

Answers (1)

Ryan C
Ryan C

Reputation: 101

I repeatedly came across the method setControllerFactory. Unfortunately I can't find a good explanation for how to use it properly and what it's specific purpose is

By default, the FXMLLoader.load() method instantiates the controller named in the fxml document using the 0-arg constructor. The FXMLLoader.setControllerFactory​ method is used when you want your FXMLLoader object to instantiate controllers in a certain way, e.g. use a different controller constructor on specific arguments, call a method on the controller before it's returned, etc, as in

FXMLLoader loader = new FXMLLoader(...);
loader.setControllerFactory(c -> {
   return new MyController("foo", "bar");
});

Now when you call loader.load() the controller will be created as above. However, calling the FXMLLoader.setController​ method on a preexisting controller may be easier.

I feel as if I would benefit the most from a methode/class with which I could organize all my custom controllers, load and initialize them at the appropriate time and then later access them again

When I first came across this problem, as you have, I tried and retried many approaches. What I finally settled on was turning my main application class into a singleton. The singleton pattern is great when you need to create one instance of a class which should be accessible throughout your program. I know there are many people who will take issue with that (in that it's essentially a global variable with added structure), but I've found that it reduced complexity significantly in that I no longer had to manage a somewhat artificial structure of object references going every which way.

The singleton lets controllers communicate with your main application class by calling, for example, MyApp.getSingleton(). Still in the main application class, you can then organize all of your views in a private HashMap and add public add(...), remove(...), and activate(...) methods which can add or remove views from the map or activate a view in the map (i.e. set the scene's root to your new view).

For an application with many views that may be placed in different packages, you can organize their locations with an enum:

public enum View {
    LOGIN("login/Login.fxml"),
    NEW_USER("register/NewUser.fxml"),
    USER_HOME("user/UserHome.fxml"),
    ADMIN_HOME("admin/AdminHome.fxml");

    public final String location;

    View(String location) {
        this.location = "/views/" + location;
    }
}

Below is an example of the main application class:

public final class MyApp extends Application {

    // Singleton
    private static MyApp singleton;
    public MyApp() { singleton = this; }
    public static MyApp getSingleton() { return singleton; }

    // Main window
    private Stage stage;

    private Map<View, Parent> parents = new HashMap<>();

    @Override
    public void start(Stage primaryStage) {
        stage = primaryStage;
        stage.setTitle("My App");
        add(View.LOGIN);
        stage.setScene(new Scene(parents.get(View.LOGIN)));
        stage.show();
    }

    public void add(View view) {
        var loader = new FXMLLoader(getClass().getResource(view.location));

        try {
            Parent root = loader.load();
            parents.put(view, root);
        } catch (IOException e) { /* Do something */ }
    }

    public void remove(View view) {
        parents.remove(view);
    }

    public void activate(View view) {
        stage.getScene().setRoot(parents.get(view));
    }

    public void removeAllAndActivate(View view) {
        parents.clear();
        add(view);
        activate(view);
    }
}

If you have application-wide resources you can put them in the app class and add getters/setters so your controllers can access them. Here is an example controller class:

public final class Login implements Initializable {

    MyApp app = MyApp.getSingleton();

    // Some @FXML variables here..

    @FXML private void login() {
        // Authenticate..
        app.removeAllAndActivate(View.USER_HOME);
    }

    @FXML private void createAccount() {
        app.add(View.NEW_USER);
        app.activate(View.NEW_USER);
    }

    @Override
    public void initialize(URL url, ResourceBundle rb) {}
}

Upvotes: 10

Related Questions