There are basically three ways I see to do this:
- Define a model representing the data (
Settings
), and create a single instance of it. Reload the FXML files each time, and pass the single instance to the controller. Bind the data in the UI with the data in the model. That way, when you reload the FXML, it will be updated with the same data. (This is my preferred option.)
- Create the controllers once. Reload the FXML files each time, setting the same controller each time. Have the
initialize()
method update the UI from either locally-stored fields, or a model. The @FXML
-annotated fields will be replaced when you reload the FXML file and the initialize()
method will be called, updating the new controls with the existing data. (This feels a little artificial. Morally speaking, any method called initialize()
should only be executed once. However, this is perfectly workable.)
- Load each FXML file once and cache the UI (and probably the controller). Then when the user selects something in the list view, just display the already-loaded view. This is probably the simplest, but costs a little more in memory as you are keeping all views in memory at all times.
Suppose you have a model, which might look like this:
public class Settings {
private final UserInfo userInfo ;
private final Preferences prefs ;
private final Appearance appearance ;
public Settings(UserInfo userInfo, Preferences prefs, Appearance appearance) {
this.userInfo = userInfo ;
this.prefs = prefs ;
this.appearance = appearance ;
}
public Settings() {
this(new UserInfo(), new Preferences(), new Appearance());
}
public UserInfo getUserInfo() {
return userInfo ;
}
public Preferences getPreferences() {
return prefs ;
}
public Appearance getAppearance() {
return appearance ;
}
}
and
public class UserInfo {
private final StringProperty name = new SimpleStringProperty() ;
private final StringProperty department = new SimpleStringProperty() ;
// etc...
public StringProperty nameProperty() {
return name ;
}
public final String getName() {
return nameProperty().get();
}
public final void setName(String name) {
nameProperty().set(name);
}
// etc...
}
(and similarly for Preferences
, Appearance
, etc.)
Now you define controllers for you individual screens that use a model, e.g.
public class UserInfoController {
private final UserInfo userInfo ;
@FXML
private TextField name ;
@FXML
private ComboBox<String> department ;
public UserInfoController(UserInfo userInfo) {
this.userInfo = userInfo ;
}
public void initialize() {
name.textProperty().bindBidirectional(userInfo.nameProperty());
department.valueProperty().bindBidirectional(userInfo.departmentProperty());
}
}
and then you main controller looks like:
public class MainController {
@FXML
private BorderPane root ;
@FXML
private ListView<String> selector ;
private Settings settings = new Settings() ; // or pass in from somewhere else..
public void initialize() {
selector.getSelectionModel().selectedItemProperty().addListener((obs, oldSelection, newSelection) -> {
if ("User Information".equals(newSelection)) {
loadScreen("UserInfo.fxml", new UserInfoController(settings.getUserInfo()));
} else if ("Preferences".equals(newSelection)) {
loadScreen("Preferences.fxml", new PreferencesController(settings.getPreferences()));
} else if ("Appearance".equals(newSelection)) {
loadScreen("Appearance.fxml", new AppearanceController(settings.getAppearance()));
} else {
root.setCenter(null);
}
}
private void loadScreen(String resource, Object controller) {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource(resource));
loader.setController(controller);
root.setCenter(loader.load());
} catch (IOException exc) {
exc.printStackTrace();
root.setCenter(null);
}
}
}
(Obviously you can make the handler for the list view cleaner by defining a simple view class encapsulating the resource name, display name, and a factory for the controller, and populating the list view with it, instead of switching on strings.)
Note that since you are setting the controller on the FXMLLoader
in code, UserInfo.fxml
, Preferences.fxml
and Appearance.fxml
should not have a fx:controller
attribute defined.
The second option is just a mild refactoring of this. Create the controllers once and keep a reference to them. Note that if you wanted, you could get rid of the model in this version, as the controllers have the data, so you can just refer back to them. So this might look like
public class UserInfoController {
@FXML
private TextField name ;
@FXML
private ComboBox<String> department ;
private final StringProperty nameProp = new SimpleStringProperty();
private final ObjectProperty<String> departmentProp = new SimpleObjectProperty();
public StringProperty nameProperty() {
return nameProp;
}
public final String getName() {
return nameProperty().get();
}
public final void setName(String name) {
nameProperty().set(name);
}
public ObjectProperty<String> departmentProperty() {
return departmentProp ;
}
public final String getDepartment() {
return departmentProperty().get();
}
public final void setDepartment(String department) {
departmentProperty().set(department);
}
public void initialize() {
// initialize controls with data currently in properties,
// and ensure changes to controls are written back to properties:
name.textProperty().bindBidirectional(nameProp);
department.valueProperty().bindBidirectional(departmentProp);
}
}
and then
public class MainController {
@FXML
private BorderPane root ;
@FXML
private ListView<String> selector ;
private UserInfoController userInfoController = new UserInfoController();
private PreferencesController preferencesController = new PreferencesController();
private AppearanceController appearanceController = new AppearanceController();
public void initialize() {
// initialize controllers with data if necessary...
selector.getSelectionModel().selectedItemProperty().addListener((obs, oldSelection, newSelection) -> {
selector.getSelectionModel().selectedItemProperty().addListener((obs, oldSelection, newSelection) -> {
if ("User Information".equals(newSelection)) {
loadScreen("UserInfo.fxml", userInfoController);
} else if ("Preferences".equals(newSelection)) {
loadScreen("Preferences.fxml", preferencesController);
} else if ("Appearance".equals(newSelection)) {
loadScreen("Appearance.fxml", appearanceController);
} else {
root.setCenter(null);
}
}
}
private void loadScreen(String resource, Object controller) {
// as before...
}
}
This works because you don't create new controllers when the FXML files are reloaded, and the initialize methods in the controllers update the controls with the data already there. (Note which way round the bindBidirectional
methods are invoked.)
The third option can be implemented either in the main controller, or in the main fxml file. To implement it in the controller, you basically do
public class MainController {
@FXML
private BorderPane root ;
@FXML
private ListView<String> selector ;
private Parent userInfo ;
private Parent prefs;
private Parent appearance;
// need controllers to get data later...
private UserInfoController userInfoController ;
private PreferencesController prefsController ;
private AppearanceController appearanceController ;
public void initialize() throws IOException {
FXMLLoader userInfoLoader = new FXMLLoader(getClass().getResource("userInfo.fxml));
userInfo = userInfoLoader.load();
userInfoController = userInfoLoader.getController();
FXMLLoader prefsLoader = new FXMLLoader(getClass().getResource("preferences.fxml));
prefs = prefsLoader.load();
prefsController = prefsLoader.getController();
FXMLLoader appearanceLoader = new FXMLLoader(getClass().getResource("appearance.fxml));
appearance = appearanceLoader.load();
appearanceController = appearanceLoader.getController();
// configure controllers with data if needed...
selector.getSelectionModel().selectedItemProperty().addListener((obs, oldSelection, newSelection) -> {
if ("User Information".equals(newSelection)) {
root.setCenter(userInfo);
} else if ("Preferences".equals(newSelection)) {
root.setCenter(prefs);
} else if ("Appearance".equals(newSelection)) {
root.setCenter(prefs);
} else {
root.setCenter(null);
}
}
}
}
Note here you would revert to the usual fx:controller
attribute in your FXML files.
This will work as you are only loading the FXML files once, so the view simply persists with all its state.
If you want to define the views in FXML in this approach, you can:
Main fxml file:
<!-- imports etc omitted -->
<BorderPane xmlns="http://javafx.com/javafx/8.0.111" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="com.example.MainController">
<left>
<ListView fx:id="selector" />
</left>
<fx:define>
<fx:include fx:id="userInfo" source="UserInfo.fxml" >
</fx:define>
<fx:define>
<fx:include fx:id="prefs" source="Preferences.fxml" >
</fx:define>
<fx:define>
<fx:include fx:id="appearance" source="Appearance.fxml" >
</fx:define>
</BorderPane>
The rule for FXML-injection for <fx:include>
is that the root of the included FMXL file is injected with the specified fx:id
(e.g. userInfo
) and the controllers for those included files ("nested controllers") are injected to the field with the name give when "Controller"
is appended to the fx:id
(e.g. to userInfoController
). So the main controller for this would now look like
public class MainController {
@FXML
private BorderPane root ;
@FXML
private ListView<String> selector ;
@FXML
private Parent userInfo ;
@FXML
private Parent prefs;
@FXML
private Parent appearance;
// need controllers to get data later...
@FXML
private UserInfoController userInfoController ;
@FXML
private PreferencesController prefsController ;
@FXML
private AppearanceController appearanceController ;
public void initialize() {
// configure controllers with data if needed...
selector.getSelectionModel().selectedItemProperty().addListener((obs, oldSelection, newSelection) -> {
if ("User Information".equals(newSelection)) {
root.setCenter(userInfo);
} else if ("Preferences".equals(newSelection)) {
root.setCenter(prefs);
} else if ("Appearance".equals(newSelection)) {
root.setCenter(prefs);
} else {
root.setCenter(null);
}
}
}
}