Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Derived Observable Values

In a user interface, we often want to display not only the actual data but also values calculated from that data, such as sums, averages, combinations, or similar derived values.

For example, a person's full name can be calculated by combining the first name and last name:

String fullName = person.getFirstName() + person.getLastName();

However, a value calculated this way does not automatically update in the user interface if the first name or last name changes.

Example

Consider the following application for entering player information:

MainController.java
// Imports hidden
 import javafx.collections.FXCollections;
 import javafx.collections.ObservableList;
 import javafx.fxml.FXML;
 import javafx.fxml.Initializable;
 import javafx.scene.control.Button;
 import javafx.scene.control.Label;
 import javafx.scene.control.TableColumn;
 import javafx.scene.control.TableView;
 import javafx.scene.control.cell.TextFieldTableCell;
 import javafx.util.converter.NumberStringConverter;
 
 import java.net.URL;
 import java.util.ResourceBundle;
public class MainController implements Initializable {

    @FXML
    private TableColumn<Player, String> nameColumn;

    @FXML
    private TableColumn<Player, Number> birthYearColumn;

    @FXML
    private TableColumn<Player, Number> ageColumn;

    @FXML
    private TableView<Player> playersTable;

    @FXML
    private Label playerCountLabel;

    @FXML
    private Button addPlayerButton;

    private ObservableList<Player> players = FXCollections.observableArrayList();

    @Override
    public void initialize(URL url, ResourceBundle resourceBundle) {
        nameColumn.setCellFactory(TextFieldTableCell.forTableColumn());
        nameColumn.setCellValueFactory(cellData -> cellData.getValue().nameProperty());

        birthYearColumn.setCellFactory(TextFieldTableCell.forTableColumn(new NumberStringConverter("####")));
        birthYearColumn.setCellValueFactory(cellData -> cellData.getValue().birthYearProperty());

        // This does not work!
        //
        // setCellValueFactory requires an ObservableValue,
        // but age is an int value.
        //
        // ageColumn.setCellValueFactory(cellData -> LocalDate.now().getYear() -  cellData.getValue().getBirthYear()
        // );

        addPlayerButton.setOnAction(event -> {
            Player newPlayer = new Player();
            newPlayer.setName("New Player");
            newPlayer.setBirthYear(2000);
            players.add(newPlayer);
        });

        playersTable.setItems(players);
    }
}

In this example we can see two problems:

  • Adding a new player does not update the player count.
  • Editing a player's birth year does not update the player's age.

Converting an Observable Value into Another Value

All ObservableValue types, such as StringProperty, IntegerProperty, FloatProperty, and so on, provide a helper method called map() (See JavaDoc) that allows calculations to be performed on the value.

For example, a player's age can be calculated from the birth year as follows:

ObservableValue<Number> age = player.birthYearProperty().map(year -> LocalDate.now().getYear() - year.intValue());

The value contained in the age variable is then recalculated using LocalDate.now().getYear() - year.intValue() whenever the player's birth year changes. Because ObservableValue is itself observable, it can be used directly in a setCellValueFactory call:

public void initialize(URL url, ResourceBundle resourceBundle) {
    nameColumn.setCellFactory(TextFieldTableCell.forTableColumn());
    nameColumn.setCellValueFactory(cellData -> cellData.getValue().nameProperty());

    birthYearColumn.setCellFactory(TextFieldTableCell.forTableColumn(new NumberStringConverter("####")));
    birthYearColumn.setCellValueFactory(cellData -> cellData.getValue().birthYearProperty());

    // HIGHLIGHT_GREEN_BEGIN
    ageColumn.setCellValueFactory(cellData ->
        cellData.getValue().birthYearProperty().map(
            birthYear -> LocalDate.now().getYear() - birthYear.intValue()));
    // HIGHLIGHT_GREEN_END

    addPlayerButton.setOnAction(event -> {
        Player newPlayer = new Player();
        newPlayer.setName("New Player");
        newPlayer.setBirthYear(2000);
        players.add(newPlayer);
    });

    playersTable.setItems(players);
}

Converting a Function into an Observable Value

The number of players can be obtained by calling: players.size(). However, the size() method does not return an observable value, and an ObservableList does not provide the map() method discussed above. Nevertheless, any function can be converted into an observable value using the Bindings (See JavaDoc) class and its createXBinding helper methods. Here, X refers to the type of observable value, for example Integer, Long, String, or Object. Since size() returns an integer, we use Bindings.createIntegerBinding() (See JavaDoc) :

IntegerBinding playerCount = Bindings.createIntegerBinding(() -> players.size(), players);

Bindings.createIntegerBinding() takes at least two parameters: a lambda expression used to calculate the observable value and one or more Observable values whose changes trigger recalculation. In this example, the first parameter specifies that playerCount is always calculated using: players.size(). The second parameter, players, specifies that the value must be updated whenever the contents of the players list change.

The Bindings.createXBinding method returns a binding object, which can be used like any other observable value. In this case, we can bind the text of playerCountLabel to it:

public void initialize(URL url, ResourceBundle resourceBundle) {
    nameColumn.setCellFactory(TextFieldTableCell.forTableColumn());
    nameColumn.setCellValueFactory(cellData -> cellData.getValue().nameProperty());

    birthYearColumn.setCellFactory(TextFieldTableCell.forTableColumn(new NumberStringConverter("####")));
    birthYearColumn.setCellValueFactory(cellData -> cellData.getValue().birthYearProperty());

    ageColumn.setCellValueFactory(cellData -> cellData.getValue().birthYearProperty().map(birthYear -> LocalDate.now().getYear() - birthYear.intValue()));

    addPlayerButton.setOnAction(event -> {
        Player newPlayer = new Player();
        newPlayer.setName("New Player");
        newPlayer.setBirthYear(2000);
        players.add(newPlayer);
    });

    // HIGHLIGHT_GREEN_BEGIN
    IntegerBinding playerCount = Bindings.createIntegerBinding(() -> players.size(), players);
    playerCountLabel.textProperty().bind(playerCount.asString());
    // HIGHLIGHT_GREEN_END

    playersTable.setItems(players);
}

Using the bind() method of a Property, we can bind one value to another observable value.

In this case, the text of playerCountLabel is bound to the number of players.

As a result, whenever the players list changes:

  • playerCount detects the change and recalculates its value using players.size().
  • playerCount.asString() detects the change in playerCount and updates its own value by calling playerCount.toString().
  • playerCountLabel.textProperty() detects the change in playerCount.asString() and updates its displayed text accordingly.

After these changes, both the player count and each player's age update automatically.