Properties und Binding in JavaFXv.7.0

Properties und Binding sind zwei mächtige Sprach-Mechanismen in JavaFX, mit denen Beziehungen zwischen Variablen gestaltet werden können. Meist werden sie zur (gegenseitigen) Aktualisierung von Werten herangezogen. Die hier gezeigten Beispiele beziehen sich auf die JavaFX-Version 2.2.

Inhalt

  1. Properties
  2. Koppeln eines Wertes mit einer Komponente mittels Property und ChangeListener
  3. Binding
    1. Binding zwischen Werten
    2. Unidirectional Binding (einseitige Bindung) eines Wertes an eine oder mehrere Komponenten
    3. Bidirectional Binding (wechselseitige Bindung)

Properties

Properties ermöglichen es, Eigenschaften einer Klasse so zu fassen, dass sie an andere Daten-Objekte (z.B. Komponenten, Nodes, andere Eigenschaften, etc.) gekoppelt werden können. So kann etwa die Anzeige der Höhe eines Kontostandes auf einem Label, Textfeld o.ä. bei jeder Änderung seines Standes automatisch aktualisiert werden.

Betrachten wir als Beispiel eine Klasse Konto mit einer privaten Instanzvariablen stand. Die Anzeige der Höhe dieses Kontostandes soll zeitgleich zu jeder Änderung aktualisiert werden. Bislang wurde ein solcher Fall etwa durch ein Observer-Entwurfsmuster gelöst, ein Verfahren, das je nach Implementierung durchaus seine Grenzen haben konnte.

import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;

public class Konto {
    
    private DoubleProperty stand;

    public final double getStand() {
        if (stand != null)
            return stand.get();
        return 0;
    }

    public final void setStand(double hoehe) {
        this.standProperty().set(hoehe);
    }

    public final DoubleProperty standProperty() {
        if (stand == null) {
            stand = new SimpleDoubleProperty(0);
        }
        return stand;
    }
}

Im Beispiel fällt zunächst auf, dass nicht wie erwartet, ein primitiver double als Datentyp für den Kontostand gewählt wurde, sondern ein Objekt vom Typ DoubleProperty. Hierbei handelt es sich um ein den primitiven double-Typ kapselndes Objekt, das besonders die Benachrichtigungsfunktionalität einer Property bereitstellt. Die Klassen des Packages javafx.beans.property implementieren hierzu z.B. alle das Interface Observable und/oder ObservableValue, das einen Wert kapselt. JavaFX kennt solche Wrapper für die Datentypen int, long, float und double.

Die Klasse weist drei Methoden auf, von denen zwei die üblichen Getter- und Setter-Methoden sind, die gleichwohl insofern von der bislang bekannten Syntax abweichen, als sie den gekapselten primitiven Typ über die Methoden DoubleProperty.get() und DoubleProperty.set() ansprechen. Um eine eventuelle NullPointerException zu vermeiden, muss im Getter natürlich vorher die Existenz des kapselnden Objektes abgefragt werden.
Die dritte Methode stellt die eigentliche Besonderheit dar, da sie die Properties-Funktionalität gewährleistet. Sie gibt das Property-Objekt selbst zurück. Es muss darauf geachtet werden, dass der Bezeichner der Methode aus dem Namen der Variablen und einem nachfolgenden Property zusammengesetzt wird.
Auch hier wird zunächst auf die Existenz des Objektes geprüft und dies initial als SimpleDoubleProperty erzeugt, einer vollständigen Implementierung der abstrakten Klasse DoubleProperty.

Koppeln eines Wertes mit einer Komponente mittels Property und ChangeListener.

Einer der offensichtlichsten Vorteile von Properties besteht darin, dass primitive Datentypen mit einer Funktionalität versehen werden können, die sonst nur Referenztypen vorbehalten bleibt. So ist es in unserem Fall möglich, den Kontostand mit einem ChangeListener zu versehen, sodass seine Änderung etwa auf einem Label angezeigt werden kann.

import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;

public class BindingsBsp1 extends Application {

