Reputation: 3585
I was hoping someone would be able to explain to me exactly why SceneBuilder is so tempermental when it comes to importing custom controls.
Take for example a relatively simple custom control (As only an example):
public class HybridControl extends VBox{
final private Controller ctrlr;
public CustomComboBox(){
this.ctrlr = this.Load();
}
private Controller Load(){
final FXMLLoader loader = new FXMLLoader();
loader.setRoot(this);
loader.setClassLoader(this.getClass().getClassLoader());
loader.setLocation(this.getClass().getResource("Hybrid.fxml"));
try{
final Object root = loader.load();
assert root == this;
} catch (IOException ex){
throw new IllegalStateException(ex);
}
final Controller ctrlr = loader.getController();
assert ctrlr != null;
return ctrlr;
}
/*Custom Stuff Here*/
}
And then you have the Controller class here:
public class Controller implements Initializable{
/*FXML Variables Here*/
@Override public void initialize(URL location, ResourceBundle resources){
/*Initialization Stuff Here*/
}
}
This works just fine. The .jar compiles fine, SceneBuilder reads the .jar just fine, it imports the control just fine, which is great.
The thing that irks me is that it requires two separate classes to accomplish, which is not THAT big of a deal except that I feel like this should be doable with just a single class.
I have it as above now, but I've tried two other ways that both fail (SceneBuilder won't find and let me import the controls) and I was hoping someone would tell me why so I can get on with my life.
In the second case I attempted a single class which extended a VBox and implemented Initializable:
public class Hybrid extends VBox implements Initializable{ /*In this case the FXML file Controller would be set to this class.*/
/*FXML Variables Here*/
public Hybrid(){
this.Load();
}
private void Load(){
final FXMLLoader loader = new FXMLLoader();
loader.setRoot(this);
loader.setClassLoader(this.getClass().getClassLoader());
loader.setLocation(this.getClass().getResource("Hybrid.fxml"));
try{
final Object root = loader.load();
assert root == this;
} catch (IOException ex){
throw new IllegalStateException(ex);
}
assert this == loader.getController();
}
@Override public void initialize(URL location, ResourceBundle resources){
/*Initialization Stuff Here*/
}
}
This makes PERFECT sense to me. It SHOULD work, at least in my head, but it doesn't. The jar compiles fine, and I'd even wager it would work perfectly fine in a program, but when I try to import the .jar into Scene Builder, it doesn't work. It's not present in the list of importable controls.
So... I tried something different. I tried nesting the Controller class within the Control class:
public class Hybrid extends VBox{ /*In this case the FXML Controller I had set to Package.Hybrid.Controller*/
final private Controller ctrlr
public Hybrid(){
this.ctrlr = this.Load();
}
private Controller Load(){
/*Load Code*/
}
public class Controller implements Initializable{
/*Controller Code*/
}
}
This didn't work either. I tried it public, private, public static, private static, none of them worked.
So why is this the case? Why does SceneBuilder fail to recognize a custom control unless the Control class and the Controller class are two separate entities?
Thanks to James_D below I was able to get an answer AND make custom controls work the way I would like. I was also able to create a generic Load method that works for all custom classes if the name of the Class is the same as the name of the FXML file:
private void Load(){
final FXMLLoader loader = new FXMLLoader();
String[] classes = this.getClass().getTypeName().split("\\.");
String loc = classes[classes.length - 1] + ".fxml";
loader.setRoot(this);
loader.setController(this);
loader.setClassLoader(this.getClass().getClassLoader());
loader.setLocation(this.getClass().getResource(loc));
try{
final Object root = loader.load();
assert root == this;
} catch (IOException ex){
throw new IllegalStateException(ex);
}
assert this == loader.getController();
}
Just thought I would share that. Note, again, that it only works if, for example, your class was named Hybrid, and your FXML file was named Hybrid.fxml.
Upvotes: 1
Views: 561
Reputation: 209339
Your second (and third) version won't work at all (SceneBuilder or no SceneBuilder), because the assertion
this == loader.getController()
will fail. When you call loader.load()
the FXMLLoader
sees the fx:controller="some.package.Hybrid"
and creates a new instance of it. So now you have two instances of the Hybrid
class: the one which invoked load
on the FXMLLoader
and the one which is set as the controller of the loaded FXML.
You need to remove the fx:controller
attribute from the FXML file, and set the controller directly in your code, as in the documentation:
private void Load(){
final FXMLLoader loader = new FXMLLoader();
loader.setRoot(this);
loader.setClassLoader(this.getClass().getClassLoader());
loader.setLocation(this.getClass().getResource("Hybrid.fxml"));
// add this line, and remove the fx:controller attribute from the fxml file:
loader.setController(this);
try{
final Object root = loader.load();
assert root == this;
} catch (IOException ex){
throw new IllegalStateException(ex);
}
assert this == loader.getController();
}
Experimenting with SceneBuilder, it seems it will attempt to create custom controls by calling their no-arg constructor, and expects that to complete without creating an exception. It does seem like it's not able to handle injecting @FXML
annotated values correctly in this particular scenario. I would recommend filing a bug at jira for this.
As a workaround, you will probably have to write your code so that executing the no-arg constructor completes without throwing an exception even if the @FXML
-annotated fields are not injected. (Yes, this is a pain.)
Upvotes: 1