Java – Set custom FXML properties as parameters for custom javafx component

fxmljavajavafx

I created custom component TableBlock. It consists of a Label and TableView. TableViewcan have, for example, from 1 to 1000 rows. Number of rows is defined by parameter "rowsFromPrefs" in FXML file. This parameter is needed for creation of TableView. TableView is fully created by JAva code, in fxml is just its tag and parameter with a number of rows.

As i know, when JavaFX constructs FXML component, it first calls constructor, then @FXML annotated fields, then starts initialize() method.

In my case when initialize() starts, variable rowsFromPrefs still is null! But, if i try to get the value of rowsFromPrefs from other thread (not JavaFX-launcher), i see that it defined = "2" like it should be.

So i can`t understand at what moment Java assigns object paramters from FXML file. How can i pass parameter from fxml file to object when it is being created.

I saw @NamedArg annotation for constructor parameters. Is it the only one way of passing parameter when objects are creating?

the controller can define an initialize() method, which will be called once on >an implementing controller when the contents of its associated document have >been completely loaded:

TableBlock.java

public class TableBlock extends VBox{
    @FXML
    private String rowsFromPrefs;
    @FXML
    private Label label;

public TableBlock() {
    FXMLLoader fxmlLoader = new   FXMLLoader(getClass().getResource("TableBlock.fxml"));
    fxmlLoader.setRoot(this);
    fxmlLoader.setController(this);
    try {
        fxmlLoader.load();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

@FXML
public void initialize() {
    this.table = createTable(rowsFromPrefs);
}

public String getRowsFromPrefs() {
    System.out.println("getRowsFromPrefs");
    return rowsFromPrefs;
}


public void setRowsFromPrefs(String rowsFromPrefs) {
    this.rowsFromPrefs = rowsFromPrefs;
}

}

TableBlock.fxml

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

<?import javafx.scene.control.*?>
<?import ru.laz.model.controls.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.layout.AnchorPane?>
<?import ru.laz.model.controls.tableblock.*?>


<fx:root type="javafx.scene.layout.VBox" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
   <children>
      <Label text="Label" />
   </children>
</fx:root>

View.java

public class View extends Application {
Parent root = null;
private Scene scene;

@Override
    public void init() {
    try {
            root = FXMLLoader.load(getClass().getResource("View.fxml"));
            root.requestLayout();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
}

@Override
    public void start(final Stage stage) throws Exception {
     scene = new Scene(root, 640, 480, Color.LIGHTGRAY);
     stage.show();
}

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

}

View.fxml

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

<?import java.lang.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.control.*?>
<?import ru.laz.model.controls.tableblock.*?>


<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
   <children>
      <TableBlock rowsFromPrefs="2" id="IDDQD"/>
   </children>
</AnchorPane>

Best Answer

First, note that the @FXML annotation on rowsFromPrefs is serving no purpose. @FXML causes a value to be injected for the field when the FXML file for which the current object is the controller has an element with an fx:id attribute whose value matches the field name. Since TableBlock.fxml has no element with fx:id="rowsFromPrefs", this annotation isn't doing anything.

When the FXMLLoader that is loading View.fxml encounters the <TableBlock> element, it creates a TableBlock instance by calling its constructor. Then it will set the values specified by the attributes. So your FXML element

<TableBlock rowsFromPrefs="2" id="IDDQD"/>

is essentially equivalent to

TableBlock tableBlock = new TableBlock();
tableBlock.setRowsFromPrefs("2");
tableBlock.setId("IDDQD");

Of course, the constructor for TableBlock just does what the code says to do: it creates a FXMLLoader, sets the root and controller for that FXMLLoader, and then calls load(). The load process for that FXMLLoader will set the @FXML-injected fields on the controller (the TableBlock object whose constructor is executing), and then call initialize().

So initialize() is invoked as part of the call to FXMLLoader.load() that is in the TableBlock constructor; of course this all happens before setRowsFromPrefs("2"); is invoked.

So in summary, TableBlock.initialize() is called after TableBlock.fxml has been parsed, and any elements defined there injected into their corresponding @FXML-annotated fields, but this happens before View.fxml has been loaded.

One way to fix this is to pass rowsFromPrefs to the TableBlock constructor. To do this, use the @NamedArg annotation:

public class TableBlock extends VBox{

    private final String rowsFromPrefs;

    @FXML
    private Label label;

    public TableBlock(@NamedArg("rowsFromPrefs") String rowsFromPrefs) {

        this.rowsFromPrefs = rowsFromPrefs ;
        FXMLLoader fxmlLoader = new   FXMLLoader(getClass().getResource("TableBlock.fxml"));
        fxmlLoader.setRoot(this);
        fxmlLoader.setController(this);
        try {
            fxmlLoader.load();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @FXML
    public void initialize() {
        this.table = createTable(rowsFromPrefs);
    }

    public String getRowsFromPrefs() {
        System.out.println("getRowsFromPrefs");
        return rowsFromPrefs;
    }


}

Now your attribute in the FXML will be passed to the constructor instead of to a set method, so rowsFromPrefs will be initialized before you call fxmlLoader.load(), as required.

The other option, of course, would simply be to move the code from the initialize() method to the setRowsFromPrefs(...) method. I would use the option described above if you intend rowsFromPrefs to be fixed for each TableBlock instance, and use the second option only if you want to be able to change rowsFromBlocks during the lifecycle of an individual TableBlock instance.

Related Topic