Accessing FXML Controller Class from predefined Controller class - java

I implemented a simple application with the MVC-Pattern and used a console for the output. Now I tried to replace the console with a simple JavaFX-Approach to test the independence of my design.
The whole logic is covered in the Controller class Admin. It is has an object a_view which gets initialized at creation and implements the IView interface, which has a showWelcomeText() function.
I initialize the Scene and the controller in Main.java:
#Override
public void start(Stage primaryStage) {
try {
FXMLLoader fxmlLoader = new FXMLLoader();
Parent root = fxmlLoader.load(getClass().getResource("/view/DesktopApp.fxml").openStream());
Scene scene = new Scene(root,800,500);
primaryStage.setScene(scene);
primaryStage.show();
IView a_view = (IView) fxmlLoader.getController(); // JavaFXGUI class connected to the root
//a_view.showWelcomeMessage(); // shows message
Admin secretary = new Admin(a_view);
secretary.manage(); // shows empty Form
} catch(Exception e) {
e.printStackTrace();
}
}
The JavaFXGUI-controller is passed as parameter and then initialized in the admin class.
public Admin(IView a_view){
this.a_view = a_view;
md_list = dao.MembersDAO.jaxbXMLToObject(); // read out of XML
}
public void manage(){
a_view.showWelcomeMessage();
However when I run the program it just shows me an empty form and does not display the welcome text. If I comment out the call of manage() and call a_view.showWelcomeMessage() directly it works fine and the message is shown in the form.
I thought it could be a problem to pass the variable(a_view) as parameter (e.g. Java does not pass reference), so I also tried to declare it as static public in the Admin class. It results in the same even both calls refer to the same object.
I searched a lot and amongst other stuff I saw this thread about how to access the controller Accessing FXML controller class
but couldn't make it work with the calls from the original controller class. Where is the difference between calling the public static (or private and pass as parameter) and calling it from the start method?
Kind regards

Related

NullPointerException when starting a Popupwindow with default values set, otherwise it works

When I open a Popup-window by a button event from the maincontroller
the popup appears and everything looks perfect.
But when I try to set data in comboboxes or textfields
PopUpPersController:
public PopUpPersController() {
initialize();
}
private void initialize() {
txtMa_LohnGesKum.setText("1200.12");
}
and press the button to open the popup, I get a null pointer exception.
The maincontroller is called Projektcontroller,
the associated FXML-file is called Projekt.fxml
The popupwindow controller is called "PopUpPersController",
the associated FXML-file called PersCalc.fxml.
All elements have fxId's.
I thought the filling of comboboxes or textfields with defaultdata would work the same way as in the Maincontroller...
I did not find an answer that covers this question.
I am quite new to Java, so thanks a lot for your help and best regards
Marcus
The code from the Main or ProjectController for the Button opening the popupwindow:
#FXML
void onAction_Test(ActionEvent event) {
try {
Parent root1 = FXMLLoader.load(getClass().getResource("/ui/fxml/PersCalc.fxml"));
Stage persStage = new Stage();
persStage.setTitle("Personalkosten-Rechner");
persStage.setScene(new Scene(root1));
persStage.show();
} catch (IOException e) {
e.printStackTrace();
}
}
I tried to identify the major errors:
javafx.fxml.LoadException in PersCalc.fxml
The code in this line:
<BorderPane xmlns="http://javafx.com/javafx/10.0.1"xmlns:fx="http://javafx.com/fxml/1" fx:controller="ui.controller.PopUpPersController">
2.From the button event in the ProjectController (see above)
The code in this line:
Parent root1=FXMLLoader.load(getClass().getResource("/ui/fxml/PersCalc.fxml"));
3.The assignment of data to the textfield
Caused by: java.lang.NullPointerException at
ui.controller.PopUpPersController.initialize(PopUpPersController.java:123)
The code in this line (see above initialize()):
txtMa_LohnGesKum.setText("1200.12");
Loading a fxml containing the fx:controller attribute results in FXMLLoader using the constructor taking 0 arguments to create an instance of the controller class. The constructor is invoked before any of the fields are injected resulting in a NullPointerException, even if the fields are accessible to FXMLLoader and the fxml file contains the proper fx:id attributes.
Assuming you did set up the field/fx:id properly, making the initialize method visible to FXMLLoader instead of invoking initialize from the constructor should fix the issue:
public PopUpPersController() {
}
#FXML // you need this annotation for non-public members to be visible to FXMLLoader
private void initialize() {
txtMa_LohnGesKum.setText("1200.12");
}
Make sure the fields FXMLLoader should inject to are also annotated with #FXML (or public) and required fx:id attributes are set in the fxml file.

FXML: "Run after initialized"

I have a JavaFX application that uses FXML alongside a controller class written in Java. In the Java controller I need to take care not to operate on an FXML Node element until it's been initialized (otherwise I'll get a NullPointerException), which isn't guaranteed until the initialize method is run. So I find myself doing this a lot:
The controller is set in the FXML file like this:
<Pane fx:controller="Controller" ...>
...
</Pane>
And then here's the controller in the Java file.
class Controller{
#FXML
Pane aPane;
int globalValue;
public void setSomething(int value){
globalValue = value;
if(!(aPane == null)){ //possibly null if node not initialized yet
aPane.someMethod(globalValue)
}
}
#FXML
void initialize(){
aPane.someMethod(globalValue) //guaranteed not null at this point
}
}
This works, but it's clunky and repetitive. I have to create the globalValue attribute just in case the setSomething method is called before initialize has been called, and I have to make sure the operations in my setSomething method are identical to the operations in initialize.
Surely there's a more elegant way to do this. I know that JavaFX has the Platform.runlater(...) method that guarantees something will be run on the main application thread. Perhpas there's something like Platform.runAfterInitialize(...) that waits until initialization, or runs immediately if initialization already happened? Or if there's another way to do it I'm open to suggestions.
If you specify the controller in the FXML file with fx:controller="Controller", then when you call FXMLLoader.load(...), the FXMLLoader:
parses the FXML file
creates an instance of Controller by (effectively) calling its no-arg constructor (or, in advanced usage, by invoking the controller factory if you set one)
creates the UI elements corresponding to the elements in the FXML file
injects any elements with an fx:id into matching fields in the controller instance
registers event handlers
invokes initalize() on the controller instance (if such a method is defined)
returns the UI element corresponding to the root of the FXML hierarchy
Only after load() completes (i.e. after the #FXML-annotated fields are injected) can you get a reference to the controller with loader.getController(). So it is not possible (aside from doing something extremely unusual in a controller factory implementation) for you to invoke any methods on the controller instance until after the #FXML-injected fields are initialized. Your null checks here are redundant.
On the other hand, if you use FXMLLoader.setController(...) to initialize your controller, in which case you must not use fx:controller, you can pass the values to the constructor. Simply avoiding calling a set method on the controller before passing the controller to the FXMLLoader means you can assume any #FXML-annotated fields are initialized in the controller's public methods:
class Controller{
#FXML
Pane aPane;
int globalValue;
public Controller(int globalValue) {
this.globalValue = globalValue ;
}
public Controller() {
this(0);
}
public void setSomething(int value){
globalValue = value;
aPane.someMethod(globalValue)
}
#FXML
void initialize(){
aPane.someMethod(globalValue) //guaranteed not null at this point
}
}
and
FXMLLoader loader = new FXMLLoader(getClass().getResource("path/to/fxml"));
Controller controller = new Controller(42);
loader.setController(controller);
Node root = loader.load();

Inject Data with Guice into JavaFX ViewController

Today I added Guice to my Java FX Application. The main goal was to replace the singletons I had with Injection and break up dependencies.
So far everything worked fine, this is the code I have to start a new Scene:
public class App extends Application{
public static void main(String[] args){
launch(args);
}
#Override
public void start(Stage primaryStage) throws Exception {
final String LANGUAGE_BUNDLE = "myBundlePath";
final String FXML = "myFXMLPath";
try {
ResourceBundle resourceBundle = ResourceBundle.getBundle(LANGUAGE_BUNDLE, Locale.GERMAN, this.getClass().getClassLoader());
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource(FXML), resourceBundle, new JavaFXBuilderFactory(), getGuiceControllerFactory());
primaryStage.setScene(new Scene(fxmlLoader.load()));
primaryStage.show();
}catch (IOException ex) {
ex.printStackTrace();
}
}
private Callback<Class<?>, Object> getGuiceControllerFactory(){
Injector injector = Guice.createInjector(new GuiceModule());
return new Callback<Class<?>, Object>() {
#Override
public Object call(Class<?> clazz) {
return injector.getInstance(clazz);
}
};
}
}
My Guice Module looks like this:
public class GuiceModule extends AbstractModule {
#Override
protected void configure() {
bind(ITester.class).to(Tester.class);
bind(ISecondTest.class).to(SecondTest.class);
}
}
Please note that i substituted the paths for the ressource bundle and the fxml file as they would have revealed my identity. But loading and everything works, so this shouldn't be a problem ;)
Now the problem is, that I want to instantiate a new view with a button click in a different view. The second view should display a more detailed version of the data in view 1.
Everything that I need to pass to the second view is an Integer (or int), but I have absolutely no clue on how to do this.
I have the standard FX setup with:
View.fxml (with a reference to the ViewController)
ViewController.java
Model.java
ViewModel.java
The ViewController then binds to properties offered by the ViewModel.
I need the int to choose the correct model.
Everything I could find was about the Annotation #Named but as far as I can see, this wouldn't be usable to inject dynamic data.
Could you please give me a hint what this what I want to do is called?
Or long story short: How can I inject a variable, chosen by a different view, in a second ViewController, and leaving the rest in the standard FX-way?
Any help appreciated and thanks in advance!
Regards, Christian
After trying around a bit more, it seems like I found a solution by myself!
However, it "feels" ugly what I'm doing, so I'd like to have some confirmation ;)
First the theory: Guice supports "AssistedInject". This is, when a class can not be constructed by a default constructor. In order to be able to use AssistedInject, you have to download the extension (I downloaded the jar from maven repository).
What AssistedInject does for you is that it allows you to specify a factory which builds the variable for you. So here is what I have done:
First, create an interface for the class which you want to use later, in my case:
public interface IViewController {
}
Second, create an interface for the factory. Important: you do not have to implement the factory
public interface IControllerFactory {
ViewController create(#Assisted int myInt);
}
Third, add the constructor with the corresponding parameters to your class which you want to instantiate later, and let it implement the interface you created like so:
public class ViewController implements IViewController{
#AssistedInject
public ViewController(#Assisted int i){
final String LANGUAGE_BUNDLE = "languageBundle";
final String FXML = "View.fxml";
try{
ResourceBundle resourceBundle = ResourceBundle.getBundle(LANGUAGE_BUNDLE, Locale.GERMAN, this.getClass().getClassLoader());
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource(FXML), resourceBundle, new JavaFXBuilderFactory());
fxmlLoader.setController(this);
Stage second = new Stage();
second.setScene(new Scene(fxmlLoader.load()));
second.show();
}catch (IOException e){
e.printStackTrace();
}
System.out.println("ViewController constructor called with: " + i);
}
Here are a few things to note:
The annotation "#AssistedInject" for the method
The annotation "#Assisted" for the parameter which we want to supply externally
we set the controller for the loader manually (with fxmlLoader.setController(this);)
I had to remove the controller configuration in the fxml file, so no "fx:controller" in the fxml!
Next we need to add a variable into the class from where we want to instantiate the other class:
#Inject
IControllerFactory controllerFactory;
We can use it in the class like so:
controllerFactory.create(3)
Note: we call the method "create" which we never implemented in the ViewController class! Guice knows it has to call the constructor - magic
As last step, we need to add the connection to our context in our GuiceModule, like so:
#Override
protected void configure(){
install(new FactoryModuleBuilder()
.implement(IPagingDirectoryViewController.class, PagingDirectoryViewController.class)
.build(IPagingDirectoryControllerFactory.class));
}
Note I got the error: Cannot resolve method 'implement java.lang.Class<"The interface class">, java.lang.Class<"The implementing class">'. This was because I forgot to let my Controller class implement the interface.
Okay, so that's how I got it working.
As I said however, I'd be really happy about some opinions!
Regards, Christian
In your Module Configuration you could simply add a Provider Method for FXMLLoader, in which you assign Guices 'injector.getInstance()' as ControllerFactory for the loader.
#Provides
public FXMLLoader getFXMLLoader(com.google.inject.Injector injector) {
FXMLLoader loader = new FXMLLoader();
loader.setControllerFactory(injector::getInstance);
return loader;
}
All you have to do now, is to bind your ViewControllers in the configure() method of your module configuration.
// for example:
bind(ViewController.class);
And make sure the controller class is properly bound in your fxml file.
fx:controller="your.package.ViewController"
Now you simply use your injector to get an instance of FXMLLoader.

Why is SceneBuilder so particular about Custom Controls?

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?
EDIT:
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.
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.)

