JavaFX Binding, Events & Styling

JavaFX Properties

18 min Lesson 1 of 12

JavaFX Properties

If you have built any Swing or raw-AWT application you know the frustration: you update a field in your model, then manually call label.setText(...), repaint, and hope you did not miss any path. JavaFX solves this at the language level with a property system — a set of observable wrappers around ordinary Java values. When the value changes, every interested party is notified automatically. This lesson covers what properties are, how they work under the hood, and the idiomatic way to write a model class that uses them.

What Is an Observable Property?

A JavaFX property is an object that wraps a value and provides three capabilities:

  1. Read/write access to the underlying value (get() / set()).
  2. Change notification — listeners registered on the property are called whenever the value changes.
  3. Binding support — the property can be linked to another property so they stay in sync without manual synchronisation code.

JavaFX ships concrete implementations for every primitive type and for Object:

  • StringProperty / SimpleStringProperty
  • IntegerProperty / SimpleIntegerProperty
  • DoubleProperty / SimpleDoubleProperty
  • BooleanProperty / SimpleBooleanProperty
  • ObjectProperty<T> / SimpleObjectProperty<T>

The naming pattern is consistent: the interface (StringProperty) describes the contract; the implementation (SimpleStringProperty) is what you instantiate in your own model classes.

The JavaFX Property Hierarchy

The full type hierarchy is worth understanding so you can read JavaFX API documentation without confusion:

  • Observable — base interface; can fire invalidation events.
  • ObservableValue<T> — adds getValue() and change listeners.
  • ReadOnlyProperty<T> — adds getBean() and getName() (metadata about the owning object and field name).
  • Property<T> — adds setValue() and binding methods.
  • WritableValue<T> — adds setValue() independently.

Specialised interfaces like StringProperty extend both Property<String> and WritableStringValue, adding type-specific get()/set(String) that avoid boxing.

Invalidation vs. Change listeners: JavaFX distinguishes between an invalidation (the value might have changed — used for lazy evaluation) and a change (the value has changed and you receive old/new). For beginners, change listeners are the obvious choice; invalidation listeners are a performance optimisation you reach for when the new value is expensive to compute and may never actually be read.

Writing a Model Class with Properties

The JavaFX convention for a model class (often called a JavaFX Bean) mirrors the Java Beans convention but adds a third accessor — the property accessor — which returns the property object itself so callers can bind to it.

import javafx.beans.property.IntegerProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; public class Product { // 1. Declare private property fields private final StringProperty name = new SimpleStringProperty(this, "name", ""); private final IntegerProperty stock = new SimpleIntegerProperty(this, "stock", 0); // 2. Standard getter — returns the primitive/String value public String getName() { return name.get(); } public int getStock() { return stock.get(); } // 3. Standard setter — writes through the property public void setName(String value) { name.set(value); } public void setStock(int value) { stock.set(value); } // 4. Property accessor — exposes the property for binding public StringProperty nameProperty() { return name; } public IntegerProperty stockProperty() { return stock; } }

The three-argument constructor (new SimpleStringProperty(bean, name, initialValue)) passes metadata: this is the owning object and "name" is the field name. This metadata is optional but valuable when debugging — it appears in property toString() output and in Scene Builder diagnostics.

Always follow the three-accessor pattern. Every model field exposed through the UI should have a plain getter, a plain setter, and a xxxProperty() accessor. Controllers and bindings use the property accessor; service and persistence code uses the plain getter/setter. Mixing the two audiences through a single accessor leads to tangled, hard-to-test code.

Attaching Change Listeners

Once you have a property object you can register a ChangeListener on it. The listener receives the observable itself, the old value, and the new value:

Product p = new Product(); p.setName("Laptop"); p.setStock(42); // Listen for stock changes p.stockProperty().addListener((observable, oldValue, newValue) -> { System.out.printf("Stock changed: %d -> %d%n", oldValue.intValue(), newValue.intValue()); if (newValue.intValue() < 5) { System.out.println("WARNING: low stock!"); } }); p.setStock(3); // prints: Stock changed: 42 -> 3 / WARNING: low stock! p.setStock(100); // prints: Stock changed: 3 -> 100

Notice the lambda signature for IntegerProperty: even though the underlying value is int, the listener receives Number (the boxed super-type). Call .intValue() to unbox.

Read-Only Properties

Sometimes you want a property that is publicly readable but only writable from inside the class — for example, a computed value like a total price. JavaFX provides ReadOnlyIntegerWrapper (and the equivalent for other types) for this pattern:

import javafx.beans.property.ReadOnlyIntegerProperty; import javafx.beans.property.ReadOnlyIntegerWrapper; public class Cart { // Internal writable wrapper — only this class can write private final ReadOnlyIntegerWrapper itemCount = new ReadOnlyIntegerWrapper(this, "itemCount", 0); // Public read-only view exposed to the outside world public ReadOnlyIntegerProperty itemCountProperty() { return itemCount.getReadOnlyProperty(); } public int getItemCount() { return itemCount.get(); } public void addItem() { itemCount.set(itemCount.get() + 1); // write from inside the class } }

The wrapper holds the real IntegerProperty. getReadOnlyProperty() returns a view that shares the same value but exposes no set() method. Callers can bind to it and listen for changes but cannot modify it.

Do not return the wrapper itself. If your xxxProperty() accessor returns the ReadOnlyIntegerWrapper instead of calling getReadOnlyProperty(), external code can cast it back to a writable IntegerProperty and break your encapsulation. Always return the read-only view from the public accessor.

Properties on the JavaFX UI Thread

All JavaFX scene-graph mutations — including writes to properties that are bound to UI nodes — must happen on the JavaFX Application Thread. If you update a property from a background thread (say, after a network call) you must marshal the update:

// Wrong — updating a model property from a background thread new Thread(() -> { String result = fetchFromNetwork(); product.setName(result); // may cause rendering inconsistencies }).start(); // Correct — marshal back to the FX thread new Thread(() -> { String result = fetchFromNetwork(); javafx.application.Platform.runLater(() -> product.setName(result)); }).start();

Platform.runLater() queues the Runnable onto the JavaFX pulse queue. For heavier background work, the next lesson introduces Task and Service which handle this automatically.

Summary

JavaFX properties are observable wrappers that decouple the producer of a value from every consumer that cares about it. The three-accessor Bean pattern — plain getter, plain setter, and a xxxProperty() method — is the standard contract your model classes should follow. In the next lesson you will see how to connect two properties together with binding, eliminating the change-listener boilerplate entirely for the most common synchronisation needs.