Skip to main content
Entwickler Themen
Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage

Models und Bindings

Bindings verbinden JavaFX-Properties mit Model-Attributen oder mit einem berechneten Controller-Ergebnis. Die Deklarationen stehen in einer BindingList unter fx:define; dadurch werden sie keine Scene-Graph-Kinder.

Attribute und Property-Typen

Ein Model-Attribut ist eine JavaBeans-Property mit Getter und Setter, die nicht auf Object deklariert ist. Ein Getter allein genügt nicht.

Deklarierter Typ Runtime-Property
String StringProperty
int, Integer IntegerProperty; null wird 0
long, Long LongProperty; null wird 0
float, Float, double, Double Passende numerische Property; null wird 0
boolean, Boolean BooleanProperty; null wird false
Sonstige Typen ObjectProperty, gegebenenfalls mit null

Primitive, Enums, Arrays, String, Boolean, Number, Collection, Map, Date, Calendar und java.time-Typen bleiben einfache Werte. Ein anderer deklarierter Typ mit eigenen les- und schreibbaren Attributen ist komplex; dafür hält die Runtime ein Kind-ViewModel. Person.home verweist so auf eine Address, Address.note auf eine Note. Collections bleiben Listen und sind keine einzelnen Kind-ViewModels.

Ein Name wird zuerst unter den gespeicherten Attributen gesucht. latestMessage bezeichnet die eingebaute Nachrichten-Property. Zusätzliche öffentliche nameProperty()-Methoden einer Unterklasse können weitere Properties bereitstellen. storedProperty(name) ist der geschützte Zugriff auf ein wirklich gespeichertes Model-Attribut; er ruft keinen reflektierenden Accessor auf und scheitert bei unbekannten Namen.

DirectBinding

<fx:define>
    <BindingList>
        <DirectBinding control="nameField" property="text"
                       path="name" direction="both"/>
    </BindingList>
</fx:define>

Die entsprechenden Klassen müssen in FXML importiert sein. control bezeichnet eine fx:id, property="text" eine öffentliche textProperty() mit Rückgabetyp Property. path wählt das Model-Attribut. converter ist optional und benennt eine Converter-Klasse. Die Default-Property einer BindingList heißt elements; erlaubt sind DirectBinding, ComputedBinding und Repeat. Unbekannte Kinder führen zu Not a binding element:.

Richtung Verhalten
both Initialer Model-Wert erscheint im Control; spätere Änderungen laufen in beide Richtungen
view Initialer und späterer Property-Wert gehen zum Control; Control-Änderungen schreiben nicht zurück
model Initialer Wert erscheint im Control; danach schreiben nur Control-Änderungen ins Model

Die Rückschreibung erfolgt bei jeder Property-Änderung, bei Text also auch während des Tippens. Ein separates Speichern gehört nicht zum Binding. Die Richtung ist Pflicht und hat keinen Standardwert. Ein ungültiger String führt zu Not a direction:; eine ausgelassene Richtung kann derzeit beim Anwenden eine NullPointerException verursachen.

Für eine bereits vorhandene ObservableList gilt ein Sonderfall: Das Control erhält genau dieselbe Liste. Richtung und Converter werden dabei nicht ausgewertet. Eine view-Deklaration macht eine geteilte Liste also nicht unveränderlich.

Pfade

home.street durchläuft das komplexe Attribut home und bindet dessen street. Frühere Segmente müssen komplexe Attribute sein. Ist ein Zwischenobjekt null, bleibt dieses Binding unangewendet und beide Seiten behalten ihren Wert; der Ladevorgang kann trotzdem erfolgreich sein.

Die Auflösung geschieht einmal. Nach dem Austausch der home-Property bleibt ein vorhandenes home.street-Binding am früheren Kind. Ein einfacher Pfad home kann dagegen spätere Änderungen des letzten komplexen Attributs weiterreichen. Dabei erhält etwa eine Component-Model-Property das zugrunde liegende Model-Objekt, nicht dessen ViewModel. Ihre eigenen Bindings werden dennoch nicht erneut aufgebaut; für ein anderes Model eine neue Component laden.

Pfade adressieren keine Listenindizes. Repeat und Table verwenden eigene Collection-Deklarationen. Ein direkter Bean-Setter löst keine dieser Benachrichtigungen aus.

Converter

Ein Converter implementiert de.neitzel.fx.view.Converter und wird über ApplicationContext.getComponent erzeugt. Er kann dadurch selbst Dienste per Injection erhalten.

