I would like to ask about program structure.
I have ControllerStatistic
related with FXMLStatistic
in which I define TabPane
.
In initialize
in ControllerStatistic
I add tab for every month. Every tab contains FXMLTableViewMonthly
with ControllerMonthly
. In ControllerMonthly
I would like to populate table with rows for every day month has. I have month information from static field:
private static int countControllers = 0;
public ControllerUdostepnianieNaZewnatrz() {
countControllers++;
monthNumber = countControllers;
}
I populate table in initialize
.
This works but I don't think it's the right way.
I would like to pass month parameter in ControllerStatistic
to ControllerMonthly
.
I see 2 options here:
In ControllerStatistic
I get controller from loader and set month, then in ControllerMonthly
I can't populate in initialize
(month field is null) So I need populate in ControllerStatistic
after setting month field.
I can also remove fx:controller
from FXML and construct a new controller in code as described in Passing Parameters JavaFX FXML by @jewelsea (he mentioned he doesn't like that solution). Then I think I can populate in ControllerMonthly
in initialize
.
I opt to use 2nd approach. First looks very bad for me (to populate after setting month - solution looks like causing a lot of errors).
How to do this?
Well there is no general bad or good. It depends on your usecase/design and taste.
Lets have a look at other FX - elements without fxml first, and how you would populate them, to get on the right track. Take an AnchorPane for example. First you create it and after it is created you populate it with additional elements. When you are done you show the whole thing. You don't overwrite some initialize() method in the AnchorPane:
public void createAStage(String foo){
AnchorPane pane = new AnchorPane();
Stage stage = new Stage();
Scene scene = new Scene(pane);
stage.setScene(scene);
//here we populate the pane with a Label
//and set that Label again to some value that was passed to this method(foo):
pane.getChildren().add(new Label(foo));
stage.show();
}
There is nothing wrong with doing that. And so there is nothing wrong with setting data in some class that was created from fxml after initialize() was called. And yes, in that case you don't populate in initialize() but from outside in your factory - but so what?
Sometimes I need to (re)set values every once in a while after the dialog was created. So I create a method for this. Having that method I use it to populate it:
public class DialogController implements Initializable {
@FXML
private AnchorPane dialog;
@FXML
private Label lb_size;
private Setting settings = null;
/**
* Initializes the controller class.
*/
@Override
public void initialize(URL url, ResourceBundle rb) {}
public void setSettings(Settings settings, int size) {
this.settings = settings;
this.lb_size.setText("" + size);
}
}
Then I construct it:
public DialogController createDialog(Settings settings, int size){
final FXMLLoader loader = new FXMLLoader(FXMLLoader.class.getResource("/fxml/afxm.fxm"));
try {
final Stage stage = new Stage();
stage.setScene(new Scene(loader.load()));
final DialogController controller = loader.getController();
controller.setSettings(settings,size);
stage.show();
return controller;
} catch (IOException ex) {
throw new InternalApplicationError("Resource missing", ex);
}
}
Now whenever I need to set the Settings to something else I would call:
controller.setSettings(settings,size);
This of course fails if there was a constraint rg. that settings may never be null. Usually if you can/want to reasign values you need to take care of that case anyways so your class should be able to handle to be constructed with settings=null as it may happen if you reasing it. So you have to check it somewhere and ensure you get no nullpointer. Same holds true for the size field - if it was not set prior showing it will show a default- but that may be very well what you want.
Sometimes(depends) I feel that a separate factory is an unecessary additional class and rather want to have things together in one class.
For that I have a simple baseclass:
public class FXMLStage extends Stage implements Initializable {
protected URL url = null;
protected ResourceBundle resourceBundle = null;
@SuppressWarnings("LeakingThisInConstructor")
public FXMLStage(String filename) {
final FXMLLoader loader = new FXMLLoader(FXMLLoader.class.getResource(filename));
try {
loader.setControllerFactory(p -> this);
this.setScene(new Scene(loader.load()));
} catch (IOException ex) {
throw new InternalApplicationError("Resource missing", ex);
}
}
@Override
public void initialize(URL url, ResourceBundle rb) {
this.url = url;
this.resourceBundle = rb;
}
}
Here the initialize only remembers the resourcebundle and the url so I can use it later. Nothing else.
I set the controller with loader.setControllerFactory(p->this) instead of loader.setController(this) for one single reason: I can create/update the Java code for the controller automatically. The IDE is able to create/update fields in the controller automaticaly from the fxml if the cotroller is set in the fxml. And if a controller is set in the fxml you can't set it explicitely in the code. So thats more a workaround for my convenience.
If it weren't for that I'd prefer setting the controller simple with loader.setController(this);
Also I do not check the class that is passed in p: "loader.setControllerFactory(p -> this);" - you may want to do so as it will fail of course if the fxml doesn't match the controller(wrong class). But I rather want it to fail if something is wrong(wrong fxml for the controller) instead silently continuing. So an errormessage telling me that I use a wrong controller is acceptable for me. Whats more: it will also fail if you have nested controllers - in that case you certainly want to check the class and return the apropriate controller - and imo rather use a real factory in that case.
Now from that baseclass I derive a particular controller class:
public class SampleDialog extends FXMLStage {
@FXML
private AnchorPane dialog;
@FXML
private Label lb_size;
//....
//some additional fields to initialize...
private final Session session;
public SampleDialog(Session session, int size) {
super("/fxml/SampleDialog.fxml");
//initialize aditional fields:
this.session=session;
lb_size.setText("" + size);
}
}
So I can do the initializing directly in the constructor - which I think is a good place for initializing fields of a class. So it doesn't overwrite initialize() at all.
Note the "Session" object, that is passed to the constructor(don't matter what data it may be - you will have your kind of data). It is stored as reference in the dialog. So whatever changes happen to the data from outside is directly reflected in the dialog.
If it were an Observable for example you could bind elements of your dialog to it - which would allways reflect the state of that data.
If it were a ObservableList you could populate a ListView in the dialog with it and whenever that ObservableList is changed, the ListView would reflect the state of the list. Same for a TableView(for example I populate TableViews from HashMaps that are created and populated/updated somewhere else.).
So separation of model, view and controller becomes possible.
Only keep the special purpose of initialize() in mind. It is part of the construction process! So if you overwrite it, not all fields may have been initialized yet when it is called and so it may fail if you try to use one of those uninitialized fields. Well thats what the inizialize() method is all about: initializing unintialized fields and its name should give you fair warning.
Now I want to use it:
SampleDialog dialog = new SampleDialog(session,5);
dialog.show();
Or if I don't need the object:
new SampleDialog(session,5).show();
Final remark: I didn't have your controller, so I coudn't create examples that are spot on. I used a Stage because it is simple to reproduce but using Tabs and TableViews is not fudamentally different. Also I didn't try to give you all kind of approaches - you have them in your linked question. I tried to give some examples of how different aproaches and scenarios may look like in a real world app and what may happen in the examples - in the hope to trigger some idea of what is going on and to show that there is a tradeoff of a lot more than two ways. Good luck!