How can i use Guice in JavaFX controllers?

I have a JavaFX application where I would like to introduce Guice because my Code
is full of factorys right now, only for the purpose of testing.
I have one use case where i have a controller class of a certain view.
This controller class has a viewmodel and I pass the model to the viewmodel via
the constructor of the controller class.
In the controller class I have a contactservice object that provides the edit/save/delete operations.
As of now I have an interface of that object and provide an implementation and a Mock. This object can be retrieved by a Factory.getInstance() method.
What I want to do is something like this:
public class NewContactController implements Initializable {
// ------------------------------------------------------------------------
// to inject by guice
// ------------------------------------------------------------------------
private ContactService contactService;
#Inject
public void setContactService(ContactService srv) {
this.contactService = srv;
}
// edit window
public NewContactController(Contact c) {
this.viewModel = new NewContactViewModel(c);
}
// new window
public NewContactController() {
this.viewModel = new NewContactViewModel();
}
#FXML
void onSave(ActionEvent event) {
//do work like edit a contcat,
contactService.editContact(viewModel.getModelToSave());
}
#Override
public void initialize(URL location, ResourceBundle resources) {
// bind to viewmodel---------------------------------------------------
}
}
How can I achive this? Is it a good a idea to do something like that?
While I was searching for a solution I found fx-guice and similar frameworks but how can i combine these two?
Specially how can I let this fields be injected AND instanciate the controller myself or at least give it some constructor args?
I don't use Guice, but the simplest approach would appear to be just to use a controller factory on the FXMLLoader. You can create a controller factory that instructs the FXMLLoader to use Guice to initialize your controllers:
final Injector injector = Guice.createInjector(...);
FXMLLoader loader = new FXMLLoader(getClass().getResource(...));
loader.setControllerFactory(new Callback<Class<?>, Object>() {
#Override
public Object call(Class<?> type) {
return injector.getInstance(type);
}
});
// In Java 8, replace the above with
// loader.setControllerFactory(injector::getInstance);
Parent root = loader.<Parent>load();
There's a good DI framework for javaFX called afterburner.fx. Check it out, I think it's the tool you're looking for.
Assuming you (could) instantiate the controller by hand/guice and not from the FXML, you could use https://github.com/google/guice/wiki/AssistedInject if you need to pass any non DIable parameter to the constructor. You would then set the controller manually to the FXMLLoader with .setController(this) and load the FXML file in the constructor of the controller.
Not sure if there are any drawbacks, but this kind of system seems to work for me :)
To use JavaFx with Guice :
Extend javafx.application.Application & call launch method on that class from the main method. This is the application’s entry point.
Instantiate dependency injection container of your choice. E.g. Google Guice or Weld.
In application’s start method, instantiate FXMLLoader and set it’s controller factory to obtain controllers from the container. Ideally obtain the FXMLLoader from the container itself, using a provider. Then give it an .fxml file resource. This will create content of the newly created window.
Give the Parent object instantiated in previous step to Stage object (usually called primaryStage) supplies as an argument to the start(Stage primaryStage) method.
Display the primaryStage by calling it’s show() method.
Code Example MyApp.java
public class MyApp extends Application {
#Override
public void start(Stage primaryStage) throws IOException {
Injector injector = Guice.createInjector(new GuiceModule());
//The FXMLLoader is instantiated the way Google Guice offers - the FXMLLoader instance
// is built in a separated Provider<FXMLLoader> called FXMLLoaderProvider.
FXMLLoader fxmlLoader = injector.getInstance(FXMLLoader.class);
try (InputStream fxmlInputStream = ClassLoader.getSystemResourceAsStream("fxml\\login.fxml")) {
Parent parent = fxmlLoader.load(fxmlInputStream);
primaryStage.setScene(new Scene(parent));
primaryStage.setTitle("Access mini Stock App v1.1");
primaryStage.show();
primaryStage.setOnCloseRequest(e -> {
System.exit(0);
});
} catch (IOException ex) {
ex.printStackTrace();
}
}
public static void main(String[] args) {
launch();
}
}
Then FXMLLoaderProvider.java
public class FXMLLoaderProvider implements Provider<FXMLLoader> {
#Inject Injector injector;
#Override
public FXMLLoader get() {
FXMLLoader loader = new FXMLLoader();
loader.setControllerFactory(p -> {
return injector.getInstance(p);
});
return loader;
}
}
Thanks to mr. Pavel Pscheidl who provided us with this smart code at Integrating JavaFX 8 with Guice

Categories

Resources