Reputation: 117
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>
Upvotes: 5
Views: 6628
Reputation: 209330
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.
Upvotes: 8