Search code examples
javafxincludefxml

JavaFX FXML include fxml causes NullPointerException


I want to extract a button to a new fxml file and change the main label with it. Without extraction it works perfectly.
main.fxml:

<VBox xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1"
      fx:controller="org.example.MainController">
   <Label fx:id="label" text="default"/>
   <Button onAction="#changeLabel" text="sayHello" />
</VBox>

MainController:

public class MainController {
    @FXML
    private Label label;

    @FXML
    private void changeLabel() {
        label.setText("Changed");
    }
}

With extraction I get NullPointerException in MainController.changeLabel()
main.fxml with include:

<VBox xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1"
      fx:controller="org.example.MainController">
   <Label fx:id="label" text="default"/>
   <fx:include source="button.fxml"/>
</VBox>


button.fxml:

<AnchorPane xmlns="http://javafx.com/javafx/11.0.1"
            xmlns:fx="http://javafx.com/fxml/1"
            fx:controller="org.example.MainController">
    <Button onAction="#changeLabel" text="sayHello" />
</AnchorPane>

What can cause this NPE?


Solution

  • You should (almost?) always use a different class for controllers for different FXML files. (The only exception I can think of is if you want to define different FXML files to represent different layouts for the same controls.)

    One approach is to inject the controller for the included FXML (the "Nested Controller") into the main controller. (See documentation.)

    public class MainController {
        @FXML
        private Label label;
    
        @FXML
        private ButtonController buttonController ;
    
        @FXML
        private void initialize() {    
            buttonController.setOnButtonPressed(this::changeLabel);
        }
    
        private void changeLabel() {
            label.setText("Changed");
        }
    }
    
    public class ButtonController {
    
        private Runnable onButtonPressed ;
    
        public void setOnButtonPressed(Runnable onButtonPressed) {
            this.onButtonPressed = onButtonPressed ;
        }
    
        public Runnable getOnButtonPressed() {
            return onButtonPressed ;
        }
    
        @FXML
        private void changeLabel() {
            if (onButtonPressed != null) {
                onButtonPressed.run();
            }
        }
    }
    

    And then the FXML files look like

    <VBox xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1"
          fx:controller="org.example.MainController">
       <Label fx:id="label" text="default"/>
       <fx:include fx:id="button" source="button.fxml"/>
    </VBox>
    

    and

    <VBox xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1"
          fx:controller="org.example.ButtonController">
       <Label fx:id="label" text="default"/>
       <fx:include source="button.fxml"/>
    </VBox>
    

    Generally speaking, it's a bad idea for controllers to have references to each other, as it breaks encapsulation and adds unnecessary dependencies. A better approach is to use a MVC design.

    public class Model {
    
        private final StringProperty text = new SimpleStringProperty() ;
    
        public StringProperty textProperty() {
            return text ;
        }
    
        public final String getText() {
            return textProperty().get();
        }
    
        public final void setText(String text) {
            textProperty().set(text);
        }
    }
    

    Now you can do

    public class MainController {
        @FXML
        private Label label;
    
        private final Model model ;
    
        public MainController(Model model) {
            this.model = model ;
        }
    
        @FXML
        private void initialize() {    
            label.textProperty().bind(model.textProperty());
        }
    
    }
    

    and

    public class ButtonController {
    
        private final Model model ;
    
    
        public ButtonController(Model model) {
            this.model = model ;
        }
    
        @FXML
        private void changeLabel() {
            model.setText("Changed");
        }
    }
    

    The FXML files are as above, and you need to specify a controller factory when you load the FXML (so that the controllers are instantiated by passing the model instance to the constructors):

    final Model model = new Model();
    
    FXMLLoader loader = new FXMLLoader(getClass().getResource("/path/to/main.fxml");
    loader.setControllerFactory(type -> {
        if (type.equals(MainController.class))   return new MainController(model);
        if (type.equals(ButtonController.class)) return new ButtonController(model);
        throw new IllegalArgumentException("Unexpected controller type: "+type);
    });
    Parent root = loader.load();
    // ...