Search code examples
javafxfxml

Where to include nested controller in main fxml file


I'm not sure where I should be putting my <fx:include fx:id="NTB" source="NTB.fxml" /> in my main .fxml file. I'll include the relevant files at the bottom.

File Structure:

  • Driver class: Test.java
  • fxml for Driver: Test.fxml
  • Controller for Driver's fxml: Controller.java
  • fxml for secondary window: NTB.fxml
  • Controller for secondary window: NTB.java

Test.java displays a TableView as well as a toolbar that contains some buttons. When the newTask button is clicked I'm trying to have it call the display() method from NTB.java which will display a new window that takes some input from the user and stores it in a list that's visible to that class. Once the user clicks the done button in that new window the window closes and updates the table contents.

Currently with how these files are setup the program will compile and run and when the newTask button is clicked it produces an error but doesn't crash which is expected since the NTBController is null by default since the <fx:include /> tag isn't present yet.

The main issue I'm having is that so far no matter where I've put the <fx:include /> tag in Test.fxml the program doesn't launch at produces an error that points to the line the include tag is on in Test.fxml, the last line of the BorderPane definition in NTB.fxml, and the loadFXML & start function in Test.java.

Relevant files:

Test.fxml:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>


<BorderPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="1080.0" prefWidth="1920.0" fx:controller="com.test.Controller" xmlns="http://javafx.com/javafx/13" xmlns:fx="http://javafx.com/fxml/1">
   <fx:include fx:id="nTB" source="NTB.fxml" />
   <center>
      <TableView fx:id="table" prefHeight="352.0" prefWidth="543.0">
        <columns>
          <TableColumn fx:id="orderCol" prefWidth="75.0" text="Order" />
          <TableColumn fx:id="nameCol" prefWidth="75.0" text="Name" />
            <TableColumn fx:id="xCol" prefWidth="75.0" text="X" />
            <TableColumn fx:id="yCol" prefWidth="75.0" text="Y" />
            <TableColumn fx:id="buttonCol" prefWidth="75.0" text="Button" />
            <TableColumn fx:id="keyCol" prefWidth="75.0" text="Key Code" />
            <TableColumn fx:id="descriptionCol" prefWidth="75.0" text="Description" />
            <TableColumn fx:id="delayCol" prefWidth="75.0" text="Delay" />
        </columns>
         <columnResizePolicy>
            <TableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
         </columnResizePolicy>
      </TableView>
   </center>
   <top>
      <HBox alignment="CENTER" prefHeight="34.0" prefWidth="600.0">
         <children>
            <Button fx:id="newTask" mnemonicParsing="false" onAction="#newTaskDisplay" text="New"  />
            <Button fx:id="editTask" mnemonicParsing="false" onAction="#editTasksOnA" text="Edit" />
            <Button fx:id="deleteTask" mnemonicParsing="false" onAction="#deleteTasksOnA" text="Delete" />
            <Button fx:id="runTasks" mnemonicParsing="false" onAction="#runTasksOnA" text="Run" />
            <Label fx:id="numRunsLab" text="Iterations:" />
            <TextField fx:id="numRuns" prefHeight="39.0" prefWidth="90.0" promptText="1" />
            <Button fx:id="clearTasks" mnemonicParsing="false" onAction="#clearTasksOnA" text="Clear Tasks" />
            <Button fx:id="saveTasks" mnemonicParsing="false" onAction="#saveTasksOnA" text="Save" />
            <Button fx:id="openTasks" mnemonicParsing="false" onAction="#openTasksOnA" text="Open" />
         </children>
      </HBox>
   </top>
   <right>
      <VBox alignment="CENTER" prefHeight="945.0" prefWidth="89.0">
         <children>
            <Button fx:id="moveToTop" mnemonicParsing="false" text="Move Top" />
            <Button fx:id="moveUp" mnemonicParsing="false" text="Move Up" />
            <Button fx:id="moveDown" mnemonicParsing="false" text="Move Down" />
            <Button fx:id="moveToBottom" mnemonicParsing="false" text="Move Bottom" />
         </children>
      </VBox>
   </right>
</BorderPane>

NTB.fxml:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.control.TextArea?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.ChoiceBox?>
<?import javafx.stage.Stage?>