@jakarta.inject.Singleton
public class IntegerText implements de.neitzel.fx.view.Converter {
    public Object toControlValue(Object value) {
        return value == null ? "" : value.toString();
    }
    public Object toAttributeValue(Object value) {
        return Integer.valueOf((String) value);
    }
}
<DirectBinding control="ageField" property="text" path="age"
               direction="both" converter="example.IntegerText"/>

toControlValue läuft zum Control, auch für die Initialisierung einer model-Bindung. toAttributeValue wird nur für model und both benötigt; seine Default-Implementierung wirft UnsupportedOperationException. Komplexe Attribute und ObservableLists umgehen Converter.

Bei einer Conversion-Exception bleibt das Ziel auf dem bisherigen Wert. Ungültiger eingegebener Text bleibt sichtbar. Das besitzende Runtime-ViewModel speichert einen Fehler pro Attribut, mit dessen Namen als Quelle. Weitere Fehler aktualisieren ihn; eine erfolgreiche Umwandlung entfernt ihn. Bei leerer Exception-Nachricht lautet der Text Conversion failed. Ersatzobjekte außerhalb der ViewModel-Basisklasse besitzen diese Nachrichtenverwaltung nicht automatisch.

Im Beispiel injiziert AgeConverter einen AgeSuffix und verarbeitet years 36. StreetConverter verwendet eine injizierte StreetPolicy und lehnt leere Straßen ab. VisitedOnConverter verwendet ISO-Daten und schreibt für ein leeres Feld null.

ComputedBinding

<ComputedBinding control="greetingLabel" property="text" method="#greeting"/>

Die Methode liegt auf dem Controller, ist öffentlich und hat keine Parameter. Ein führendes # wird entfernt; auch der reine Methodenname ist zulässig. Das Ergebnis geht zum Ziel-Control, nicht ins Model.

public String greeting() {
    String name = nameField.getText();
    Object age = viewModel.property("age").getValue();
    return "Hello, " + name + " (" + age + ")";
}

Die Methode läuft einmal beim Anwenden. Danach beobachtet die Runtime passende öffentliche JavaFX-Property-Methoden der vorhandenen Controls sowie gespeicherte Attribute eines Runtime-ViewModels. Die Ziel-Property wird ausgenommen, um direkte Rekursion zu vermeiden. Andere Nodes, später hinzugefügte Controls und beliebige Ersatz-ViewModels werden nicht automatisch überwacht.

Das ist eine breite Aktualisierung bei Benachrichtigungen, keine Analyse der tatsächlich gelesenen Abhängigkeiten. Methoden sollen schnell und ohne Seiteneffekte sein. Fehlende oder werfende Methoden lassen Laden beziehungsweise Aktualisierung scheitern. onAction="#handler" bleibt davon unabhängig normales FXML-Ereignisverhalten.

Controller und ViewModel-Auswahl

fx:controller benennt eine vom Container bekannte Klasse. Sie ist dort ein Singleton. @FXML-Felder werden beim Laden gefüllt. Implementiert sie HasViewModel, erfolgt setViewModel(ViewModel) vor den Bindings, aber nur bei einer ausgewählten Instanz dieser Basisklasse.

Ohne Annotation verwendet die Runtime ein passendes registriertes generiertes Default oder sonst ViewModel. @ViewModelClass auf Controller beziehungsweise Component kann eine Unterklasse oder ein Ersatzobjekt mit benannten JavaFX-Properties wählen. Es muss einen passenden Ein-Argument-Konstruktor für das Model geben; bei mehreren passenden Parametertypen gewinnt der spezifischste. Das ViewModel wird nicht durch den Injection-Container konstruiert.

Die erste View legt die konkrete Instanz für ein Model fest. Weitere Anforderungen werden nur akzeptiert, wenn die bestehende Instanz den angeforderten Typ erfüllt; kompatible Oberklassen beziehungsweise Interfaces sind möglich. Eine View ohne explizite Auswahl verwendet die vorhandene Instanz. Ein inkompatibler Wechsel scheitert. Generierte Defaults setzen zusätzliche Regeln für die erforderliche generierte Basisklasse; siehe Generierung.

Die Annotation einer Component betrifft deren Model, nicht das des Elternformulars. Binding-Reihenfolge ist relevant, wenn ein früherer Pfad bereits ein generisches Kind-ViewModel aufgebaut hat, bevor eine spätere Component eine inkompatible Klasse anfordert.