Ivar Eriksson
Ivar Eriksson

Reputation: 973

Scene Builder Nested Custom Nodes

I seem to have come across a pretty severe bug in Scene Builder 8.4.1 that other people have come across before for different versions (see this link). The bug is that when I try to import a custom node that in turn contains other custom nodes from a jar file only the nested nodes are found. That is, the outer node is not found.

So I was wondering. Does anyone know of a stable release of Scene Builder that does not have this bug (and preferably no other severe ones) for a more recent version of java (8, 9 or maybe 10 if it is out yet)? I would also like there to be an install wizard which will give me an exe application instead of a runnable jar for better integration with my IDE. If this does not exist what do you more Scene Builder experienced people recommend that I do? Should I make all my fxml documents without the nested nodes and add them in later manually?

Thanks for any help!

EDIT: All source files can be found here. Note that the outer container is the SliderVariable and the inner is the InfoIcon.

Upvotes: 4

Views: 927

Answers (1)

José Pereda
José Pereda

Reputation: 45476

You can nest two or more custom controls in the same jar, and that will run just fine from your IDE or from command line.

But if you import that jar from Scene Builder, some of the custom control might fail to be imported if they have a dependency on the others.

There is a reason for this, and the best part, there is an easy solution as well.

How custom control are imported?

If you have a look at Scene Builder's source code, to import the possible custom controls of a jar, there is a JarExplorer class, that has an explore method.

This method basically will go through every class in the jar, finding out if that class is a possible custom component that should be added to the user's library.

At the end, how this works, is by trying to create an FXML object based on that class:

 entryClass = classLoader.loadClass(className);
 instantiateWithFXMLLoader(entryClass, classLoader);

If that succeeds, the class is added to the components collection.

Why a nested custom control fails to be imported?

So why a nested custom control, which is a control that has another custom control as a dependency, fails to be imported? The reason for this can be found debugging Scene Builder, and printing out the exception you get:

try {
    instantiateWithFXMLLoader(entryClass, classLoader);
} catch (RuntimeException | IOException x) {
    status = JarReportEntry.Status.CANNOT_INSTANTIATE;
    x.printStackTrace(); // <-- print exception
} catch (Error | ClassNotFoundException x) {
    status = JarReportEntry.Status.CANNOT_LOAD;
    x.printStackTrace(); // <-- print exception
}

Logging this is really helpful when you are creating any type of custom control.

In the case of a nested custom control, if the dependency is already available in the class path, and by class path I mean all the dependencies loaded by Scene Builder in advance, there won't be a problem. But if it is not available, the FXMLLoader will fail to create a valid instance of this control.

Let's try your SliderVariable control. If you print out the exception, you'll see something like this:

javafx.fxml.LoadException: 
    at javafx.fxml.FXMLLoader.constructLoadException(FXMLLoader.java:2601)
    at javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2579)
    at javafx.fxml.FXMLLoader.load(FXMLLoader.java:2425)
    at com.oracle.javafx.scenebuilder.kit.library.util.JarExplorer.instantiateWithFXMLLoader(JarExplorer.java:110)
... 9 more

Caused by: java.lang.RuntimeException: javafx.fxml.LoadException: 
    file:.../SliderVariable-1.0-SNAPSHOT-shaded!/com/coolcompany/slidervariable/SliderVariable.fxml
...
Caused by: java.lang.ClassNotFoundException: com.coolcompany.infoicon.InfoIcon
    at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    at javafx.fxml.FXMLLoader.loadTypeForPackage(FXMLLoader.java:2916)
    at javafx.fxml.FXMLLoader.loadType(FXMLLoader.java:2905)
    at javafx.fxml.FXMLLoader.importClass(FXMLLoader.java:2846)
... 26 more

So basically this explains why the nested control is not imported: the inner control wasn't available in the class path in advance.

Possible solution

Obviously you could bundle separately both controls, import first the InfoIcon control, and then just import the SliderVariable control. But this could be problematic if you want to distribute these controls in a single dependency.

Best solution

So how do we make available the inner control to the outer one in runtime if both are in the same jar?

This is done by passing the classloader of this outer control class to the FXMLLoader. And this can be done calling the FXMLLoader::setClassLoader method in the outer control.

In your case:

FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("SliderVariable.fxml"));
fxmlLoader.setRoot(this);
fxmlLoader.setController(this);

// set FXMLLoader's classloader!
fxmlLoader.setClassLoader(getClass().getClassLoader());

try {
    fxmlLoader.load();
} catch (IOException exception) { }

If you try again, you will get both controls available as custom components.

Importing nested controls

Upvotes: 11

Related Questions