Don't expose the UI controls or maintain references to other controllers. Instead, expose some observable data in the controllers, and use bindings to bind everything together.
For example, suppose you have a Table.fxml file which displays a TableView<Person>
(Person
is just the example data class) and has a corresponding controller:
public class TableController {
@FXML
private TableView<Person> table ;
private final ReadOnlyObjectWrapper<Person> selectedPerson = new ReadOnlyObjectWrapper<>();
public ReadOnlyObjectProperty<Person> selectedPersonProperty() {
return selectedPerson.getReadOnlyProperty() ;
}
public final Person getSelectedPerson() {
return selectedPersonProperty().get();
}
public void initialize() {
selectedPerson.bind(table.getSelectionModel().selectedItemProperty());
}
}
And a second FXML (perhaps containing text fields for editing the data associated with the selected item in the table) Editor.fxml with a corresponding EditorController
class:
public class EditorController {
@FXML
private TextField nameTextField ;
private ObjectProperty<Person> person = new SimpleObjectProperty<>();
public ObjectProperty<Person> personProperty() {
return person ;
}
public final Person getPerson() {
return personProperty().get();
}
public final void setPerson(Person person) {
personProperty().set(person);
}
public void initialize() {
// update text field bindings when person changes:
personProperty().addListener((obs, oldPerson, newPerson) -> {
if (oldPerson != null) {
oldPerson.nameProperty().unbindBidirectional(nameTextField.textProperty());
}
if (newPerson != null) {
newPerson.nameProperty().bindBidirectional(nameTextField.textProperty());
}
}
}
}
Now when you load the FXML files you just need to bind the two exposed properties:
FXMLLoader tableViewLoader = new FXMLLoader(getClass().getResource("Table.fxml"));
Parent tableView = tableViewLoader.load();
TableController tableController = tableViewLoader.getController();
FXMLLoader editorLoader = new FXMLLoader(getClass().getResource("Editor.fxml"));
Parent editorView = editorLoader.load();
EditorController editorController = editorLoader.getController();
// assemble views...
// bind properties from controllers:
editorController.personProperty().bind(tableController.selectedPersonProperty());
There are various ways to manage the details here, e.g. you can use listeners instead of bindings to gain more control as to when values are updated, etc. But the basic idea is to expose the data necessary from the controllers and observe it, instead of tightly coupling the different controllers together.
If there is sufficient data that needs to be shared between two controllers that this gets unwieldy, then you can bundle those data into a Model
class and use exactly the same technique to share the model between the controllers. If you are willing to forego setting the controller in the FXML with a fx:controller
attribute, you can have the controller accept a Model
reference in its constructor; for example
public class TableController {
@FXML
private TableView<Person> table ;
private Model model ;
public TableController(Model model) {
this.model = model ;
}
public void initialize() {
table.getSelectionModel().selectedItemProperty().addListener((obs, oldPerson, newPerson)
-> model.setSelectedPerson(newPerson));
}
}
and the editor just observes the property in the model:
public class EditorController {
private Model model ;
@FXML
private TextField nameTextField ;
public EditorController(Model model) {
this.model = model ;
}
public void initialize() {
model.selectedPersonProperty().addListener((obs, oldPerson, newPerson)
-> nameTextField.setText(newPerson.getName()));
}
}
Then the assembly code looks like
Model model = new Model();
TableController tableController = new TableController(model);
FXMLLoader tableLoader = new FXMLLoader(getClass().getResource("Table.fxml"));
tableLoader.setController(tableController);
Parent tableView = tableLoader.load();
EditorController editorController = new EditorController(model);
FXMLLoader editorLoader = new FXMLLoader(getClass().getResource("Editor.fxml"));
editorLoader.setController(editorController);
Parent editorView = editorLoader.load();
// assemble views...
In this version, the FXML files cannot have fx:controller
attributes.
Another variant can set the controller factory on the FXMLLoader
, so that it loads classes defined by the fx:controller
attribute, but you control how it loads them (i.e. it can pass a model to the constructor).
Finally, you might consider a dedicated dependency injection framework for injecting a model into the controllers. afterburner.fx is excellent for this.