2

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 NullPointerExceptions 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.

Community
  • 1
  • 1
JD3
  • 33
  • 1
  • 7

4 Answers4

0

It doesn't look like you are defining a label in Sub.xml, which could be why nothing is being injected into the label field. Try updating Sub.xml to contain the following:

<?import Super?>

<fx:root xmlns:fx="http://javafx.com/fxml/1" type="Super">
    <Label fx:id="label"/>
</fx:root>

Does that work?

The problem is that the call to getClass() in Super returns Sub.class when you are instantiating a Sub. So it loads Sub.xml, which I guess is not what you want (it seems like you're trying to load both Super.xml and Sub.xml). You might be able to do that by explicitly loading Super.xml in the Super constructor AND explicitly loading Sub.xml in the Sub constructor.

Greg Brown
  • 3,168
  • 1
  • 27
  • 37
  • Yes, adding the label also to the subclass does work. But this is what I would like to avoid. Imagine `Super` being a huge component. Then I would have to reproduce (= copy) the whole FXML specification. Yes, I would like to load both fxml files. First _Super.fxml_ for the `Super` sub-object and then _Sub.fxml_ for the `Sub` sub-object. But `FXMLLoader` runs `initialize` after every load. Hence, an incompletely injected objects gets run. – JD3 Dec 11 '15 at 20:57
  • What if you only call setController() in the subclass? – Greg Brown Dec 11 '15 at 21:51
  • `javafx.fxml.LoadException: Root hasn't been set. Use method setRoot() before load`. I think a valid root object is always required. – JD3 Dec 11 '15 at 22:00
  • Controller, not root. – Greg Brown Dec 11 '15 at 22:01
  • Meaning, call setRoot() in the base class but don't call setController(). – Greg Brown Dec 11 '15 at 22:04
  • D'oh. I now tested the following: Load *Super.fxml* without Controller, then load *Sub.fxml* with Controller. Both with the same root. When loading the second time the `NullPointerException` occurs. I have the feeling, especially when reading this link (http://stackoverflow.com/questions/31569299/how-to-extend-custom-javafx-components-that-use-fxml) again, that some other mechanism, like this extension has to be used. – JD3 Dec 11 '15 at 22:21
  • Do you have access to the source code for FXMLLoader? Any idea what is causing the NPE? – Greg Brown Dec 11 '15 at 22:23
  • Did you update the constructors to load explicit FXML files (not using class name)? – Greg Brown Dec 11 '15 at 22:25
  • Yes, the `null` value is the label field, which has not been injected. See the test code here http://paste.ofcode.org/344rbL888eLxVfk2vnDeJpq – JD3 Dec 11 '15 at 22:51
  • As I recall, initialize() should only be called when the loader's controller is non-null. Is that not the case? – Greg Brown Dec 11 '15 at 23:47
  • Yes, but injection of `@FXML`-fields also only takes place when the controller is non-null. – JD3 Dec 11 '15 at 23:59
  • Correct. By only calling `load()` in the leaf class, you should be ensuring that `initialize()` is only called once, and injection only takes place once. As long as the leaf class (i.e. `Sub`) sets a non-null controller, it seems like this should work OK. – Greg Brown Dec 12 '15 at 01:10
  • Yes, but without setting the controller when loading *Super.fxml* the `@FXML` entries there get not injected. In my original example this leads to the field `label` being `null` when the load for *Sub.fxml* finishes. – JD3 Dec 12 '15 at 09:23
  • Please see http://paste.ofcode.org/7Wy2HaZf25axqMjF8Bv5Cw for a complete example. Note the comment in *Super.java*. – JD3 Dec 12 '15 at 09:38
  • Yea, never choose a random hoster for your code. Seems to delete contents within the hour. I adapted the question to contain the code. – JD3 Dec 12 '15 at 12:45
  • I saw that. Just posted a new answer. – Greg Brown Dec 12 '15 at 12:47
0

I think I see the problem. If you don't call setController() in Super(), there is no place to inject label, so the field remains null. If you do call setController() in super, then Sub's implementation of initialize() is called twice - once by the call to load() in Super() and again by the call to load() in Sub.

In theory, this should work as long as you guard against NPEs in Sub. If Sub#initialize() is called and button is still null, you know that you're being initialized for Super and you should delegate to super.initialize(). When button is non-null, you wouldn't call super.

Greg Brown
  • 3,168
  • 1
  • 27
  • 37
  • Exactly, thats the situation. One could imagine several kinds of conditional guards. Checking for `null` like you suggested. Counting the calls and only execute own initialization code when the matching inheritance level is being initialized. Using `initialize` not at all and call private init-functions in the constructor after load (no _if_ at all). But all those "solutions" are hacky. Isn't there a correct and clean approach to that? – JD3 Dec 12 '15 at 12:58
  • A private flag at each level would also work, and doesn't seem like much of a "hack". But ideally, it does seem like the ability to specify a type-specific initializer would be helpful. – Greg Brown Dec 12 '15 at 13:32
  • I think inheritance is something natural for JavaFX UI components. Consequently, manually controlling the initialization flow seems wrong to me. I still hope there is a more clean approach to that... I will wait some time and if nothing better comes up I will accept your answer. Thank you for your help! – JD3 Dec 12 '15 at 13:46
  • As the initial author of `FXMLLoader`, I can assure you that this is not a use case that was considered when the class was originally written. :-) However, I haven't touched that code in a long time, so someone with more recent experience with it may be able to offer more insight. – Greg Brown Dec 12 '15 at 13:52
  • Seems like you're right. I accepted your answer. Thank you for your patience! – JD3 Dec 15 '15 at 08:09
0

I know this post is a bit old but I ran into the same problem and finally found a solution for correctly initializing parents/children when inheriting and having injections and properties in both child and parent. Here is the simple architecture that I am using:

public class Parent extends HBox {

    @FXML
    private Label labelThatIsInBothFXMLs;

    public Parent() {
        this(true);
    }

    protected Parent(boolean doLoadFxml) {
        if (doLoadFxml) {
            loadFxml(Parent.class.getResource(...));
        }
    } 

    protected void loadFxml(URL fxmlFile) {
        FXMLLoader loader .... //Load the file
    }

    @Initialize
    protected void initialize() {
        // Do parent initialization.
        labelThatIsInBothFXMLs.setText("Works!");
    }

}

public class Child extends Parent {

    @FXML
    private Label labelOnlyInChildFXML;

    public Child() {
        super(false);
        loadFxml(Child.class.getResource(...));
    }

    @Override
    protected void initialize() {
        super.initialize();
        // Do child initialization.
        labelOnlyInChildFXML.setText("Works here too!");
    }
}

The important part to notice is that the lowest level child is the one that calls the fxml load. This is so that all levels of constructors are ran before the fxml load starts injecting data using reflection. If a parent loads the fxml the child have yet to create the class properties causing the reflection injection to fail. This is true for properties set in the FXML as well.

Flipbed
  • 719
  • 9
  • 18
0

There is a simple way for Flipbed's answer

public class Super extends HBox {

@FXML
private Label label;

public Super() {
    super();
    if(getClass() == Super.class)
        loadFxml(Super.class.getResource("/fxml/Super.fxml"), this, Super.class);
}

Thats all you need

Eric Chan
  • 49
  • 3