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.