<Stage fx:id="window" fx:controller="com.test.NTB" xmlns="http://javafx.com/javafx/13" xmlns:fx="http://javafx.com/fxml/1" >
    <BorderPane >
        <center>
            <VBox alignment="CENTER">
                <HBox alignment="CENTER">
                    <padding>
                        <Insets top="20.0" right="10.0" bottom="0.0" left="5.0" />
                    </padding>
                    <VBox alignment="CENTER">
                        <children>
                            <Label fx:id="nameLabel" text="Name: " />
                        </children>
                    </VBox>
                    <VBox alignment ="CENTER">
                        <children>
                            <TextField fx:id="nameField" promptText="Name..." />
                        </children>
                    </VBox>
                </HBox>
                <HBox alignment="CENTER">
                    <padding>
                        <Insets top="20.0" right="10.0" bottom="0.0" left="5.0" />
                    </padding>
                    <VBox alignment="CENTER">
                        <children>
                            <Label fx:id="xLabel" text="X Coord: " />
                        </children>
                    </VBox>
                    <VBox>
                        <children>
                            <TextField fx:id="xField" promptText="0.0" />
                        </children>
                    </VBox>
                </HBox>
                <HBox alignment="CENTER">
                    <padding>
                        <Insets top="20.0" right="10.0" bottom="0.0" left="5.0" />
                    </padding>
                    <VBox alignment="CENTER">
                        <children>
                            <Label fx:id="yLabel" text="Y Coord: " />
                        </children>
                    </VBox>
                    <VBox>
                        <children>
                            <TextField fx:id="yField" promptText="0.0" />
                        </children>
                    </VBox>
                </HBox>
                <HBox alignment="CENTER">
                    <padding>
                        <Insets top="20.0" right="10.0" bottom="0.0" left="5.0" />
                    </padding>
                    <VBox alignment="CENTER">
                        <children>
                            <Label fx:id="buttonLabel" text="Button: " />
                        </children>
                    </VBox>
                    <VBox>
                        <children>
                            <ChoiceBox fx:id="buttonBox" value="MouseButton.PRIMARY" />
                        </children>
                    </VBox>
                </HBox>
                <HBox alignment="CENTER">
                    <padding>
                        <Insets top="20.0" right="10.0" bottom="0.0" left="5.0" />
                    </padding>
                    <VBox alignment="CENTER">
                        <children>
                            <Label fx:id="keyLabel" text="Key: " />
                        </children>
                    </VBox>
                    <VBox>
                        <children>
                            <TextField fx:id="keyField" maxWidth="25.0" promptText="A" />
                        </children>
                    </VBox>
                </HBox>
                <HBox alignment="CENTER">
                    <padding>
                        <Insets top="20.0" right="10.0" bottom="0.0" left="5.0" />
                    </padding>
                    <VBox alignment="CENTER">
                        <children>
                            <Label fx:id="descriptionLabel" text="Description: " />
                        </children>
                    </VBox>
                    <VBox>
                        <children>
                            <TextArea fx:id="descriptionArea" promptText="Enter description..." maxWidth="150" maxHeight="100" />
                        </children>
                    </VBox>
                </HBox>
                <HBox alignment="CENTER">
                    <padding>
                        <Insets top="20.0" right="10.0" bottom="0.0" left="5.0" />
                    </padding>
                    <VBox alignment="CENTER">
                        <children>
                            <Label fx:id="delayLabel" text="Delay: " />
                        </children>
                    </VBox>
                    <VBox>
                        <children>
                            <TextField fx:id="delayField" promptText="0" />
                        </children>
                    </VBox>
                </HBox>
            </VBox>
        </center>
        <bottom>
            <HBox alignment="CENTER">
                <Button fx:id="doneButton" onAction="#doneButtonOnA" text="Done" />
                <Button fx:id="cancelButton" onAction="#cancelButtonOnA" text="Cancel" />
            </HBox>
        </bottom>
    </BorderPane>
</Stage>

Controller.java:

package com.test;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.PrintWriter;
import java.net.URL;
import java.util.ResourceBundle;
import java.util.Scanner;

import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.input.KeyCode;
import javafx.scene.input.MouseButton;
import javafx.stage.FileChooser;
import javafx.stage.Window;

public class Controller extends Test implements Initializable
{
    @FXML private TableView<Task> table;
    @FXML private TableColumn<Task, Integer> orderCol;
    @FXML private TableColumn<Task, String> nameCol;
    @FXML private TableColumn<Task, Double> xCol;
    @FXML private TableColumn<Task, Double> yCol;
    @FXML private TableColumn<Task, MouseButton> buttonCol;
    @FXML private TableColumn<Task, String> keyCol;
    @FXML private TableColumn<Task, String> descriptionCol;
    @FXML private TableColumn<Task, Long> delayCol;
    @FXML private TextField numRuns;
    @FXML private Window nTB;
    @FXML private NTB nTBController;
    
