javafxtableviewnested-properties

How to update JavaFX TableView if nested object binding changes?


I have an ObservableList<Member> of members that I want to display in a TableView<Member>.

The Member class consists of a Person (and other values which are not of importance as they will not appear in the tableview). Each Person has a StringProperty "name" and an ObjectProperty<Dog> "dog". Also each dog has a StringProperty "name".

The tableview should show the name of the person in the first column and the name of the dog in the second column. Like this: Example table view

I managed to achieve this by binding both TableColumn<Member, Person> to the member.personProperty() and using custom cell factories to either display the name of the person or the name of the dog.

Now, I also have to ComboBoxes through which the user is able to update the names in the selected row of the TableView. Therefore I created bindings between the selected item and the valueProperty() of the combo boxes.

While the table view updates the cell for the person's name if it is changed via the combo box, the cell for the dog's name doesn't automatically show the new value. I know that this is because the Member doesn't get notified about the changes to the dog property. But I haven't found a solution how to make an object aware of changes within it's sub objects.

So I guess, in summary, my questions are (1) how to display nested objects in a flat table view and (2) how to automatically update the table view if any of the (potentially) nested properties changes.

Example code:

package sample;

import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.scene.control.*;
import javafx.util.StringConverter;

public class ControllerString {
    private class Dog
    {
        private final StringProperty name;

        public Dog(String name) {
            this.name = new SimpleStringProperty(name);
        }

        public String getName() {
            return name.get();
        }

        public StringProperty nameProperty() {
            return name;
        }

        public void setName(String name) {
            this.name.set(name);
        }
    }

    private class Person
    {
        private final StringProperty name;
        private final ObjectProperty<Dog> dog;

        public Person(String name, Dog dog) {
            this.name = new SimpleStringProperty(name);
            this.dog = new SimpleObjectProperty<>(dog);
        }

        public String getName() {
            return name.get();
        }

        public StringProperty nameProperty() {
            return name;
        }

        public void setName(String name) {
            this.name.set(name);
        }

        public Dog getDog() {
            return dog.get();
        }

        public ObjectProperty<Dog> dogProperty() {
            return dog;
        }

        public void setDog(Dog dog) {
            this.dog.set(dog);
        }
    }

    private class Member
    {
        private final ObjectProperty<Person> person;

        public Member(Person person) {
            this.person = new SimpleObjectProperty<>(person);
        }

        public Person getPerson() {
            return person.get();
        }

        public ObjectProperty<Person> personProperty() {
            return person;
        }

        public void setPerson(Person person) {
            this.person.set(person);
        }
    }

    @FXML
    private TableView<Member> membersTableView;
    @FXML
    private TableColumn<Member, Person> nameTableColumn;
    @FXML
    private TableColumn<Member, Person> dogTableColumn;
    @FXML
    private ComboBox<Person> nameComboBox;
    @FXML
    private ComboBox<Dog> dogComboBox;

    private final ObservableList<Member> members = FXCollections.observableArrayList();

    @FXML
    private void initialize()
    {
        nameTableColumn.setCellValueFactory(cellData -> cellData.getValue().personProperty());
        nameTableColumn.setCellFactory(column -> new TableCell<Member, Person>() {
            @Override
            protected void updateItem(Person person, boolean empty) {
                super.updateItem(person, empty);

                setContentDisplay(ContentDisplay.TEXT_ONLY);

                if(person == null || empty)
                {
                    setText(null);
                }
                else
                {
                    setText(person.getName());
                }
            }
        });

        dogTableColumn.setCellValueFactory(cellData -> cellData.getValue().personProperty());
        dogTableColumn.setCellFactory(column -> new TableCell<Member, Person>()
        {
            @Override
            protected void updateItem(Person person, boolean empty) {
                super.updateItem(person, empty);

                setContentDisplay(ContentDisplay.TEXT_ONLY);

                if(person == null || empty)
                {
                    setText(null);
                }
                else
                {
                    setText(person.getDog().getName());
                }
            }
        });

        nameComboBox.setConverter(new StringConverter<Person>() {
            @Override
            public String toString(Person person) {
                if(person == null)
                {
                    return null;
                }

                return person.getName();
            }

            @Override
            public Person fromString(String name) {
                return new Person(name, new Dog("Puppy"));
            }
        });

        dogComboBox.setConverter(new StringConverter<Dog>() {
            @Override
            public String toString(Dog dog) {
                if(dog == null)
                {
                    return null;
                }

                return dog.getName();
            }

            @Override
            public Dog fromString(String name) {
                return new Dog(name);
            }
        });

        membersTableView.getSelectionModel().getSelectedItems().addListener((ListChangeListener<Member>) change -> {
            while(change.next()) {
                if(change.wasRemoved()) {
                    Member oldValue = change.getRemoved().get(0);
                    nameComboBox.valueProperty().unbindBidirectional(oldValue.personProperty());
                }

                if(change.wasAdded()) {
                    Member newValue = change.getAddedSubList().get(0);
                    nameComboBox.valueProperty().bindBidirectional(newValue.personProperty());
                }
            }
        });

        nameComboBox.valueProperty().addListener(((observable, oldValue, newValue) -> {
            if(oldValue != null) {
                dogComboBox.valueProperty().unbindBidirectional(oldValue.dogProperty());
            }

            if(newValue != null)
            {
                dogComboBox.valueProperty().bindBidirectional(newValue.dogProperty());
            }
        }));

        membersTableView.setItems(members);

        members.add(new Member(new Person("Bob", new Dog("Caesar"))));
    }
}

Solution

  • If you modify the visibility of Person and Dog to public, Bindings.select can be used with the cellValueFactorys which allows you to get rid of the custom TableCell implementations:

    public class Dog {
        ...
    }
    
    public class Person {
        ...
    }
    
    ...
    
    @FXML
    private TableColumn<Member, String> nameTableColumn;
    @FXML
    private TableColumn<Member, String> dogTableColumn;
    
    ...
    
    @FXML
    private void initialize() {
        nameTableColumn.setCellValueFactory(cellData -> Bindings.select(cellData.getValue().personProperty(), "name"));
        dogTableColumn.setCellValueFactory(cellData -> Bindings.select(cellData.getValue().personProperty(), "dog", "name"));
        ...
    

    If one of the intermediate properties can contain null though, you can expect this to cause a lot of warnings...