0

Specifically, I have a Primary Stage and two other Stages initialized from the Primary Stage when a menu item is selected.

All three Stages contain TableViews that display different views of my data, and allow relevant actions. When the user is working in one of these Stages and performs an action that changes the data, I would like the changes to be reflected in all three TableViews.

Each of the TableViews is backed by an ObservableArrayList. They update automatically when an element is added or removed, but I have to call the TableView.refresh() method anytime the data changes in any other way and I want it to show.

From reading other posts it seems that it is possible to pass a reference of a Parent Controller object to a Child controller, but it is not considered good practice. It occurred to me that perhaps I could create a new class that would be responsible for refreshing the tables in all 3 Stages, however that would require obtaining a reference to each of the controller objects somehow.

I'm stuck and I'd be grateful for any suggestions!

  • 3
    You could observe your actual model for changes and use that to trigger the refresh? – Zephyr Mar 25 '21 at 01:52
  • 1
    https://stackoverflow.com/questions/32342864/applying-mvc-with-javafx – SedJ601 Mar 25 '21 at 04:07
  • 2
    _I have to call the TableView.refresh()_ then there's something wrong in your setup .. [mcve] please – kleopatra Mar 25 '21 at 05:41
  • "They update automatically when an element is added or removed, but I have to call the TableView.refresh() method anytime the data changes in any other way and I want it to show." --> You should investigate ObservableList extractors: https://docs.oracle.com/javase/8/javafx/api/javafx/collections/FXCollections.html#observableList-java.util.List-javafx.util.Callback- – DaveB Mar 27 '21 at 13:59

1 Answers1

0

In attempting to create a minimal reproducible example I figured out what I was doing wrong:

In my original code I was converting Simple Double Properties to Simple String Properties before displaying them in the table, in order to control how they were displayed. The conversion was executed in the overwritten Call() method of Column.setCellValueFactory(). Somehow this conversion was causing the table not to respond to data changes right away. Here is some code to illustrate what I am talking about:

public class Controller {

    @FXML
    public TableView<Person> mainTable;
    @FXML
    public Button editButton;
    @FXML
    public BorderPane mainBorderPane;
    public Button openSecondButton;
    public Button refreshButton;


    public void initialize(){

        DataModel.getInstance().addPerson(new Person("Frank", 1, 20));
        DataModel.getInstance().addPerson(new Person("Cindy", 2, 20));
        DataModel.getInstance().addPerson(new Person("Eric", 3, 67));

        mainTable.setItems(DataModel.getInstance().getPeople());

        TableColumn<Person, String> nameColumn = new TableColumn<>("Name");
        nameColumn.setCellValueFactory(new Callback<TableColumn.CellDataFeatures<Person, String>, ObservableValue<String>>(){
            @Override
            public ObservableValue<String> call(TableColumn.CellDataFeatures<Person, String> c){
                return c.getValue().nameProperty();
            }
        });
        TableColumn<Person, Integer> idColumn = new TableColumn<>("Id");
        idColumn.setCellValueFactory(new Callback<TableColumn.CellDataFeatures<Person, Integer>, ObservableValue<Integer>>() {
            @Override
            public ObservableValue<Integer> call(TableColumn.CellDataFeatures<Person, Integer> person) {
                return person.getValue().idProperty().asObject();
            }
        });
        TableColumn<Person, Integer> ageColumn = new TableColumn<>("Age");
        ageColumn.setCellValueFactory(new Callback<TableColumn.CellDataFeatures<Person, Integer>, ObservableValue<Integer>>() {
            @Override
            public ObservableValue<Integer> call(TableColumn.CellDataFeatures<Person, Integer> person) {
                return person.getValue().ageProperty().asObject();
            }
        });

        TableColumn<Person, String> ageStringColumn = new TableColumn<>("Age String");
        ageStringColumn.setCellValueFactory(new Callback<TableColumn.CellDataFeatures<Person, String>, ObservableValue<String>>() {
            @Override
            public ObservableValue<String> call(TableColumn.CellDataFeatures<Person, String> person) {
                return new SimpleStringProperty(String.valueOf(person.getValue().getAge()));
            }
        });

        mainTable.getColumns().addAll(nameColumn, idColumn, ageColumn, ageStringColumn);
    }