    @Override
    public void initialize(URL url, ResourceBundle resources)
    {   
        numRuns.textProperty().addListener((observable, oldValue, newValue) -> {
            if(newValue.matches("\\d*")) return;
            numRuns.setText(newValue.replaceAll("[^\\d]", ""));
        });
        orderCol.setCellValueFactory(column-> new ReadOnlyObjectWrapper<Integer>(table.getItems().indexOf(column.getValue()) + 1));
        nameCol.setCellValueFactory(new PropertyValueFactory<Task, String>("name"));
        xCol.setCellValueFactory(new PropertyValueFactory<Task, Double>("x"));
        yCol.setCellValueFactory(new PropertyValueFactory<Task, Double>("y"));
        buttonCol.setCellValueFactory(new PropertyValueFactory<Task, MouseButton>("button"));
        keyCol.setCellValueFactory(new PropertyValueFactory<Task, String>("keyCode"));
        descriptionCol.setCellValueFactory((new PropertyValueFactory<Task, String>("description")));
        delayCol.setCellValueFactory(new PropertyValueFactory<Task, Long>("delay"));

        table.getItems().setAll(list);
    }

    @FXML private void newTaskDisplay()
    {
        nTBController.display();
        table.getItems().setAll(list);
    }

Test.java:

package com.test;

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

import java.io.IOException;

public class Test extends Application {

    private static Scene scene;
    protected static Stage stage;

    protected static ObservableList<Task> list = FXCollections.observableArrayList();

    @Override
    public void start(Stage primStage) throws IOException {
        stage = primStage;
        scene = new Scene(loadFXML("Test"), 640, 480);
        scene.getStylesheets().add(getClass().getResource("Test.css").toExternalForm());

        stage.setScene(scene);
        stage.show();
    }

    static void setRoot(String fxml) throws IOException {
        scene.setRoot(loadFXML(fxml));
    }

    private static Parent loadFXML(String fxml) throws IOException {
        FXMLLoader fxmlLoader = new FXMLLoader(Test.class.getResource(fxml + ".fxml"));
        return fxmlLoader.load();
    }

    public static void main(String[] args) {
        launch();
    }

}

Error on run:

Exception in Application start method
java.lang.reflect.InvocationTargetException
        at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:119)   
        at java.base/java.lang.reflect.Method.invoke(Method.java:577)
        at javafx.graphics/com.sun.javafx.application.LauncherImpl.launchApplicationWithArgs(LauncherImpl.java:464)
        at javafx.graphics/com.sun.javafx.application.LauncherImpl.launchApplication(LauncherImpl.java:363)
        at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
        at java.base/java.lang.reflect.Method.invoke(Method.java:577)
        at java.base/sun.launcher.LauncherHelper$FXHelper.main(LauncherHelper.java:1081)
Caused by: java.lang.RuntimeException: Exception in Application start method
        at javafx.graphics/com.sun.javafx.application.LauncherImpl.launchApplication1(LauncherImpl.java:900)
        at javafx.graphics/com.sun.javafx.application.LauncherImpl.lambda$launchApplication$2(LauncherImpl.java:195)     
        at java.base/java.lang.Thread.run(Thread.java:833)
Caused by: javafx.fxml.LoadException: Element does not define a default property.
/C:/Users/dylan/Desktop/testing/test/target/classes/com/test/NTB.fxml:133
/C:/Users/dylan/Desktop/testing/test/target/classes/com/test/Test.fxml:15

        at javafx.fxml/javafx.fxml.FXMLLoader.constructLoadException(FXMLLoader.java:2621)
        at javafx.fxml/javafx.fxml.FXMLLoader$Element.set(FXMLLoader.java:187)
        at javafx.fxml/javafx.fxml.FXMLLoader$ValueElement.processEndElement(FXMLLoader.java:798)
        at javafx.fxml/javafx.fxml.FXMLLoader.processEndElement(FXMLLoader.java:2838)
        at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2557)
        at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2466)
        at javafx.fxml/javafx.fxml.FXMLLoader$IncludeElement.constructValue(FXMLLoader.java:1154)
        at javafx.fxml/javafx.fxml.FXMLLoader$ValueElement.processStartElement(FXMLLoader.java:754)
        at javafx.fxml/javafx.fxml.FXMLLoader.processStartElement(FXMLLoader.java:2722)
        at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2552)
        at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2466)
        at javafx.fxml/javafx.fxml.FXMLLoader.load(FXMLLoader.java:2435)
        at com.test/com.test.Test.loadFXML(Test.java:36)
        at com.test/com.test.Test.start(Test.java:23)
        at javafx.graphics/com.sun.javafx.application.LauncherImpl.lambda$launchApplication1$9(LauncherImpl.java:846)    
        at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runAndWait$12(PlatformImpl.java:455)
        at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$10(PlatformImpl.java:428)
        at java.base/java.security.AccessController.doPrivileged(AccessController.java:399)
        at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$11(PlatformImpl.java:427)
        at javafx.graphics/com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:96)
        at javafx.graphics/com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
        at javafx.graphics/com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(WinApplication.java:174)
        ... 1 more