    @Override
    public void start(Stage primaryStage) {
        final Konto konto = new Konto();
        konto.setStand(0);
        Button incButt = new Button("+1");
        final Label label = new Label(new Double(konto.getStand()).toString());
        label.setMinSize(incButt.getMinWidth(), incButt.getMinHeight());
        label.setAlignment(Pos.CENTER);
        konto.standProperty().addListener(new ChangeListener<Object>() {
            @Override
            public void changed(ObservableValue<?> o, Object oldVal,
                    Object newVal) {
                label.setText(new Double(konto.getStand()).toString());
            }
        });

        incButt.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                konto.setStand(konto.getStand() + 1);
            }
        });

        GridPane grid = new GridPane();
        grid.setAlignment(Pos.CENTER);
        grid.setVgap(10);
        grid.add(label, 0, 0);
        grid.add(incButt, 0, 1);

        Scene scene = new Scene(grid, 150, 100);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

Im Beispiel wird ein einfaches, kleines JavaFX-Fenster mit einem Button und einem Label erzeugt. Das Label zeigt den Kontostand, der durch den Button inkrementiert werden kann. Dem Property-Objekt des Kontostandes wird ein ChangeListener hinzugefügt, der die Methode setText() des Labels anspricht und mit den geänderten Werten versieht.
Der Button macht nichts anderes, als den aktuellen Kontostand selbst abzufragen und zu inkrementieren. Es wird deutlich, dass das Property-Objekt des Kontostandes selbst für die Aktualisierung der Labeldarstellung verantwortlich zeichnet.

Binding

Bindings umfassen einen oder mehrere Werte (dependencies), deren Zustand überwacht und bei Änderungen automatisch angepasst wird.

Binding zwischen Werten.

Erweitern wir obiges Eingangs-Beispiel und stellen uns zwei Konten vor, deren Summe bei Änderung eines Kontostandes angepasst wird.

import javafx.beans.binding.NumberBinding;    

public class BindingsBsp2 {

    public static void main(String[] args) {
        Konto konto1 = new Konto();
        konto1.setStand(100);
        Konto konto2 = new Konto();
        konto2.setStand(50);
        NumberBinding sum = konto1.standProperty().add(konto2.standProperty());
        System.out.println(sum.getValue());        // 150.0
        konto2.setStand(20);
        System.out.println(sum.getValue());        // 120.0
    }
}

Nach der Bildung zweier Konto-Objekte werden hier zunächst deren Stände auf Ausgangswerte gesetzt. In der folgenden Zeile werden die Werte beider Properties durch add() addiert und in einem Objekt vom Typ NumberBinding gespeichert, auf dessen gekapselten Wert durch getValue() zugegriffen werden kann. Die beiden Methodenaufrufe standProperty() greifen bei der Berechnung automatisch auf die jeweils gekapselten Werte zu.
Die Änderung des zweiten Kontostandes und die anschließende Abfrage beweisen die Aktualisierung der Summe durch das Binding.

Unidirectional Binding (einseitige Bindung) eines Wertes an eine oder mehrere Komponenten

In JavaFX besitzen eine ganze Reihe an Klassen bereits eingebaute Properties-Funktionalität. So auch alle Komponenten, die von javafx.scene.Node abgeleitet sind. Zu ihnen gehören alle Knoten-Elemente, insbesondere auch die Controls, die Interaktionen mit dem User entgegennehmen können.
Das Beispiel zeigt ein einfaches Fenster, in dem der Stand eines Kontos durch zwei Buttons erhöht, bzw. erniedrigt werden kann. Der jeweilige Stand wird durch zwei Anzeigeelemente, eine ProgressBar und einen ProgressIndicator angezeigt. Um das Beispiel so einfach wie möglich zu halten, werden die Kontostand-Werte im Standard-Anzeigebereich der Controls zwischen 0 und 1 gehalten und eine Initialisierung von 0.3 vorgenommen. Die Buttons führen Änderungen von jeweils 0.01 Punkten durch.

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;

