JavaFX. Where to put service class reference? Controller or Main app starter class?

1.6k views Asked by At

I work with JavaFX application and use FXML to implement MVC pattern. I already make proof of concept, and now start to create JavaFX user interface.

In my previous experience with spring MVC, there was usual to create service and inject them in controller class via annotations. But with JavaFX I cannot find any recommendations how to do that. Also I not sure am I have to put services to controller, or call a main class method from controller. The second solution holds service reference in main application class.

Please note my application runs service classes in concurrent threads. So all of them implements Runnable interface

1

There are 1 answers

0
James_D On

I would avoid having the controllers needing to refer to the main application class as it introduces an extra dependency that is not really necessary. So have each controller keep a reference to the service object.

To provide the service to the controllers, you can basically use one of the techniques outlined in this question.

There are basically three ways to do this:

Creating the controller and setting it in the FXMLLoader directly

In this version, you do not use the fx:controller attribute in the root element of your FXML file (doing so will cause an exception to be thrown).

Given

public interface Service { ... }

and

public class SomeController {

    private final Service service ;

    public SomeController(Service service) {
        this.service = service ;
    }

    // ...
}

Then you can load the FXML file with

Service service = ... ;
FXMLLoader loader = new FXMLLoader(getClass().getResource("path/to/fxml/file.fxml"));
SomeController controller = new SomeController(Service.class);
loader.setController(controller);
Parent uiRoot = loader.load();

Retrieving the controller from the FXMLLoader and setting the service

If you want to be able to use the fx:controller attribute, your controller class must have a no-argument constructor. In this case, you can set the service on the controller after the FXMLLoader has completed loading. This looks like:

public class SomeController {

    private Service service ;

    public void initService(Service service) {
        this.service = service ;
        // update UI with values from service...
    }

    // ...
}

Note that here you will likely have to refactor some code from the initialize() method, as that code may depend on the service, which will not have been set when initialize() is invoked. Just move any such code to the initService(...) method. Loading the FXML file now looks like

Service service = ... ;
FXMLLoader loader = new FXMLLoader(getClass().getResource("path/to/fxml/file.fxml"));
Parent uiRoot = loader.load();
SomeController controller = loader.getController();
controller.initService(service);

Using a controller factory

The third approach uses a controller factory. This is slightly more complex, but has some advantages. In particular, if your fxml file uses fx:include tags, the controller factory will be reused when the included fxml files are loaded, so those controllers can have a service object initialized as well. Managing included fxml files with the above two approaches is possible, but a little convoluted.

The controller factory is essentially a function that maps a Class<?> to the controller that should be used (presumably one of that class, though there is requirement for that). The default controller factory just invokes newInstance() on the Class<?> object (which is why you need a no-arg constructor). Here is a general controller factory implementation that calls a constructor taking a Service parameter if one exists, and calls the no-arg constructor if not.

Service service = ... ;

Callback<Class<?>, Object> controllerFactory = type -> {
    try {
        for (Constructor<?> c : type.getConstructors()) {
            if (c.getParameterCount() == 1 
                && c.getParameterTypes()[0] == Service.class) {

                return c.newInstance(service);
            }
        }
        return type.newInstance();
    } catch (Exception e) {
        throw new RuntimeException(e);
   }
};

You can create this once and use it for any FXML you load (note that it references a single Service instance):

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

This will work with the fx:controller attribute even if it refers to a class with a constructor taking a Service parameter (such as the first controller example above).

If you are used to dependency-injection frameworks, you might be interested in afterburner.fx by Adam Bien. This works by setting a controller factory that examines the controller class for @Inject annotations and sets those values on the controller, so all you have to do is annotate the service field in the controller and follow the specific afterburner.fx naming conventions, and everything happens automagically.

I also recommend this article, also by Adam Bien, which discusses some strategies for communicating with services from your controller (including handling concurrency issues).