Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

FXML: "Run after initialized"

Tags:

java

javafx

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.

like image 934
J-bob Avatar asked Oct 25 '16 18:10

J-bob


1 Answers

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();
like image 143
James_D Avatar answered Oct 20 '22 00:10

James_D