Property Binding
In the previous lesson you saw that every JavaFX UI value — a slider position, a text field's string, a checkbox state — is wrapped in a Property object rather than stored as a plain field. That wrapper exists for exactly one reason: so that different parts of your application can bind to it and stay automatically synchronized without any manual update loop.
This lesson covers the two fundamental binding modes every JavaFX developer must know: one-way binding and bidirectional binding.
One-Way Binding: bind()
One-way binding declares that property A should always mirror the current value of property B. Whenever B changes, A is updated immediately and automatically. A cannot be set directly while it is bound — attempting to do so throws an exception at runtime.
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
public class OneWayDemo {
public static void main(String[] args) {
DoubleProperty source = new SimpleDoubleProperty(10.0);
DoubleProperty target = new SimpleDoubleProperty();
// target will always equal source
target.bind(source.asObject() != null ? source : source); // illustrative; simply:
target.bind(source);
System.out.println(target.get()); // 10.0
source.set(42.0);
System.out.println(target.get()); // 42.0 — updated automatically
// target.set(99.0); // RuntimeException: A bound value cannot be set.
}
}
A cleaner, real-world example: keep a progress bar synchronized with a loading percentage property on a view-model:
import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.scene.Scene;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.Slider;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class BindingApp extends Application {
@Override
public void start(Stage stage) {
Slider slider = new Slider(0, 1, 0.5);
ProgressBar bar = new ProgressBar();
// One-way: bar always shows whatever the slider reports
bar.progressProperty().bind(slider.valueProperty());
VBox root = new VBox(10, slider, bar);
root.setPadding(new javafx.geometry.Insets(20));
stage.setScene(new Scene(root, 300, 120));
stage.setTitle("One-Way Binding Demo");
stage.show();
}
}
Drag the slider and the progress bar tracks it — zero lines of event handling needed. The binding framework handles the wiring entirely.
Direction matters: target.bind(source) makes target depend on source. If you write it the other way around — source.bind(target) — the dependency reverses. Always say it aloud: "target follows source."
Unbinding
Call unbind() to release a property from its binding. After that the property can be set freely again:
bar.progressProperty().unbind();
bar.progressProperty().set(1.0); // now legal
Bidirectional Binding: bindBidirectional()
Sometimes two properties should always stay equal and either side can change. The classic example is a text field and a backing data model: the user types in the field and the model should update; the application sets the model and the field should update. This is bidirectional binding.
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class BiDirectionalDemo {
public static void main(String[] args) {
StringProperty modelName = new SimpleStringProperty("Alice");
StringProperty fieldText = new SimpleStringProperty();
modelName.bindBidirectional(fieldText);
// Model drives field
System.out.println(fieldText.get()); // "Alice"
// Field drives model
fieldText.set("Bob");
System.out.println(modelName.get()); // "Bob"
// Model drives field again
modelName.set("Charlie");
System.out.println(fieldText.get()); // "Charlie"
}
}
In a real UI you would connect a TextField to a property on your view-model:
TextField nameField = new TextField();
StringProperty viewModelName = new SimpleStringProperty("Alice");
// Either side may change; both always agree
nameField.textProperty().bindBidirectional(viewModelName);
Prefer bidirectional binding for form fields. It removes the need to write a ChangeListener on the field and a separate listener on the model. Both directions are covered in one line.
Unbinding Bidirectional Bindings
Use unbindBidirectional() — you must pass the same property object that was used when binding:
nameField.textProperty().unbindBidirectional(viewModelName);
// now both properties change independently
Memory leak risk: bidirectional bindings hold strong references to both properties. If you replace one of the objects (e.g., you load a new entity into the form) without calling unbindBidirectional() first, the old property is kept alive. Always unbind before rebinding to a new object.
One-Way vs Bidirectional — When to Use Which
- One-way (
bind) — use when a derived value should follow a source but the derived side must not be changed directly. Examples: a label that displays a slider value; a button that is disabled when a text field is empty; a computed total that reflects a quantity property.
- Bidirectional (
bindBidirectional) — use when two UI elements or a UI element and a model property must always stay in sync, and either side is a legitimate originator of change. Examples: a text field and a view-model property; a toggle button and a boolean setting.
Practical Pattern: A View-Model Form
Here is how both binding modes work together in a real MVVM-style form. The view-model owns the data; the view wires to it with bindings only — no event handlers are written in the view class:
import javafx.beans.property.*;
/** View-model: owns the data and business rules. */
public class PersonViewModel {
private final StringProperty firstName = new SimpleStringProperty("");
private final StringProperty lastName = new SimpleStringProperty("");
// Derived, read-only property: full name computed from the two source properties
private final StringProperty fullName = new SimpleStringProperty();
public PersonViewModel() {
// One-way computed binding (covered fully in Lesson 3)
fullName.bind(firstName.concat(" ").concat(lastName));
}
public StringProperty firstNameProperty() { return firstName; }
public StringProperty lastNameProperty() { return lastName; }
public StringProperty fullNameProperty() { return fullName; }
}
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class PersonFormApp extends Application {
@Override
public void start(Stage stage) {
PersonViewModel vm = new PersonViewModel();
TextField firstField = new TextField();
TextField lastField = new TextField();
Label fullLabel = new Label();
// Bidirectional: either the field or the model can initiate change
firstField.textProperty().bindBidirectional(vm.firstNameProperty());
lastField.textProperty().bindBidirectional(vm.lastNameProperty());
// One-way: fullLabel is read-only, always derived from the model
fullLabel.textProperty().bind(vm.fullNameProperty());
VBox root = new VBox(8,
new Label("First:"), firstField,
new Label("Last:"), lastField,
new Label("Full:"), fullLabel
);
root.setPadding(new javafx.geometry.Insets(16));
stage.setScene(new Scene(root, 280, 220));
stage.setTitle("MVVM Form");
stage.show();
}
}
Type in either text field and the full-name label updates live. Call vm.firstNameProperty().set("Alice") from any thread or button handler and both the field and the label update automatically. This is the real value of the JavaFX property system: declarative data flow, zero manual synchronisation code.
Summary
bind() creates a one-way dependency: the bound property always mirrors its source and cannot be set directly. bindBidirectional() links two properties so changes on either side propagate to the other. Always unbind before reconnecting to a new source, especially in bidirectional scenarios, to avoid memory leaks. In the next lesson you will move beyond simple same-type bindings and learn how to create computed and fluent bindings that transform, combine, and derive values across different property types.