J-bob J-bob - 1 month ago 5
Java Question

FXML: "Run after initialized"

I have a JavaFX application that uses FXML alongside a controller class written in Java. In the Java controller I need to take care not to operate on an FXML

Node
element until it's been initialized (otherwise I'll get a NullPointerException), which isn't guaranteed until the
initialize
method is run. So I find myself doing this a lot:

The controller is set in the FXML file like this:

<Pane fx:controller="Controller" ...>
...
</Pane>


And then here's the controller in the Java file.

class Controller{
@FXML
Pane aPane;
int globalValue;

public void setSomething(int value){
globalValue = value;
if(!(aPane == null)){ //possibly null if node not initialized yet
aPane.someMethod(globalValue)
}
}

@FXML
void initialize(){
aPane.someMethod(globalValue) //guaranteed not null at this point
}


}


This works, but it's clunky and repetitive. I have to create the
globalValue
attribute just in case the
setSomething
method is called before
initialize
has been called, and I have to make sure the operations in my
setSomething
method are identical to the operations in
initialize
.

Surely there's a more elegant way to do this. I know that JavaFX has the
Platform.runlater(...)
method that guarantees something will be run on the main application thread. Perhpas there's something like
Platform.runAfterInitialize(...)
that waits until initialization, or runs immediately if initialization already happened? Or if there's another way to do it I'm open to suggestions.

Answer

If you specify the controller in the FXML file with fx:controller="Controller", then when you call FXMLLoader.load(...), the FXMLLoader:

  1. parses the FXML file
  2. creates an instance of Controller by (effectively) calling its no-arg constructor (or, in advanced usage, by invoking the controller factory if you set one)
  3. creates the UI elements corresponding to the elements in the FXML file
  4. injects any elements with an fx:id into matching fields in the controller instance
  5. registers event handlers
  6. invokes initalize() on the controller instance (if such a method is defined)
  7. returns the UI element corresponding to the root of the FXML hierarchy

Only after load() completes (i.e. after the @FXML-annotated fields are injected) can you get a reference to the controller with loader.getController(). So it is not possible (aside from doing something extremely unusual in a controller factory implementation) for you to invoke any methods on the controller instance until after the @FXML-injected fields are initialized. Your null checks here are redundant.


On the other hand, if you use FXMLLoader.setController(...) to initialize your controller, in which case you must not use fx:controller, you can pass the values to the constructor. Simply avoiding calling a set method on the controller before passing the controller to the FXMLLoader means you can assume any @FXML-annotated fields are initialized in the controller's public methods:

class Controller{
    @FXML
    Pane aPane;
    int globalValue;

    public Controller(int globalValue) {
        this.globalValue = globalValue ;
    }

    public Controller() {
        this(0);
    }

    public void setSomething(int value){
        globalValue = value;
        aPane.someMethod(globalValue)
    }

    @FXML
    void initialize(){
        aPane.someMethod(globalValue) //guaranteed not null at this point
    }


}

and

FXMLLoader loader = new FXMLLoader(getClass().getResource("path/to/fxml"));
Controller controller = new Controller(42);
loader.setController(controller);
Node root = loader.load();