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 ComboBox
es 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"))));
}
}
If you modify the visibility of Person
and Dog
to public
, Bindings.select
can be used with the cellValueFactory
s 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...