I would like to subclass custom JFX components to change/extend their behavior. As a real world example, I would like to extend a data viewer component with editing functionality.
Consider the following very minimal scenario.
Using the class Super
works perfectly.
But when instantiating the subclass Sub
(in a FXML-file) the FXMLLoader
does not inject the @FXML
field label
anymore.
Therefore calling initialize
leads to a NullPointerException
when accessing the field with value null
. I suppose FXMLLoader
somehow needs the information to also initialize the Super
sub-object of Sub
using Super.fxml.
Please note that the method initialize
gets automatically called by FXMLLoader
after injection.
I'm aware that nesting the super component inside the sub component should work fine, but I would still like to know if this is possible using inheritance.
Widening the visibility of label
to protected
did obviously not solve this problem. Defining an extension point in fx:root
in combination with @DefaultProperty
(this solution has been proposed here) has worked neither.
I appreciate any help.
fxml/Super.fxml
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.control.*?>
<fx:root xmlns:fx="http://javafx.com/fxml/1" type="HBox">
<Label fx:id="label"/>
</fx:root>
Super.java
import java.io.IOException;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
public class Super extends HBox {
@FXML
protected Label label;
public Super() {
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/" + getClass().getSimpleName() + ".fxml"));
fxmlLoader.setRoot(this);
fxmlLoader.setController(this);
try {
fxmlLoader.load();
} catch (IOException exception) {
throw new RuntimeException(exception);
}
}
public void initialize() {
label.setText("Super");
}
}
fxml/Sub.fxml
<?import test.Super?>
<fx:root xmlns:fx="http://javafx.com/fxml/1" type="Super"></fx:root>
Sub.java
public class Sub extends Super {
public Sub() {
super();
}
}
UPDATE
Like in this question the way to go seems to be to call the FXMLLoader
for each level of inheritance (which has a FXML-file attached). The problem comes down to injecting @FXML
-annotated fields being connected to calling initialize
afterwards. Meaning, if we want the fields to become injected, initialize
gets called afterwards for each single load
. But when initialize
gets overridden by each subclass the most specific implementation gets called n
times (where n
is the number of inheritance levels).
Something like
public void initialize() {
if (getClass() == THISCLASS) {
realInitialize();
}
}
would [Update]not[/Update] solve this problem, but appears like a hack to me.
Consider this demo code by @mrak, which shows the loading on each inheritance level. When we implement initialize
methods in both levels the problem described above occurs.
Here a more complete minimal working example based on mraks code.
Super.java
package test;
import java.io.IOException;
import java.net.URL;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
public class Super extends HBox {
@FXML
private Label label;
public Super() {
super();
loadFxml(Super.class.getResource("/fxml/Super.fxml"), this, Super.class);
}
public void initialize() {
label.setText("initialized");
}
protected static void loadFxml(URL fxmlFile, Object rootController, Class<?> clazz) {
FXMLLoader loader = new FXMLLoader(fxmlFile);
if (clazz == rootController.getClass()) { // PROBLEM
loader.setController(rootController);
}
loader.setRoot(rootController);
try {
loader.load();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Sub.java
package test;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
public class Sub extends Super {
@FXML
private Button button;
public Sub() {
super();
loadFxml(Sub.class.getResource("/fxml/Sub.fxml"), this, Sub.class);
}
@Override
public void initialize() {
super.initialize();
button.setText("initialized");
}
}
Super.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.control.*?>
<fx:root xmlns:fx="http://javafx.com/fxml/1" type="HBox">
<Label fx:id="label" text="not initialized"/>
</fx:root>
Sub.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import test.Super?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.Button?>
<fx:root xmlns:fx="http://javafx.com/fxml/1" type="Super">
<Button fx:id="button" text="not initialized"/>
</fx:root>
See the commented line in Super.loadFxml
. Using this condition leads to the injection of only the @FXML
entries in the leaf. But initialize
gets called only once. Not using this condition leads to (theoretically) the injection of all @FXML
entries. But initialize
takes place after every load, hence NullPointerException
s occur on each non-leaf initialization.
The problem can be fixed when not using initialize
at all and calling some init function myself. But again, this appears very hacky to me.