    @FXML
    private void showSecondStage(ActionEvent actionEvent) throws IOException {
        Stage secondStage = new Stage();
        secondStage.setTitle("Secondary Stage");
        secondStage.initModality(Modality.NONE);
        secondStage.initStyle(StageStyle.UTILITY);
        Parent parent = FXMLLoader.load(getClass().getResource("secondary.fxml"));
        secondStage.setScene(new Scene(parent));
        secondStage.initOwner(mainBorderPane.getScene().getWindow());
        secondStage.show();
    }

    public boolean handleEditPersonRequest() {

        Dialog<ButtonType> dialog = new Dialog<>();
        dialog.initOwner(mainBorderPane.getScene().getWindow());
        dialog.setTitle("Edit Person");
        FXMLLoader fxmlLoader = new FXMLLoader();
        fxmlLoader.setLocation(Controller.class.getResource("dialog.fxml"));
        try {
            dialog.getDialogPane().setContent(fxmlLoader.load());
        } catch (IOException e) {
            e.printStackTrace();
        }
        DialogController controller = fxmlLoader.getController();
        controller.setFields(mainTable.getSelectionModel().getSelectedItem());
        dialog.getDialogPane().getButtonTypes().add(ButtonType.OK);
        dialog.getDialogPane().getButtonTypes().add(ButtonType.CANCEL);
        Button okButton = (Button) dialog.getDialogPane().lookupButton(ButtonType.OK);
        okButton.addEventFilter(ActionEvent.ACTION, event -> {
            if (!controller.validateAndProcess()) {
                event.consume();
                System.out.println("Invalid entry, try again");
            }});

        Optional<ButtonType> result = dialog.showAndWait();
        return result.isPresent() && result.get() == ButtonType.OK;
    }

    public void refreshTable(ActionEvent actionEvent) {
        mainTable.refresh();
    }
}

And the .fxml file

<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.VBox?>
<BorderPane fx:id="mainBorderPane" fx:controller="sample.Controller"
            xmlns:fx="http://javafx.com/fxml" >
    <left>
        <VBox>
        <Button text="Edit Person" fx:id="editButton" onAction="#handleEditPersonRequest"/>
        <Button text = "Open Second Window" fx:id="openSecondButton" onAction="#showSecondStage"/>
            <Button text="Refresh table" fx:id="refreshButton" onAction="#refreshTable"/>
        </VBox>
    </left>
    <center>
        <TableView fx:id="mainTable" />
    </center>
</BorderPane>

Here is the dialog controller:

public class DialogController {
    public TextField nameField;
    public TextField idField;
    public TextField ageField;
    public Person person;

    public void setFields(Person selectedPerson) {
        person = selectedPerson;
        nameField.setText(person.getName());
        idField.setText(String.valueOf(person.getId()));
        ageField.setText(String.valueOf(person.getAge()));
    }

    public boolean validateAndProcess(){
        try{
            String name = nameField.getText();
            int id = Integer.parseInt(idField.getText());
            int age = Integer.parseInt(ageField.getText());
            person.setName(name);
            person.setId(id);
            person.setAge(age);
            return true;
        }catch (NumberFormatException | NullPointerException e){
            e.printStackTrace();
            return false;
        }
    }
}

And it's .fxml file

<?xml version="1.0" encoding="UTF-8"?>

<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<VBox  xmlns="http://javafx.com/javafx"
            xmlns:fx="http://javafx.com/fxml"
            fx:controller="sample.DialogController"
            prefHeight="400.0" prefWidth="600.0">
    <Label text="Name"/>
    <TextField fx:id="nameField"/>
    <Label text="Id"/>
    <TextField fx:id="idField"/>
    <Label text="Age"/>
    <TextField fx:id="ageField"/>

</VBox>

I'm not going to include the code for the second window, as it's not needed to see the problem.