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

Johdetut Observable-arvot

Käyttöliittymässä haluamme usein näyttää varsinaisen tiedon lisäksi tiedoista laskettuja arvoja, kuten arvojen summaa, keskiarvoa, yhdistelmää tai vastaavaa.

Esimerkiksi henkilön koko nimi voidaan laskea etunimen ja sukunimen yhdistelmänä:

String kokonimi = henkilo.getEtunimi() + henkilo.getSukunimi();

Näin laskettu arvo ei kuitenkaan päivity automaattisesti käyttöliittymässä, jos etunimi tai sukunimi muuttuu.

Esimerkki

Katsotaan seuraavaa sovellusta henkilön tietojen syöttämiseksi:

MainController.java
public class MainController implements Initializable {
    @FXML
    private TableColumn<Pelaaja, String> nimiColumn;

    @FXML
    private TableColumn<Pelaaja, Number> syntymavuosiColumn;

    @FXML
    private TableColumn<Pelaaja, Number> ikaColumn;

    @FXML
    private TableView<Pelaaja> pelaajatTable;

    @FXML
    private Label pelaajiaLkmLabel;

    @FXML
    private Button lisaaPelaajaButton;

    private ObservableList<Pelaaja> pelaajat = FXCollections.observableArrayList();

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

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

        // Tämä ei toimi!
        // setCellValueFactory vaatii ObservableValue-arvon, mutta ikä on int-kokonaisluku
        // ikaColumn.setCellValueFactory(cellData -> LocalDate.now().getYear() - cellData.getValue().getSyntymavuosi());

        lisaaPelaajaButton.setOnAction(event -> {
            Pelaaja uusiPelaaja = new Pelaaja();
            uusiPelaaja.setNimi("Uusi Pelaaja");
            uusiPelaaja.setSyntymavuosi(2000);
            pelaajat.add(uusiPelaaja);
        });

        pelaajatTable.setItems(pelaajat);
    }
}

Tässä esimerkissä näemme kaksi ongelmaa:

  • Uuden pelaajan lisääminen ei päivitä pelaajien lukumäärää
  • Pelaajan syntymävuoden muokkaaminen ei päivitä pelaajan ikää

Observable-arvon muuttaminen toiseksi arvoksi

Kaikki ObservableValue-arvot, kuten StringProperty, IntegerProperty, FloatProperty, jne. sisältävät map()-apumetodin (ks. JavaDoc), jonka avulla arvolle voi suorittaa laskutoimituksia. Esimerkiksi pelaajan ikä saadaan laskettua syntymävuodesta seuraavasti:

ObservableValue<Number> ika = pelaaja.syntymavuosiProperty().map(vuosi -> LocalDate.now().getYear() - vuosi.intValue());

Tällöin ika-muuttujan sisältämä arvo lasketaan syntymävuodesta kaavalla LocalDate.now().getYear() - vuosi.intValue() aina, kun pelaajan syntymävuosi muuttuu. Koska ObservableValue on havaittava arvo, voimme käyttää sitä setCellValueFactory-metodissa:

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

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

    // HIGHLIGHT_GREEN_BEGIN
    ikaColumn.setCellValueFactory(cellData -> 
        cellData.getValue().syntymavuosiProperty().map(
            syntymavuosi -> LocalDate.now().getYear() - syntymavuosi.intValue()));
    // HIGHLIGHT_GREEN_END

    lisaaPelaajaButton.setOnAction(event -> {
        Pelaaja uusiPelaaja = new Pelaaja();
        uusiPelaaja.setNimi("Uusi Pelaaja");
        uusiPelaaja.setSyntymavuosi(2000);
        pelaajat.add(uusiPelaaja);
    });

    pelaajatTable.setItems(pelaajat);
}

Funktion muuttaminen Observable-arvoksi

Pelaajien lukumäärä saadaan kutsumalla pelaajat.size(). size()-metodi ei kuitenkaan palauta havaittavaa arvoa, eikä pelaajat-lista sisällä yllä mainittua map()-metodia. Voimme kuitenkin muuntaa minkä tahansa funktion havaittavaksi käyttämällä Bindings-luokan (ks. JavaDoc) createXBinding-apumetodeja. Tässä X tarkoittaa havaittavan arvon tyyppiä, eli esimerkiksi Integer, Long, String tai Object. Koska size() on kokonaisluku, käytämme Bindings.createIntegerBinding()-metodia (ks. JavaDoc).

IntegerBinding pelaajienLkm = Bindings.createIntegerBinding(() -> pelaajat.size(), pelaajat);

Bindings.createIntegerBinding() ottaa vähintään kaksi parametria: lambdalausekkeen, josta havaittava arvo lasketaan ja yhden tai useamman Observable-arvon, jonka muuttuessa havaittava arvo lasketaan uudestaan. Tässä tapauksessa ensimmäinen parametri kertoo, että pelaajienLkm-arvo lasketaan aina lausekkeella pelaajat.size(). Toinen parametri pelaajat kertoo, että arvo on päivitettävä aina, kun pelaajat-listan sisältö muuttuu.

Bindings.createXBinding-metodi palauttaa Binding-tyyppisen havaittavan arvon, jonka voi käyttää samalla tavalla kuin muut Observable-arvot. Tässä tapauksessa voimme sitoa pelaajiaLkmLabel-kentän tekstin textProperty()-arvoon:

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

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

    ikaColumn.setCellValueFactory(cellData -> cellData.getValue().syntymavuosiProperty().map(syntymavuosi -> LocalDate.now().getYear() - syntymavuosi.intValue()));

    lisaaPelaajaButton.setOnAction(event -> {
        Pelaaja uusiPelaaja = new Pelaaja();
        uusiPelaaja.setNimi("Uusi Pelaaja");
        uusiPelaaja.setSyntymavuosi(2000);
        pelaajat.add(uusiPelaaja);
    });

    // HIGHLIGHT_GREEN_BEGIN
    IntegerBinding pelaajienLkm = Bindings.createIntegerBinding(() -> pelaajat.size(), pelaajat);
    pelaajiaLkmLabel.textProperty().bind(pelaajienLkm.asString());
    // HIGHLIGHT_GREEN_END

    pelaajatTable.setItems(pelaajat);
}

Property-tyypin bind()-metodilla voimme sitoa arvon toiseen havaittavaan arvoon. Tässä tapauksessa pelaajiaLkmLabel-kentän teksti sidotaan pelaajien lukumäärään. Tällöin, jos pelaajat-lista muuttuu, niin

  • pelaajienLkm-arvo havaitsee muutoksen ja laskee arvonsa uudestaan lausekkeella pelaajat.size()
  • pelaajienLkm.asString() havaitsee muutoksen pelaajienLkm-arvossa ja päivittää arvonsa kutsumalla pelaajienLkm.toString()
  • pelaajiaLkmLabel.textProperty() havaitsee muutoksen pelaajienLkm.asString()-arvossa ja päivittää oman sisältönsä vastaamaan uutta arvoa

Muutosten jälkeen pelaajien lukumäärä ja yksittäisen pelaajan ikä päivittyy automaattisesti: