joshsweaney
joshsweaney

Reputation: 57

Accessing Children in Custom FXML Component Init

If I have a custom JavaFX component like this (for e.g.):

public class MenuWidget extends VBox implements Initializable {
    @FXML
    StackPane menus;

    public MenuWidget() {
        FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/resources/MenuWidget.fxml"));
        fxmlLoader.setRoot(this);
        fxmlLoader.setController(this);

        try {
            fxmlLoader.load();
        } catch (IOException exception) {
            throw new RuntimeException(exception);
        }
    }

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        System.out.println(menus.getChildren().size());
    }
}

With this FXML:

<fx:root type="javafx.scene.layout.VBox" prefWidth="300.0" xmlns:fx="http://javafx.com/fxml/1">
    <StackPane fx:id="menus">
        <padding>
            <Insets top="5" left="5" bottom="5" right="5"></Insets>
        </padding>
    </StackPane>    
</fx:root>

And I use the custom component like this in another FXML file:

<MenuWidget>
    <menus>
        <fx:include source="FirstMenu.fxml" />
        <fx:include source="SecondMenu.fxml" />
    </menus>            
</MenuWidget>

Why does the Initialize() method in MenuWidget print 0? Essentially I need to access the children of the stackpane when the MenuWidget is constructed so that I can setup other menu controls of the top level menu (which I've removed from this example). Shouldn't the FXMLLoader populate the controller (the MenuWidget) with all its properties before the init method is called?

EDIT: Figured out the init is called before constructor finishes, so tried moving init code into the constructor (after the fmxmlLoader.load() call) and it still doesn't work.

Upvotes: 0

Views: 555

Answers (1)

Slaw
Slaw

Reputation: 46255

The MenuWidget class and its associated FXML file are entirely self-contained. You aren't including anything there and you are not adding any children to the StackPane. In other words, this:

public class MenuWidget extends VBox implements Initializable {
    @FXML
    StackPane menus;

    public MenuWidget() {
        FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/resources/MenuWidget.fxml"));
        fxmlLoader.setRoot(this);
        fxmlLoader.setController(this);

        try {
            fxmlLoader.load();
        } catch (IOException exception) {
            throw new RuntimeException(exception);
        }
    }

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        System.out.println(menus.getChildren().size());
    }
}

Loads this:

<fx:root type="javafx.scene.layout.VBox" prefWidth="300.0" xmlns:fx="http://javafx.com/fxml/1">
    <StackPane fx:id="menus">
        <padding>
            <Insets top="5" left="5" bottom="5" right="5"></Insets>
        </padding>
    </StackPane>    
</fx:root>

And once it's finished with that, the initialize method is invoked. Nothing there added anything to menus so the result of calling menus.getChildren().size() is of course 0.

Somewhere else you are loading this:

<MenuWidget>
    <menus>
        <fx:include source="FirstMenu.fxml" />
        <fx:include source="SecondMenu.fxml" />
    </menus>            
</MenuWidget>

Which causes a MenuWidget to be instantiated, which involves calling the MenuWidget#initialize method, and then attempts to add children to menus. To put it another way, if this was valid and working FXML, then the children would be added after the MenuWidget instance was created and initialized.

However, the <menus> element should be causing your application to throw an exception. The MenuWidget class does not define a read-only list property named menus. If you want to uses <menus>, and you want elements of that list to be added to the children of the menus stack pane, then modify your MenuWidget class to be:

public class MenuWidget extends VBox implements Initializable {
    @FXML
    StackPane menus;

    public MenuWidget() {
        FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/resources/MenuWidget.fxml"));
        fxmlLoader.setRoot(this);
        fxmlLoader.setController(this);

        try {
            fxmlLoader.load();
        } catch (IOException exception) {
            throw new RuntimeException(exception);
        }
    }

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        System.out.println(menus.getChildren().size());
    }

    // add read-only list property (the "property" is read-only, not 
    // the list itself) named "menus"
    public final ObservableList<Node> getMenus() {
        return menus.getChildren();
    }
}

But that seems conceptually wrong (at least to me). I'm not exactly sure what you're trying to do, but maybe you should be fx:include-ing the other FXML files directly into the MenuWidget FXML file, rather than what you're currently doing. That way you could inject the controllers and/or views (see nested controllers) into the MenuWidget class. I'm also not sure if the use of fx:root is entirely warranted in this case, based on what you've shown us. Inheriting from VBox does not seem to be adding any benefit to your code (i.e. you're not adding any functionality)—especially since you're only adding a single child to it (then adding children to that child). Perhaps a standard FXML file + controller would be more appropriate.

Upvotes: 3

Related Questions