There are three ways I can see for you to approach this. I have created a greatly simplified version of the application to demonstrate each of these and focus on the three different approaches. They all use the following:
A simplified Customer
class (with just one property):
package org.jamesd.examples.tableexample;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class Customer {
private final StringProperty name = new SimpleStringProperty();
public Customer(String name){
setName(name);
}
public final String getName() {
return nameProperty().get();
}
public StringProperty nameProperty() {
return name;
}
public final void setName(String name) {
nameProperty().set(name);
}
}
The main FXML file:
CustomerOverview.fxml
:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.Button?>
<BorderPane xmlns:fx="http://javafx.com/fxml"
fx:controller="org.jamesd.examples.tableexample.CustomerOverviewController">
<padding>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0"/>
</padding>
<top>
<HBox spacing="5">
<Label text="Search:"/>
<TextField fx:id="searchField" onTextChange="#updateFilterText"/>
</HBox>
</top>
<center>
<TableView fx:id="customerTable">
<columns>
<TableColumn fx:id="nameColumn" text="Name"/>
</columns>
</TableView>
</center>
<bottom>
<HBox spacing="5" alignment="CENTER_RIGHT">
<Button text="Add.." onAction="#addCustomer"/>
</HBox>
</bottom>
</BorderPane>
and the FXML for the "Add Customer" dialog:
AddCustomerDialog.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.geometry.Insets?>
<GridPane xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="org.jamesd.examples.tableexample.AddCustomerDialogController"
hgap="5"
vgap="5">
<padding><Insets topRightBottomLeft="10"/></padding>
<columnConstraints>
<ColumnConstraints hgrow="NEVER" halignment="RIGHT"/>
<ColumnConstraints hgrow="ALWAYS" halignment="LEFT"/>
</columnConstraints>
<Label text="Name:" GridPane.rowIndex="0" GridPane.columnIndex="0"/>
<TextField fx:id="nameField" GridPane.rowIndex="0" GridPane.columnIndex="1"/>
<HBox GridPane.rowIndex="1" GridPane.columnIndex="0" GridPane.columnSpan="2" alignment="CENTER_RIGHT" spacing="5">
<Button text="OK" defaultButton="true" onAction="#commit" disable="${nameField.text.blank}"/>
<Button text="Cancel" cancelButton="true" onAction="#cancel"/>
</HBox>
</GridPane>
The application is launched in the first two approaches in the usual way:
package org.jamesd.examples.tableexample;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;
import java.io.IOException;
public class CustomerApp extends Application {
@Override
public void start(Stage stage) throws IOException {
FXMLLoader fxmlLoader = new FXMLLoader(CustomerApp.class.getResource("CustomerOverview.fxml"));
Scene scene = new Scene(fxmlLoader.load());
stage.setTitle("Customer Overview");
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
Method 1: Wait for the dialog to close and then query its controller
This is the simplest approach but not really the most elegant. The basic idea is to have the controller for the dialog expose a Customer
object, which is set when the user presses "OK". If the user cancels, it can be set to null
.
In the controller for the main view, use showAndWait()
to show the Stage
containing "Add Customer" dialog. This will block execution until that stage closes, so you can then retrieve the customer.
This is basically equivalent to SedJ601's solution.
The main controller looks like this:
package org.jamesd.examples.tableexample;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.collections.transformation.SortedList;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.stage.Modality;
import javafx.stage.Stage;
import java.io.IOException;
public class CustomerOverviewController {
@FXML
private TextField searchField;
@FXML
private TableView<Customer> customerTable;
@FXML
private TableColumn<Customer, String> nameColumn;
private final ObservableList<Customer> allCustomers = FXCollections.observableArrayList();
private final FilteredList<Customer> filteredCustomers = new FilteredList<>(allCustomers);
@FXML
private void initialize() {
nameColumn.setCellValueFactory(cellData -> cellData.getValue().nameProperty());
updateFilterText();
SortedList<Customer> sortedCustomers = new SortedList<>(filteredCustomers);
sortedCustomers.comparatorProperty().bind(customerTable.comparatorProperty());
customerTable.setItems(sortedCustomers);
}
@FXML
private void updateFilterText() {
String filter = searchField.getText();
if (filter.isBlank()) {
filteredCustomers.setPredicate(customer -> true);
} else {
filteredCustomers.setPredicate(customer -> customer.getName().contains(filter));
}
}
@FXML
private void addCustomer() throws IOException {
FXMLLoader loader = new FXMLLoader(getClass().getResource("AddCustomerDialog.fxml"));
Parent root = loader.load();
Scene scene = new Scene(root);
Stage stage = new Stage();
stage.setTitle("Add Customer");
stage.setScene(scene);
stage.initOwner(customerTable.getScene().getWindow());
stage.initModality(Modality.WINDOW_MODAL);
// showAndWait() will block execution, so code after this line is executed when the stage is closed:
stage.showAndWait();
// get the Customer the user added, if there is one, and add it to the table's items list:
AddCustomerDialogController controller = loader.getController();
Customer customer = controller.getCustomer();
if (customer != null) {
allCustomers.add(customer);
}
}
}
and the controller for the dialog looks like this:
package org.jamesd.examples.tableexample;
import javafx.fxml.FXML;
import javafx.scene.control.TextField;
public class AddCustomerDialogController {
@FXML
private TextField nameField;
private Customer customer;
@FXML
private void commit() {
String name = nameField.getText();
if (! name.isBlank()) {
this.customer = new Customer(nameField.getText());
}
closeWindow();
}
@FXML
private void cancel() {
this.customer = null;
closeWindow();
}
private void closeWindow() {
nameField.getScene().getWindow().hide();
}
public Customer getCustomer() {
return customer;
}
}
Method 2: Pass the table's items list to the dialog controller
This is also a fairly simple method, and it bears some relationship to MVC (see below) in the sense you are sharing data between two different controllers, one of which updates the data and one of which observes it. However, it's not a very robust solution and relies on making sure, for example, you set the list in the dialog controller before the dialog is displayed. This will work for small applications but for medium or large scale applications the code will get quite complex.
In this method, the controller for the dialog is responsible for adding the new customer to the table's items list. For that to happen, it needs a reference to the list, which is passed to that controller after the FXML is loaded.
The controller for the dialog looks like this:
package org.jamesd.examples.tableexample;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.scene.control.TextField;
public class AddCustomerDialogController {
private ObservableList<Customer> customerList;
@FXML
private TextField nameField;
public void setCustomerList(ObservableList<Customer> customerList) {
this.customerList = customerList;
}
@FXML
private void commit() {
String name = nameField.getText();
if (! name.isBlank()) {
// Add new customer to list of customers:
customerList.add(new Customer(nameField.getText()));
}
closeWindow();
}
@FXML
private void cancel() {
closeWindow();
}
private void closeWindow() {
nameField.getScene().getWindow().hide();
}
}
and the main controller looks like this. The only difference here is in the addCustomer()
method.
package org.jamesd.examples.tableexample;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.collections.transformation.SortedList;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.stage.Modality;
import javafx.stage.Stage;
import java.io.IOException;
public class CustomerOverviewController {
@FXML
private TextField searchField;
@FXML
private TableView<Customer> customerTable;
@FXML
private TableColumn<Customer, String> nameColumn;
private final ObservableList<Customer> allCustomers = FXCollections.observableArrayList();
private final FilteredList<Customer> filteredCustomers = new FilteredList<>(allCustomers);
@FXML
private void initialize() {
nameColumn.setCellValueFactory(cellData -> cellData.getValue().nameProperty());
updateFilterText();
SortedList<Customer> sortedCustomers = new SortedList<>(filteredCustomers);
sortedCustomers.comparatorProperty().bind(customerTable.comparatorProperty());
customerTable.setItems(sortedCustomers);
}
@FXML
private void updateFilterText() {
String filter = searchField.getText();
if (filter.isBlank()) {
filteredCustomers.setPredicate(customer -> true);
} else {
filteredCustomers.setPredicate(customer -> customer.getName().contains(filter));
}
}
@FXML
private void addCustomer() throws IOException {
FXMLLoader loader = new FXMLLoader(getClass().getResource("AddCustomerDialog.fxml"));
Parent root = loader.load();
// pass the table's list to the dialog controller:
AddCustomerDialogController customerDialogController = loader.getController();
customerDialogController.setCustomerList(allCustomers);
Scene scene = new Scene(root);
Stage stage = new Stage();
stage.setTitle("Add Customer");
stage.setScene(scene);
stage.initOwner(customerTable.getScene().getWindow());
stage.initModality(Modality.WINDOW_MODAL);
stage.show();
}
}
Method 3: Use a MVC Design
Model-View-Controller (MVC) is a standard design pattern for GUI applications. This is explained a little more at Applying MVC With JavaFx. There are some variants of MVC (Model-View-Presenter and Model-View-ViewModel, for example) and in my opinion MVC with FXML usually looks a little more like MVP. I would encourage you to read up further on this and related design patterns. There are many resources available online.
The common thread to all these patterns is that the data and business logic are factored out into a separate class (the "Model"). The View typically observes the model and responds to changes in it. (JavaFX properties and observable lists are designed exactly for this purpose.) The controller/presenter/etc updates the model, which then automatically causes updates to any views that are observing it.
This approach is the most robust but needs the most learning and for you to define some additional classes. However, for medium and larger applications there is payoff for this as your controller classes and views become simpler and can just delegate a lot of work to the model. It will also make your code more consistent (fewer special cases: everything just works the same way by accessing a central model) and since it follows a standard pattern is easier for other programmers to understand.
A simple model for this application might expose the list of customers, a search filter property, and the filtered list:
package org.jamesd.examples.tableexample;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
public class Model {
private final ObservableList<Customer> allCustomers = FXCollections.observableArrayList();
private final FilteredList<Customer> filteredCustomers = new FilteredList<>(allCustomers);
private final StringProperty searchText = new SimpleStringProperty();
public Model() {
searchText.addListener((obs, oldText, newText) -> {
if (newText.isBlank()) {
filteredCustomers.setPredicate(customer -> true);
} else {
filteredCustomers.setPredicate(customer -> customer.getName().contains(newText));
}
});
}
// Convenience method, equivalent to getAllCustomers().add(customer)
public void addCustomer(Customer customer) {
allCustomers.add(customer);
}
public ObservableList<Customer> getAllCustomers() {
return allCustomers;
}
public FilteredList<Customer> getFilteredCustomers() {
return filteredCustomers;
}
public String getSearchText() {
return searchText.get();
}
public StringProperty searchTextProperty() {
return searchText;
}
public void setSearchText(String searchText) {
this.searchText.set(searchText);
}
}
Now if the dialog controller has a reference to the model, it can add the new customer to the list simply by calling model.addCustomer(..)
:
package org.jamesd.examples.tableexample;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.scene.control.TextField;
public class AddCustomerDialogController {
@FXML
private TextField nameField;
private final Model model;
public AddCustomerDialogController(Model model) {
this.model = model;
}
@FXML
private void commit() {
String name = nameField.getText();
if (! name.isBlank()) {
Customer customer = new Customer(name);
model.addCustomer(customer);
}
closeWindow();
}
@FXML
private void cancel() {
closeWindow();
}
private void closeWindow() {
nameField.getScene().getWindow().hide();
}
}
The only problem here is that this controller no longer has a no-arg constructor: its constructor requires a Model
parameter. There are various ways to get around this. One is to provide a controllerFactory
to the FXMLLoader
. The controller factory is a function (a "Callback
") which determines how to create a controller object given its class (i.e. for the class defined by the fx:controller
attribute in the FXML). This doesn't really have anything to do with MVC, or this solution to the problem (though in MVC you will always need your controllers and views to have access to a shared model). But for completeness, here is a controller factory which uses some reflection to find a constructor taking a model, and invokes that constructor:
package org.jamesd.examples.tableexample;
import javafx.util.Callback;
import java.lang.reflect.Constructor;
public class ModelControllerFactory implements Callback<Class<?>, Object> {
private final Model model;
public ModelControllerFactory(Model model) {
this.model = model;
}
// This method will be called by the FXMLLoader to create a
// controller instance when the FXML is loaded. The Class<?>
// parameter that is passed into this method is determined
// by the fx:controller attribute in the FXML.
//
// The default implementation simply invokes a no-argument
// constructor on the controller class. Since we want to
// support controller classes with a constructor taking a
// Model parameter, we check for such a constructor, and
// invoke it, passing in the model instance.
//
// If no such constructor exists, we revert to the default
// implementation and call the no-arg constructor.
@Override
public Object call(Class<?> type) {
try {
// Search for a constructor taking one parameter of type Model:
for (Constructor<?> c : type.getConstructors()) {
if (c.getParameterCount() == 1 && c.getParameterTypes()[0].equals(Model.class)) {
// Call the constructor, passing the model:
return c.newInstance(model);
}
}
// no constructor taking a model found: use default constructor:
return type.getConstructor().newInstance();
} catch (Exception exc) {
if (exc instanceof RuntimeException re) {
throw re;
} else {
throw new RuntimeException(exc);
}
}
}
}
The controller for the CustomerOverview
also has a reference to the model, and uses the list in the model for the table. Note how it sets a controller factory on the FXMLLoader
to load the dialog FXML:
package org.jamesd.examples.tableexample;
import javafx.collections.transformation.SortedList;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.stage.Modality;
import javafx.stage.Stage;
import java.io.IOException;
public class CustomerOverviewController {
@FXML
private TextField searchField;
@FXML
private TableView<Customer> customerTable;
@FXML
private TableColumn<Customer, String> nameColumn;
private final Model model;
public CustomerOverviewController(Model model) {
this.model = model;
}
@FXML
private void initialize() {
nameColumn.setCellValueFactory(cellData -> cellData.getValue().nameProperty());
SortedList<Customer> sortedCustomers = new SortedList<>(model.getFilteredCustomers());
sortedCustomers.comparatorProperty().bind(customerTable.comparatorProperty());
customerTable.setItems(sortedCustomers);
}
@FXML
private void updateFilterText() {
model.setSearchText(searchField.getText());
}
@FXML
private void addCustomer() throws IOException {
FXMLLoader loader = new FXMLLoader(getClass().getResource("AddCustomerDialog.fxml"));
loader.setControllerFactory(new ModelControllerFactory(model));
Parent root = loader.load();
Scene scene = new Scene(root);
Stage stage = new Stage();
stage.setTitle("Add Customer");
stage.setScene(scene);
stage.initOwner(customerTable.getScene().getWindow());
stage.initModality(Modality.WINDOW_MODAL);
stage.show();
}
}
Finally, since this controller also needs a model to be passed to its constructor, we need to update the application class:
public class CustomerApp extends Application {
@Override
public void start(Stage stage) throws IOException {
Model model = new Model();
FXMLLoader fxmlLoader = new FXMLLoader(CustomerApp.class.getResource("CustomerOverview.fxml"));
fxmlLoader.setControllerFactory(new ModelControllerFactory(model));
Scene scene = new Scene(fxmlLoader.load());
stage.setTitle("Customer Overview");
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
Note that both controllers end up with references to the same model instance. This is important, because the dialog controller must add the new Customer
to the same list that the overview controller is using for the table.