Search code examples
javajavafxjavafx-tableview

How to show data in TableView of nested objects?


I have a TableView<Marriage> in controller's class:

public class MarriageTableView extends AbstractTableViewController {

    @FXML
    private TableView<Marriage> tableView;

    @FXML
    private TableColumn<Groom, String> groomFirstNameColumn,  groomLastNameColumn;

    @FXML
    private TableColumn<Bride, String> brideFirstNameColumn, brideLastNameColumn;

    @FXML
    private TableColumn<Marriage, LocalDate> marriageDateColumn;
    
    @FXML
    void initialize() {
        this.groomFirstNameColumn.setCellValueFactory(new PropertyValueFactory<Groom, String>("firstName")); // <- Exception here
        this.groomLastNameColumn.setCellValueFactory(new PropertyValueFactory<Groom, String>("lastName"));
        this.brideFirstNameColumn.setCellValueFactory(new PropertyValueFactory<Bride, String>("firstName"));
        this.brideLastNameColumn.setCellValueFactory(new PropertyValueFactory<Bride, String>("lastName"));
        this.marriageDateColumn.setCellValueFactory(new PropertyValueFactory<Marriage, LocalDate>("marriageDate"));
    }

In initialize method, I am trying to set CellValueFactory for each table column, so I can show Marriage object in my tableview.

However Marriage object consists of another objects of type Groom and Bride from which I am trying to show some values.

public class Marriage {
    private Groom groom;
    private Bride bride;
    private LocalDate marriageDate;
}

public class Groom:
{
    private String firstName;
    private String lastName;
}

public class Bride:
{
    private String firstName;
    private String lastName;
}

Exception I get:

java.lang.IllegalStateException: Cannot read from unreadable property firstName

How to set CellValueFactory or edit TableColumn's generic parameters, so I can show properties of nested objects?


Solution

  • One option is to use a custom cell implementation that knows how to display the desired data of Groom and Bride. For example:

    TableColumn<Marriage, Groom> groomCol = ...;
    groomCol.setCellValueFactory(new PropertyValueFactory<>("groom"));
    groomCol.setCellFactory(tc -> new TableCell<>() {
    
      @Override
      protected void updateItem(Groom item, boolean empty) {
        super.updateItem(item, empty); // must be called
        if (empty || item == null) {
          setText(null);
        } else {
          // replace with desired format
          setText(item.getFirstName() + " " + item.getLastName());
        }
      }
    });
    

    Note that the first type argument of the TableColumn must match the type argument of the TableView (Marriage in this case).


    As an aside, do you really need both a Groom and Bride class? They seem to have the same information and could possibly be combined into a single Person class.


    As another aside, you should avoid PropertyValueFactory if you can. It was designed in an era before lambda expressions as a way to make code more concise. But with lambda expressions you can do the same but with compile-time safety (and you avoid reflection). Though note this works best if you expose your model's properties as JavaFX properties. For example:

    // model class
    public class Marriage {
      private final ObjectProperty<Groom> groom = new SimpleObjectProperty<>(this, "groom");
      public final void setGroom(Groom groom) { this.groom.set(groom); }
      public final Groom getGroom() { return groom.get(); }
      public final ObjectProperty<Groom> groomProperty() { return groom; }
    
      // other properties...
    }
    
    // table column configuration
    TableColumn<Marriage, Groom> groomCol = ...;
    groomCol.setCellValueFactory(data -> data.getValue().groomProperty());
    groomCol.setCellFactory( ... );