public class BindingsBsp3 extends Application {
    
    private static final double INITIAL_STAND = 0.3;

    @Override
    public void start(Stage primaryStage) {

        final Konto konto = new Konto();
        konto.setStand(INITIAL_STAND);
        
        Button incButt = new Button("+0.01");
        incButt.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                konto.setStand(konto.getStand() + .01);
            }
        });
        
        Button decButt = new Button("-0.01");
        decButt.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                konto.setStand(konto.getStand() - .01);
            }
        });

        ProgressBar pb = new ProgressBar(INITIAL_STAND);
        pb.progressProperty().bind(konto.standProperty());
        ProgressIndicator pi = new ProgressIndicator(INITIAL_STAND);
        pi.progressProperty().bind(konto.standProperty());

        GridPane grid = new GridPane();
        grid.add(incButt, 0, 0);
        grid.add(decButt, 1, 0);
        grid.add(pb, 0, 1);
        grid.add(pi, 1, 1);
        grid.setHgap(10);
        grid.setVgap(10);
        grid.setPadding(new Insets(10));
        Scene scene = new Scene(grid);
        primaryStage.setScene(scene);
        primaryStage.setTitle("Binding-Beispiel");
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

In den anonymen EventHandler-Klassen der Buttons wird der Wert des Kontostandes erhöht und erniedrigt, indem der jeweilige Stand abgefragt und neu gesetzt wird. Das Binding an die Anzeige erfolgt so, dass durch die Methode bind() die Property des Kontostandes an die eingebaute progressProperty von ProgressBar und ProgressIndicator gebunden wird. Da es sich bei den Fortschrittswerten der Kontrollelemente um numerische Werte handelt, gelingt dies natürlich wie im Falle des Kontostandes nur mit der Bindung an einen ebenfalls numerischen Wert. Welche Elemente über welche Properties verfügen, kann der API-Dokumentation im Abschnitt Property Summary der jeweiligen Klasse/des jeweiligen Interfaces entnommen werden.

Bidirectional Binding (wechselseitige Bindung)

Ein Binding kann auch dergestalt erfolgen, dass sich zwei Elemente gegenseitig aktualisieren. Dies soll an einem Beispiel mit einem Textfield und einem Slider demonstriert werden.

import java.text.NumberFormat;

import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Slider;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;

public class BindingsBsp5 extends Application {

    private static final double INITIAL_STAND = 0.3;

    @Override
    public void start(Stage primaryStage) {

        Slider slider = new Slider();
        slider.setValue(INITIAL_STAND);
        TextField field = new TextField();
        field.setText(new Double(INITIAL_STAND).toString());

        field.textProperty().bindBidirectional(slider.valueProperty(), NumberFormat.getNumberInstance());

        GridPane grid = new GridPane();
        grid.add(slider, 0, 0);
        grid.add(field, 0, 1);
        grid.setHgap(10);
        grid.setVgap(10);
        grid.setPadding(new Insets(10));
        Scene scene = new Scene(grid);
        primaryStage.setScene(scene);
        primaryStage.setTitle("Binding-Beispiel");
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

Das Beispiel zeigt ein einfaches Fenster lediglich mit einem Textfield und einem Slider. Der Quelltext entspricht weitgehend dem vorhergehenden Beispiel. Lediglich auf die Einbindung des Kontos wurde verzichtet. Die entscheidende Codezeile ist

field.textProperty().bindBidirectional(slider.valueProperty(), NumberFormat.getNumberInstance());

Hier wird der Wert des Sliders bidirektional an die textProperty des Textfeldes gebunden. Die Methode nimmt zwei Parameter entgegen: natürlich die valueProperty des Sliders und zusätzlich ein Objekt vom Typ java.text.NumberFormat. Es sorgt dafür, dass die Konvertierung zwischen der String-Repräsentanz des Textfeldes und dem double-Wert des Sliderwertes gewährleistet ist. Zur Verwendung in unterschiedlichen Anwendungsfällen ist die Methode mehrfach überladen.