Issue
I am working on coding a modular soundboard in Java and JavaFX using FXML. One of key aspects is the ability to load an arbitrary class that implements the Plugin abstract class at runtime. The plugin abstract class looks like this:
import javafx.scene.control.Tab;
import java.io.File;
import java.io.FileReader;
import java.net.URLClassLoader;
import java.util.Properties;
public abstract class Plugin {
protected Tab mainTab;
protected Tab settingsTab;
protected Properties propertyFile = new Properties();
protected File fileName;
protected URLClassLoader loader;
public Plugin(){
mainTab = new Tab();
}
public Plugin(String tabName){
mainTab = new Tab(tabName);
settingsTab = new Tab(tabName);
}
public abstract void save();
public Tab getMainTab(){
return mainTab;
}
public Tab getSettingsTab(){
return settingsTab;
}
public void initPropertyFile(String filename){
try{
File file = new File(System.getenv("APPDATA")+"\\Testing\\"+filename+".properties");
if(file.exists())
propertyFile.load(new FileReader(file));
else{
file.createNewFile();
propertyFile.load(new FileReader(file));
}
}catch(Exception e){
e.printStackTrace();
}
}
public void setClassLoader(URLClassLoader loader){
this.loader = loader;
}
public abstract void initController();
public Properties getPropertyFile(){
return propertyFile;
}
}
I use a URLClassLoader to load any classes found in a specified JAR file, and then I instantiate any instances of Plugin and collect them inside a list. Where I am having an issue is creating an FXML controller to use for these classes that I instantiate at runtime. Because both these classes and the controllers are loaded at runtime using a custom URLClassLoader I am unable to just specify a FXML controller inside my FXML files. If I try to, I get an error saying that the class I am using for a controller could not be found. If I don't specify a controller inside the FXML file, it is able to load the UI components, but I can't specify any actions inside it. I can set a controller using the FXMLLoader.setController() method, but when I load a controller like this though, none of the buttons I am using have any actions. I've tried using the initialize() method with FXML annotations, but this method isn't automatically called by the FXMLLoader. If I call initialize myself then it seems like the FXML hasn't been injected yet, but the UI components specified by the FXML file appear with the formatting specified by the FXML file on my screen.
import java.net.URL;
public class Plugin3 extends Plugin {
FXMLLoader mainTabLoader;
URL testURL;
public Plugin3(){
//Calls Plugin Constructor to initialize mainTab and settingsTab, and set the UI names of the tabs
super("Plugin3");
//Try creating FXMLLoader to setup the UI of the plugin
try {
mainTabLoader = new FXMLLoader();
//mainTabLoader.setController(this);
Plugin3 thisObj = mainTabLoader.getController();
testURL = getClass().getResource("testFXML.fxml");
//Load the FXML file
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void save() {
}
//Attempts to set the controller of the FXMLLoader mainTabLoader and call the initialize method of that controller
@Override
public void initController() {
try {
String classPath = "com.example.plugindevelopment.Plugin3Controller";
mainTabLoader.setController(Class.forName(classPath, true, loader).getConstructor().newInstance());
getMainTab().setContent(mainTabLoader.load(testURL));
mainTabLoader.getController().getClass().getDeclaredMethod("initialize").invoke(mainTabLoader.getController());
}catch(Exception e){
e.printStackTrace();
}
}
}
This is the controller:
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.Slider;
import javafx.scene.layout.VBox;
public class Plugin3Controller
{
@FXML
VBox root;
@FXML
Label label;
@FXML
Slider slider;
@FXML
Button button1;
protected void onButtonClick(){
System.out.println("Hello");
}
@FXML
public void initialize(){
System.out.println("Initializing Plugin3Controller");
button1 = new Button("Hello");
button1.setOnAction(event -> onButtonClick());
}
}
and this is the FXML file:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<AnchorPane prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/16" xmlns:fx="http://javafx.com/fxml/1">
<children>
<VBox fx:id="root" prefHeight="200.0" prefWidth="100.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<children>
<Label fx:id="label" prefHeight="137.2" text="text" />
<Slider fx:id="slider">
<VBox.margin>
<Insets top="100.0" />
</VBox.margin>
<padding>
<Insets top="50.0" />
</padding></Slider>
<Button fx:id="button1" mnemonicParsing="false" text="Click Me" />
</children></VBox>
</children>
</AnchorPane>
I would appreciate any insight anyone can provide, thank you.
Solution
Because both these classes and the controllers are loaded at runtime using a custom URLClassLoader I am unable to just specify a FXML controller inside my FXML files. If I try to, I get an error saying that the class I am using for a controller could not be found.
Note that you can set the Class Loader to be used by the FXMLLoader
.
The solution you attempt in which you set the controller manually has a number of errors. Most importantly:
String classPath = "com.example.plugindevelopment.Plugin3Controller";
mainTabLoader.setController(Class.forName(classPath, true, loader).getConstructor().newInstance());
getMainTab().setContent(mainTabLoader.load(testURL));
Here mainTabLoader.load(testURL)
invokes the static method FXMLLoader.load(URL)
. Since this is a static method, it doesn't reference the FXMLLoader
instance mainTabLoader
at all, and in particular won't be aware of the controller instance.
You should use
String classPath = "com.example.plugindevelopment.Plugin3Controller";
mainTabLoader.setController(Class.forName(classPath, true, loader).getConstructor().newInstance());
mainTabLoader.setLocation(testURL);
getMainTab().setContent(mainTabLoader.load());
Note the call to load()
has no arguments, invoking the instance method FXMLLoader.load()
.
Once the FXMLLoader
is properly referencing a controller instance, it will invoke initialize()
automatically. You should remove the line
mainTabLoader.getController().getClass().getDeclaredMethod("initialize").invoke(mainTabLoader.getController());
You also have an error in your controller implementation (I'm guessing this may have been a desperate attempt to fix the problems caused by the error above).
It is always a mistake to instantiate objects and assign them to @FXML
-annotated fields, as you do with
button1 = new Button("Hello");
Remove this line. It causes the event handler to be set on a button that is not displayed in the UI, instead of on the button that is declared in the FXML file.
In all you should have
public class Plugin3Controller
{
@FXML
VBox root;
@FXML
Label label;
@FXML
Slider slider;
@FXML
Button button1;
protected void onButtonClick(){
System.out.println("Hello");
}
@FXML
public void initialize(){
System.out.println("Initializing Plugin3Controller");
button1.setOnAction(event -> onButtonClick());
}
}
and
public class Plugin3 extends Plugin {
FXMLLoader mainTabLoader;
URL testURL;
public Plugin3(){
//Calls Plugin Constructor to initialize mainTab and settingsTab, and set the UI names of the tabs
super("Plugin3");
//Try creating FXMLLoader to setup the UI of the plugin
mainTabLoader = new FXMLLoader();
//mainTabLoader.setController(this);
// This line is patently nonsense: remove it:
// Plugin3 thisObj = mainTabLoader.getController();
testURL = getClass().getResource("testFXML.fxml");
//Load the FXML file
}
@Override
public void save() {
}
//Attempts to set the controller of the FXMLLoader mainTabLoader and call the initialize method of that controller
@Override
public void initController() {
try {
String classPath = "com.example.plugindevelopment.Plugin3Controller";
mainTabLoader.setController(Class.forName(classPath, true, loader).getConstructor().newInstance());
mailTabLoader.setLocation(testURL);
getMainTab().setContent(mainTabLoader.load());
}catch(Exception e){
e.printStackTrace();
}
}
}
Answered By - James_D
Answer Checked By - Willingham (JavaFixing Volunteer)