Exception running application com.test.Test

Tag to include in Test.fxml: <fx:include fx:id="NTB" source="NTB.fxml" />

If there's any other info that would help with this please ask and I'd be happy to provide.

I'm also not dead set on having this be the final structure of this program so if this is unnecessarily difficult to do and there's a simpler way to accomplish the same thing please let me know. I'm new to using fxml with JavaFX so I'm not sure of the best ways to do things quite yet.


Solution

  • The Error

    Here is the error:

    Caused by: javafx.fxml.LoadException: Element does not define a default property.
    /C:/Users/dylan/Desktop/testing/test/target/classes/com/test/NTB.fxml:133
    /C:/Users/dylan/Desktop/testing/test/target/classes/com/test/Test.fxml:15
    

    This error (specifically) is not about the location of the fx:include element. The problem is an element in the NTB.fxml file does not define a default property. And it says the error is occurring on line 133 of the FXML file. That line is the closing </BorderPane> tag. And this makes sense, because the parent element is defining a Stage, which indeed does not define a default property.

    Default Properties

    When you have something like this:

    <VBox>
        <Label/>
    </VBox>
    

    That puts the Label in the children list of the VBox. The reason this works is because Pane is annotated with @DefaultProperty("children"). That annotation is @Inherited, so VBox has the same annotation. Thus, the elements that are directly inside the <VBox> element are applied to the children property. If that annotation wasn't present, then you'd need to do:

    <VBox>
        <children>
            <Label/>
        </children>
    </VBox>
    

    The Stage class does not have a @DefaultProperty annotation. Neither do any of its superclasses. That means you can't just place elements directly inside a <Stage> element. You have to tell FXML which property you're trying to set.

    Fix NBT.fxml

    And that leads to another problem. You can't place a BorderPane in a Stage. You would need to wrap that as the root of a Scene first. Something like:

    <Stage>
        <scene>
            <Scene width="-1" height="-1">
                <!-- The Scene class is annotated with DefaultProperty("root") -->
                <BorderPane>
                    <!-- your stuff -->
                </BorderPane>
            </Scene>
        </scene>
    </Stage>
    

    Cannot embed a Stage

    Unfortunately, this leads to yet another problem. You are trying to embed NBT.fxml inside another FXML file. You can't place a Stage inside another Stage (let alone another Node). You either need to rethink your design or you need to define the root element of NBT.fxml to be some kind of Node instead of a Stage.

    If you decide to continue using fx:include here (meaning you changed the root element to be a type of Node), then keep in mind that BorderPane inherits its @DefaultProperty annotation from Pane. That means its default property is children. Adding a node directly to the children of a border pane should not be done; that child would not be managed properly. Make sure to move the fx:include element to be inside a <top>, <left>, <bottom>, <right>, or <center> element.

    Possible Workaround to "embed" a Stage (Untested)

    If you want to keep the root element of NBT.fxml as a Stage, then there might be something that can work. I have not tried this though, so I don't know if it's legal. Try putting the fx:include inside an fx:define element:

    <BorderPane ...>
        <fx:define>
            <fx:include fx:id="nTB" source="NTB.fxml" /> 
        </fx:define>
        <!-- Your stuff -->
    </BorderPane>
    

    This assumes you fixed the default property issue in NBT.fxml.

    Better solution

    That said, if your NBT.fxml file defines a Stage, then honestly I think it's better to just not fx:include it. Simply load that FXML file inside the controller of Test.fxml only when you need to display that new window. In other words:

    @FXML
    private void newTaskDisplay() throws IOException {
        FXMLLoader loader = new FXMLLoader(getClass().getResource("NBT.fxml"));
        Stage stage = loader.load();
        stage.show();
    }