Heaven acts with vitality and persistence.
In correspondence with this
The superior person keeps himself vital without ceasing.
—I Ching
In Chapters 1 and 2, we introduced you to the JavaFX 2.0 platform. You downloaded the JavaFX 2.0 SDK and the JavaFX plugin for Netbeans. You wrote and ran your first JavaFX 2.0 GUI programs. You learned the fundamental building blocks of JavaFX 2.0: the Stage
and Scene
classes, and the Node
s that go into the Scene
. And you have no doubt noticed the use of user-defined model classes to represent the application state and have that state communicated to the UI through properties and bindings.
In this chapter, we give you a guided tour of the JavaFX 2.0 properties and bindings framework. After recalling a little bit of history and presenting a motivating example that shows various ways that a JavaFX 2.0 Property
can be used, we cover key concepts of the framework: Observable
, ObservableValue
, WritableValue
, ReadOnlyProperty
, Property
, and Binding
. We show you the capabilities offered by these fundamental interfaces of the framework. We then show you how Property
objects are bound together, how Binding
objects are built out of properties and other bindings—using the factory methods in the Bindings
utility class, the fluent interface API, or going low-level by directly extending abstract classes that implement the Binding
interface—and how they are used to easily propagate changes in one part of a program to other parts of the program without too much coding. We finish this chapter by introducing the JavaFX Beans naming convention, an extension of the original JavaBeans naming convention that makes organizing your data into encapsulated components an orderly affair.
Because the JavaFX 2.0 properties and bindings framework is a nonvisual part of the JavaFX 2.0 platform, the example programs in this chapter are also nonvisual in nature. We deal with Boolean
, Integer
, Long
, Float
, Double
, String,
and Object
typed properties and bindings as these are the types in which the JavaFX 2.0 binding framework specializes. Your GUI building fun resumes in the next and further chapters.
The need for exposing attributes of Java components directly to client code, allowing them to observe and to manipulate such attributes and to take action when their values change, is recognized early in Java’s life. The JavaBeans framework in Java 1.1 provided support for properties through the now familiar getter and setter convention. It also supported the propagations of property changes through its PropertyChangeEvent
and PropertyChangeListener
mechanism. Although the JavaBeans framework is used in many Swing applications, its use is quite cumbersome and requires quite a bit of boilerplate code. Several higher-level data binding frameworks were created over the years with various levels of success. The heritage of the JavaBeans in the JavaFX 2.0 properties and bindings framework lies mainly in the JavaFX Beans getter, setter, and property getter naming convention when defining JavaFX 2.0 components. We talk about the JavaFX Beans getter, setter, and property getter naming convention later in this chapter, after we have covered the key concepts and interfaces of the JavaFX 2.0 properties and bindings framework.
Another strand of heritage of the JavaFX 2.0 properties and bindings framework comes from the JavaFX Script language that was part of the JavaFX 1.x platform. Although the JavaFX Script language was deprecated in the JavaFX 2.0 platform in favor of a Java-based API, one of the goals of the transition was to preserve most of the powers of the JavaFX Script’s bind
keyword, whose expressive power has delighted many JavaFX enthusiasts. As an example, JavaFX Script supports the binding to complex expressions:
var a = 1;
var b = 10;
var m = 4;
def c = bind for (x in [a..b] where x < m) { x * x };
This code will automatically recalculate the value of c
whenever the values of a
, b
, or m
are changed.
Although the JavaFX 2.0 properties and bindings framework does not support all of the binding constructs of JavaFX Script, it supports the binding of many useful expressions. We talk more about constructing compound binding expressions after we cover the key concepts and interfaces of the framework.
Let’s start with an example that shows off the capabilities of the Property
interface through the use of a couple of instances of the SimpleIntegerProperty
class.
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
public class MotivatingExample {
private static IntegerProperty intProperty;
public static void main(String[] args) {
createProperty();
addAndRemoveInvalidationListener();
addAndRemoveChangeListener();
bindAndUnbindOnePropertyToAnother();
}
private static void createProperty() {
System.out.println();
intProperty = new SimpleIntegerProperty(1024);
System.out.println("intProperty = " + intProperty);
System.out.println("intProperty.get() = " + intProperty.get());
System.out.println("intProperty.getValue() = " + intProperty.getValue().intValue());
}
private static void addAndRemoveInvalidationListener() {
System.out.println();
final InvalidationListener invalidationListener =
new InvalidationListener() {
@Override
public void invalidated(Observable observable) {
System.out.println("The observable has been invalidated: " +
observable + ".");
}
};
intProperty.addListener(invalidationListener);
System.out.println("Added invalidation listener.");
System.out.println("Calling intProperty.set(2048).");
intProperty.set(2048);
System.out.println("Calling intProperty.setValue(3072).");
intProperty.setValue(Integer.valueOf(3072));
intProperty.removeListener(invalidationListener);
System.out.println("Removed invalidation listener.");
System.out.println("Calling intProperty.set(4096).");
intProperty.set(4096);
}
private static void addAndRemoveChangeListener() {
System.out.println();
final ChangeListener changeListener = new ChangeListener() {
@Override
public void changed(ObservableValue observableValue, Object oldValue, Object
newValue) {
System.out.println("The observableValue has changed: oldValue = " +
oldValue + ", newValue = " + newValue);
}
};
intProperty.addListener(changeListener);
System.out.println("Added change listener.");
System.out.println("Calling intProperty.set(5120).");
intProperty.set(5120);
intProperty.removeListener(changeListener);
System.out.println("Removed change listener.");
System.out.println("Calling intProperty.set(6144).");
intProperty.set(6144);
}
private static void bindAndUnbindOnePropertyToAnother() {
System.out.println();
IntegerProperty otherProperty = new SimpleIntegerProperty(0);
System.out.println("otherProperty.get() = " + otherProperty.get());
System.out.println("Binding otherProperty to intProperty.");
otherProperty.bind(intProperty);
System.out.println("otherProperty.get() = " + otherProperty.get());
System.out.println("Calling intProperty.set(7168).");
intProperty.set(7168);
System.out.println("otherProperty.get() = " + otherProperty.get());
System.out.println("Unbinding otherProperty from intProperty.");
otherProperty.unbind();
System.out.println("otherProperty.get() = " + otherProperty.get());
System.out.println("Calling intProperty.set(8192).");
intProperty.set(8192);
System.out.println("otherProperty.get() = " + otherProperty.get());
}
}
In this example we created a SimpleIntegerProperty
object called intProperty
with an initial value of 1024
. We then updated its value through a series of different integers while we added and then removed an InvalidationListener
, added and then removed a ChangeListener
, and finally created another SimpleIntegerProperty
named otherProperty
, bound it to and then unbound it from intProperty
. The sample program used a generous amount of println
calls to show what is happening inside the program.
When we run the program in Listing 3-1, the following output is printed to the console:
intProperty = IntegerProperty [value: 1024]
intProperty.get() = 1024
intProperty.getValue() = 1024
Added invalidation listener.
Calling intProperty.set(2048).
The observable has been invalidated: IntegerProperty [value: 2048].
Calling intProperty.setValue(3072).
The observable has been invalidated: IntegerProperty [value: 3072].
Removed invalidation listener.
Calling intProperty.set(4096).
Added change listener.
Calling intProperty.set(5120).
The observableValue has changed: oldValue = 4096, newValue = 5120
Removed change listener.
Calling intProperty.set(6144).
otherProperty.get() = 0
Binding otherProperty to intProperty.
otherProperty.get() = 6144
Calling intProperty.set(7168).
otherProperty.get() = 7168
Unbinding otherProperty from intProperty.
otherProperty.get() = 7168
Calling intProperty.set(8192).
otherProperty.get() = 7168
By correlating the output lines with the program source code (or by stepping through the code in the debugger of your favorite IDE), we can draw the following conclusions.
SimpleIntegerProperty
object such as intProperty
and otherProperty
holds an int
value. The value can be manipulated with the get()
, set()
, getValue(),
and setValue()
methods. The get()
and set()
methods perform their operation with the primitive int
type. The getValue()
and setValue()
methods use the Integer
wrapper type.InvalidationListener
objects to and from intProperty
.ChangeListener
objects to and from intProperty
.Property
object such as otherProperty
can bind itself to intProperty
. When that happens, otherProperty
receives the value of intProperty
.intProperty
, whatever object that is attached to it is notified. The notification is not sent if the object is removed.InvalidationListener
objects are only informed of which object is sending out the notification and that object is only known as an Observable
.ChangeListener
objects are informed on two more pieces of information—the oldValue
and the newValue
—in addition to the object sending the notification. The sending object is known as an ObservableValue
.otherProperty
, we cannot tell from the output when or how it is notified of the change of value in intProperty
. However, we can infer that it must have known of the change because when we asked otherProperty
for its value we get back the latest value of intProperty
.Note Even though this motivating example uses an Integer property, similar examples can be made to use properties based on the Boolean, Long, Float, Double, String, and Object types. In the JavaFX 2.0 properties and bindings framework, when interfaces are extended or implemented for concrete types, they are always done for the Boolean, Integer, Long, Float, Double, String, and Object types.
This example brings to our attention some of the key interfaces and concepts of the JavaFX 2.0 properties and bindings framework: including the Observable
and the associated InvalidationListener
interfaces, the ObservableValue
and the associated ChangeListener
interfaces, the get()
, set()
, getValue(),
and setValue()
methods that allow us to manipulate the values of a SimpleIntegerProperty
object directly, and the bind()
method that allows us to relinquish direct manipulation of the value of a SimpleIntegerProperty
object by subordinating it to another SimpleIntegerProperty
object.
In the next section we show you these and some other key interfaces and concepts of the JavaFX 2.0 properties and bindings framework in more detail.
Figure 3-1 is an UML diagram showing the key interfaces of the JavaFX 2.0 properties and bindings framework. It includes some interfaces that you have seen in the last section, and some that you haven’t seen.
Note We did not show you the fully qualified names of the interfaces in the UML diagram. These interfaces are spread out in four packages: javafx.beans
, javafx.beans.binding
, javafx.beans.property
, and javafx.beans.value
. You can easily figure out which interface belongs to which package by examining the JavaFX API documentation or by the “find class” feature of your favorite IDE.
At the root of the hierarchy is the Observable
interface. You can register InvalidationListener
objects to an Observable
object to receive invalidation events. You have already seen invalidation events fired from one kind of Observable object
, the SimpleIntegerProperty
object intProperty
in the motivating example in the last section. It is fired when the set()
or setValue()
methods are called to change the underlying value from one int
to a different int
.
Note An invalidation event is fired only once by any of the implementations of the Property
interface in the JavaFX 2.0 properties and bindings framework if you call the setter with the same value several times in a row.
Another place where invalidation events are fired is from Binding
objects. You haven’t seen an example of a Binding
object yet. But there are plenty of Binding
objects in the second half of this chapter. For now we just note that Binding
objects may become invalid, for example, when its invalidate()
method is called or, as we show later in this chapter, when one of its dependencies fires an invalidation event.
Note An invalidation event is fired only once by any of the implementations of the Binding
interface in the JavaFX 2.0 properties and bindings framework if it becomes invalid several times in a row.
Next up in the hierarchy is the ObservableValue
interface. It’s simply an Observable
that has a value. Its getValue()
method returns its value. The getValue()
method that we called on the SimpleIntegerProperty
objects in the motivating example can be considered to have come from this interface. You can register ChangeListener
objects to an ObservableValue
object to receive change events.
You have seen change events being fired in the motivating example in the last section. When the change event fires, the ChangeListener
receives two more pieces of information: the old value and the new value of the ObservableValue
object.
Note A change event is fired only once by any of the implementations of the ObservableValue
interface in the JavaFX 2.0 properties and bindings framework if you call the setter with the same value several times in a row.
The distinction between an invalidation event and a change event is made so that the JavaFX 2.0 properties and bindings framework may support lazy evaluations. We show an example of this by looking at three lines of code from the motivating example:
otherProperty.bind(intProperty);
intProperty.set(7168);
System.out.println("otherProperty.get() = " + otherProperty.get());
When intProperty.set(7168)
is called, it fires an invalidation event to otherProperty
. Upon receiving this invalidation event, otherProperty
simply makes a note of the fact that its value is no longer valid. It does not immediately perform a recalculation of its value by querying intProperty
for its value. The recalculation is performed later when otherProperty.get()
is called. Imagine if instead of calling intProperty.set()
only once as in the above code we call intProperty.set()
multiple times; otherProperty
still recalculates its value only once.
Note The ObservableValue
interface is not the only direct subinterface of Observable
. There are two other direct subinterfaces of Observable
that live in the javafx.collections
package: ObservableList
and ObservableMap
with corresponding ListChangeListener
and MapChangeListener
as callback mechanisms. These JavaFX 2.0 observable collections are covered in Chapter 6, “Collections and Concurrency.”
This may be the simplest subsection in the entire chapter, for the WritableValue
interface is truly as simple as it looks. Its purpose is to inject the getValue()
and setValue()
methods into implementations of this interface. All implementation classes of WritableValue
in the JavaFX 2.0 properties and bindings framework also implement ObservableValue
, therefore you can make an argument that the value of WritableValue
is only to provide the setValue()
method.
You have seen the setValue()
method at work in the motivating example.
The ReadOnlyProperty
interface injects two methods into its implementations. The getBean()
method should return the Object
that contains the ReadOnlyRroperty
or null if it is not contained in an Object
. The getName()
method should return the name of the ReadOnlyProperty
or the empty string if the ReadOnlyProperty
does not have a name.
The containing object and the name provide contextual information about a ReadOnlyProperty
. The contextual information of a property does not play any direct role in the propagation of invalidation events or the recalculation of values. However, if provided, it will be taken into account in some peripheral calculations.
In our motivating example, the intProperty
is constructed without any contextual information. Had we used the full constructor to supply it a name:
intProperty = new SimpleIntegerProperty(null, "intProperty", 1024);
the output would have contained the property name:
intProperty = IntegerProperty [name: intProperty, value: 1024]
Now we come to the bottom of our key interfaces hierarchy. The Property
interface has as its superinterfaces all four interfaces we have examined thus far: Observable
, ObservableValue
, ReadOnlyProperty,
and WritableValue
. Therefore it inherits all the methods from these interfaces. It also provides five methods of its own:
void bind(ObservableValue<? extends T> observableValue);
void unbind();
boolean isBound();
void bindBidirectional(Property<T> tProperty);
void unbindBidirectional(Property<T> tProperty);
You have seen two of the methods at work in the motivating example in the last section: bind()
and unbind()
.
Calling bind()
creates a unidirectional binding or a dependency between the Property
object and the ObservableValue
argument. Once they enter this relationship, calling the set()
or setValue()
methods on the Property
object will cause a RuntimeException to be thrown. Calling the get()
or getValue()
methods on the Property
object will return the value of the ObservableValue
object. And, of course, changing the value of the ObservableValue
object will invalidate the Property
object. Calling unbind()
releases any existing unidirectional binding the Property
object may have. If a unidirectional binding is in effect, the isBound()
method returns true
; otherwise it returns false
.
Calling bindBidirectional()
creates a bidirectional binding between the Property
caller and the Property
argument. Notice that unlike the bind()
method, which takes an ObservableValue
argument, the bindBidirectional()
method takes a Property
argument. Only two Property
objects can be bound together bidirectionally. Once they enter this relationship, calling the set()
or setValue()
methods on either Property object will cause both objects’ values to be updated. Calling unbindBidirectional()
releases any existing bidirectional binding the caller and the argument may have. The program in Listing 3-2 shows a simple bidirectional binding at work.
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class BidirectionalBindingExample {
public static void main(String[] args) {
System.out.println("Constructing two StringProperty objects.");
StringProperty prop1 = new SimpleStringProperty("");
StringProperty prop2 = new SimpleStringProperty("");
System.out.println("Calling bindBidirectional.");
prop2.bindBidirectional(prop1);
System.out.println("prop1.isBound() = " + prop1.isBound());
System.out.println("prop2.isBound() = " + prop2.isBound());
System.out.println("Calling prop1.set("prop1 says: Hi!")");
prop1.set("prop1 says: Hi!");
System.out.println("prop2.get() returned:");
System.out.println(prop2.get());
System.out.println("Calling prop2.set(prop2.get() + "\nprop2 says: Bye!")");
prop2.set(prop2.get() + "
prop2 says: Bye!");
System.out.println("prop1.get() returned:");
System.out.println(prop1.get());
}
}
In this example we created two SimpleStringProperty
objects called prop1
and prop2
, created a bidirectional binding between them, and then called set()
and get()
on both properties.
When we run the program in Listing 3-2, the following output is printed to the console:
Constructing two StringProperty objects.
Calling bindBidirectional.
prop1.isBound() = false
prop2.isBound() = false
Calling prop1.set("prop1 says: Hi!")
prop2.get() returned:
prop1 says: Hi!
Calling prop2.set(prop2.get() + "
prop2 says: Bye!")
prop1.get() returned:
prop1 says: Hi!
prop2 says: Bye!
Caution Each Property
object may have at most one active unidirectional binding at a time. It may have as many bidirectional bindings as you want. The isBound()
method pertains only to unidirectional bindings. Calling bind()
a second time with a different ObservableValue argument while a unidirectional binding is already in effect will unbind the existing one and replace it with the new one.
The Binding
interface defines four methods that reveal the intentions of the interface. A Binding
object is an ObservableValue
whose validity can be queried with the isValid()
method and set with the invalidate()
method. It has a list of dependencies that can be obtained with the getDependencies()
method. And finally a dispose()
method signals that the binding will not be used anymore and resources used by it can be cleaned up.
From this brief description of the Binding
interface, we can infer that it represents a unidirectional binding with multiple dependencies. Each dependency, we imagine, could be an ObservableValue
to which the Binding
is registered to receive invalidation events. When the get()
or getValue()
method is called, if the binding is invalidated, its value is recalculated.
The JavaFX 2.0 properties and bindings framework does not provide any concrete classes that implement the Binding interface. However, it provides multiple ways to create your own Binding objects easily: you can extend the abstract base classes in the framework; you can use a set of static methods in the utility class Bindings
to create new bindings out of existing regular Java values (i.e., unobservable values), properties, and bindings; you can also use a set of methods that are provided in the various properties and bindings classes and form a fluent interface API to create new bindings. We go through the utility methods and the fluent interface API in the Creating Bindings section later in this chapter. For now, we show you the first example of a binding by extending the DoubleBinding
abstract class. The program in Listing 3-3 uses a binding to calculate the area of a rectangle.
import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
public class RectangleAreaExample {
public static void main(String[] args) {
System.out.println("Constructing x with initial value of 2.0.");
final DoubleProperty x = new SimpleDoubleProperty(null, "x", 2.0);
System.out.println("Constructing y with initial value of 3.0.");
final DoubleProperty y = new SimpleDoubleProperty(null, "y", 3.0);
System.out.println("Creating binding area with dependencies x and y.");
DoubleBinding area = new DoubleBinding() {
private double value;
{
super.bind(x, y);
}
@Override
protected double computeValue() {
System.out.println("computeValue() is called.");
return x.get() * y.get();
}
};
System.out.println("area.get() = " + area.get());
System.out.println("area.get() = " + area.get());
System.out.println("Setting x to 5");
x.set(5);
System.out.println("Setting y to 7");
y.set(7);
System.out.println("area.get() = " + area.get());
}
}
In the anonymous inner class, we called the protected bind()
method in the superclass DoubleBinding
, informing the superclass that we would like to listen to invalidation events from the DoubleProperty
objects x
and y
. We finally implemented the protected abstract computeValue()
method in the superclass DoubleBinding
to do the actual calculation when a recalculation is needed.
When we run the program in Listing 3-3, the following output is printed to the console:
Constructing x with initial value of 2.0.
Constructing y with initial value of 3.0.
Creating binding area with dependencies x and y.
computeValue() is called.
area.get() = 6.0
area.get() = 6.0
Setting x to 5
Setting y to 7
computeValue() is called.
area.get() = 35.0
Notice that computeValue()
is called only once when we call area.get()
twice in a row.
Caution The DoubleBinding
abstract class contains a default implementation of dispose()
that is empty and a default implementation of getDependencies()
that returns an empty list. To make this example a correct Binding
implementation we should override these two methods to behave correctly.
Now that you have a firm grasp of the key interfaces and concepts of the JavaFX 2.0 properties and bindings framework, we show you how these generic interfaces are specialized to type-specific interfaces and implemented in type-specific abstract and concrete classes.
We did not emphasize this fact in the last section because we believe its omission does not hurt the explanations there, but except for Observable
and InvalidationListener
, the rest of the interfaces are generic interfaces with a type parameter <T>
. In this section we examine how these generic interfaces are specialized to the specific types of interest: Boolean
, Integer
, Long
, Float
, Double
, String,
and Object
. We also examine some of the abstract and concrete classes of the framework and explore typical usage scenarios of each class.
Although the generic interfaces are not all specialized in exactly the same way, a common theme exists:
Boolean
type is specialized directly.Integer
, Long
, Float
, and Double
types are specialized through the Number
supertype.String
type is specialized through the Object
type.This theme exists in the type-specific specializations of all the key interfaces. As an example, we examine the subinterfaces of the ObservableValue<T>
interface:
ObservableBooleanValue
extends ObservableValue<Boolean>
, and it offers one additional method.
boolean get();
ObservableNumberValue
extends ObservableValue<Number>
, and it offers four additional methods.
int intValue();
long longValue();
float floatValue();
double doubleValue();
ObservableObjectValue<T>
extends ObservableValue<t>
, and it offers one additional method.
T get();
ObservableIntegerValue
, ObservableLongValue
, ObservableFloatValue,
and ObservableDoubleValue
extend ObservableNumberValue
and each offers an additional get()
method that returns the appropriate primitive type value.ObservableStringValue
extends ObservableObjectValue<String>
and inherits its get()
method that returns String
.Notice that the get()
method that we have been using in the examples is defined in the type-specific ObservableValue
subinterfaces. A similar examination reveals that the set()
method that we have been using in the examples is defined in the type-specific WritableValue
subinterfaces.
A practical consequence of this derivation hierarchy is that any numerical property can call bind()
on any other numerical property or binding. Indeed, the signature of the bind()
method on any numerical property is and any numerical property and binding is assignable to the generic parameter type. The program in Listing 3-2 shows that any numerical properties of different specific types can be bound to each other.
void bind(ObservableValue<? extends Number> observable);
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.FloatProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.LongProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleFloatProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleLongProperty;
public class NumericPropertiesExample {
public static void main(String[] args) {
IntegerProperty i = new SimpleIntegerProperty(null, "i", 1024);
LongProperty l = new SimpleLongProperty(null, "l", 0L);
FloatProperty f = new SimpleFloatProperty(null, "f", 0.0F);
DoubleProperty d = new SimpleDoubleProperty(null, "d", 0.0);
System.out.println("Constructed numerical properties i, l, f, d.");
System.out.println("i.get() = " + i.get());
System.out.println("l.get() = " + l.get());
System.out.println("f.get() = " + f.get());
System.out.println("d.get() = " + d.get());
l.bind(i);
f.bind(l);
d.bind(f);
System.out.println("Bound l to i, f to l, d to f.");
System.out.println("i.get() = " + i.get());
System.out.println("l.get() = " + l.get());
System.out.println("f.get() = " + f.get());
System.out.println("d.get() = " + d.get());
System.out.println("Calling i.set(2048).");
i.set(2048);
System.out.println("i.get() = " + i.get());
System.out.println("l.get() = " + l.get());
System.out.println("f.get() = " + f.get());
System.out.println("d.get() = " + d.get());
d.unbind();
f.unbind();
l.unbind();
System.out.println("Unbound l to i, f to l, d to f.");
f.bind(d);
l.bind(f);
i.bind(l);
System.out.println("Bound f to d, l to f, i to l.");
System.out.println("Calling d.set(10000000000L).");
d.set(10000000000L);
System.out.println("d.get() = " + d.get());
System.out.println("f.get() = " + f.get());
System.out.println("l.get() = " + l.get());
System.out.println("i.get() = " + i.get());
}
}
In this example we created four numeric properties and bound them into a chain in decreasing size to demonstrate that the bindings work as expected. We then reversed the order of the chain and set the double property’s value to a number that would overflow the integer property to highlight the fact that even though you can bind different sizes of numeric properties together, when the value of the dependent property is outside the range of the binding property, normal Java numeric conversion applies.
When we run the program in Listing 3-4, the following is printed to the console:
Constructed numerical properties i, l, f, d.
i.get() = 1024
l.get() = 0
f.get() = 0.0
d.get() = 0.0
Bound l to i, f to l, d to f.
i.get() = 1024
l.get() = 1024
f.get() = 1024.0
d.get() = 1024.0
Calling i.set(2048).
i.get() = 2048
l.get() = 2048
f.get() = 2048.0
d.get() = 2048.0
Unbound l to i, f to l, d to f.
Bound f to d, l to f, i to l.
Calling d.set(10000000000L).
d.get() = 1.0E10
f.get() = 1.0E10
l.get() = 10000000000
i.get() = 1410065408
We now give a survey of the content of the four packages javafx.beans
, javafx.beans.binding
, javafx.beans.property
, and javafx.beans.value
. In this section, “the SimpleIntegerProperty
series of classes” refers to the classes extrapolated over the Boolean
, Integer
, Long
, Float
, Double
, String
, and Object
types. Therefore what is said also applies to SimpleBooleanProperty
, and so on.
SimpleIntegerProperty
series of classes. They provide all the functionalities of the Property interface including lazy evaluation. They are used in all the examples of this chapter up to this point.ReadOnlyIntegerWrapper
series of classes. These classes implement the Property
interface but also have a getReadOnlyProperty()
method that returns a ReadOnlyProperty
that is synchronized with the main Property
. They are very handy to use when you need a full-blown Property
for the implementation of a component but you only want to hand out a ReadOnlyProperty
to the client of the component.IntegerPropertyBase
series of abstract classes can be extended to provide implementations of full Property
classes, although in practice the SimpleIntegerProperty
series of classes is easier to use. The only abstract methods in the IntegerPropertyBase
series of classes are getBean()
and getName()
.ReadOnlyIntegerPropertyBase
series of abstract classes can be extended to provide implementations of ReadOnlyProperty
classes. This is rarely necessary. The only abstract methods in the ReadOnlyIntegerPropertyBase
series of classes are get()
, getBean()
, and getName()
.WeakInvalidationListener
and WeakChangeListener
classes can be used to wrap InvalidationListener
and ChangeListener
instances before addListener()
is called. They hold weak references of the wrapped listener instances. As long as you hold a reference to the wrapped listener on your side, the weak references will be kept alive and you will receive events. When you are done with the wrapped listener and have unreferenced it from your side, the weak references will be eligible for garbage collection and later garbage collected. All the JavaFX 2.0 properties and bindings framework Observable objects know how to clean up a weak listener after its weak reference has been garbage collected. This prevents memory leaks when the listeners are not removed after use.That covers all the JavaFX 2.0 properties and bindings API that reside in the javafx.beans
, javafx.beans.property
, and javafx.beans.value
packages and some but not all of the APIs in the javafx.beans.binding
package. The remaining classes of the javafx.beans.binding
package are APIs that help you to create new bindings out of existing properties and bindings. That is the focus of the next section.
We now turn our focus to the creation of new bindings out of existing properties and bindings. You learned in the “Understanding Key Interfaces and Concepts” section earlier in this chapter that a binding is an observable value that has a list of dependencies which are also observable values.
The JavaFX 2.0 properties and bindings framework offers three ways of creating new bindings:
IntegerBinding
series of abstract classesBindings
IntegerExpression
series of abstract classesYou saw the direct extension approach in the “Understanding the Binding Interface” section earlier in this chapter. We explore the Bindings
utility class next.
The Bindings
class contains 163 factory methods that make new bindings out of existing observable values and regular values. Most of the methods are overloaded to take into account that both observable values and regular Java (unobservable) values can be used to build new bindings. At least one of the parameters must be an observable value. Here are the signatures of the nine overloaded add()
methods:
public static NumberBinding add(ObservableNumberValue n1, ObservableNumberValue n2)
public static DoubleBinding add(ObservableNumberValue n, double d)
public static DoubleBinding add(double d, ObservableNumberValue n)
public static NumberBinding add(ObservableNumberValue n, float f)
public static NumberBinding add(float f, ObservableNumberValue n)
public static NumberBinding add(ObservableNumberValue n, long l)
public static NumberBinding add(long l, ObservableNumberValue n)
public static NumberBinding add(ObservableNumberValue n, int i)
public static NumberBinding add(int i, ObservableNumberValue n)
When the add()
method is called, it returns a NumberBinding
whose dependencies include all the observable value parameters, and whose value is the sum of the value of its two parameters. Similarly overloaded methods exist for subtract()
, multiply(),
and divide()
.
Note Recall from the last section that ObservableIntegerValue
, ObservableLongValue
, ObservableFloatValue
, and ObservableDoubleValue
are subclasses of ObservableNumberValue
. Therefore the four arithmetic methods mentioned above can take any combinations of these observable numeric values as well as any unobservable values.
The program in Listing 3-5 uses the arithmetic methods in Bindings
to calculate the area of a triangle in the Cartesian plane with vertices (x1, y1)
, (x2, y2)
, (x3, y3)
using this formula:
Area = (x1*y2 + x2*y3 + x3*y1 – x1*y3 – x2*y1 – x3*y2) / 2
import javafx.beans.binding.Bindings;
import javafx.beans.binding.NumberBinding;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
public class TriangleAreaExample {
public static void main(String[] args) {
IntegerProperty x1 = new SimpleIntegerProperty(0);
IntegerProperty y1 = new SimpleIntegerProperty(0);
IntegerProperty x2 = new SimpleIntegerProperty(0);
IntegerProperty y2 = new SimpleIntegerProperty(0);
IntegerProperty x3 = new SimpleIntegerProperty(0);
IntegerProperty y3 = new SimpleIntegerProperty(0);
final NumberBinding x1y2 = Bindings.multiply(x1, y2);
final NumberBinding x2y3 = Bindings.multiply(x2, y3);
final NumberBinding x3y1 = Bindings.multiply(x3, y1);
final NumberBinding x1y3 = Bindings.multiply(x1, y3);
final NumberBinding x2y1 = Bindings.multiply(x2, y1);
final NumberBinding x3y2 = Bindings.multiply(x3, y2);
final NumberBinding sum1 = Bindings.add(x1y2, x2y3);
final NumberBinding sum2 = Bindings.add(sum1, x3y1);
final NumberBinding sum3 = Bindings.add(sum2, x3y1);
final NumberBinding diff1 = Bindings.subtract(sum3, x1y3);
final NumberBinding diff2 = Bindings.subtract(diff1, x2y1);
final NumberBinding determinant = Bindings.subtract(diff2, x3y2);
final NumberBinding area = Bindings.divide(determinant, 2.0D);
x1.set(0); y1.set(0);
x2.set(6); y2.set(0);
x3.set(4); y3.set(3);
printResult(x1, y1, x2, y2, x3, y3, area);
x1.set(1); y1.set(0);
x2.set(2); y2.set(2);
x3.set(0); y3.set(1);
printResult(x1, y1, x2, y2, x3, y3, area);
}
private static void printResult(IntegerProperty x1, IntegerProperty y1,
IntegerProperty x2, IntegerProperty y2,
IntegerProperty x3, IntegerProperty y3,
NumberBinding area) {
System.out.println("For A(" +
x1.get() + "," + y1.get() + "), B(" +
x2.get() + "," + y2.get() + "), C(" +
x3.get() + "," + y3.get() + "), the area of triangle ABC is " + area.getValue());
}
}
We used IntegerProperty
to represent the co-ordinates. The building up of the NumberBinding
area
uses all four arithmetic factory methods of Bindings
. Because we started with IntegerProperty
objects, even though the return type from the arithmetic factory methods of Bindings
are NumberBinding
, the actual object that is returned, up to determinant
, are IntegerBinding
objects. We used 2.0D
rather than a mere 2
in the divide()
call to force the division to be done as a double
division, not as int
division. All the properties and bindings that we build up form a tree structure with area
as the root, the intermediate bindings as internal nodes, and the properties x1
, y1
, x2
, y2
, x3
, y3
as leaves. This tree is similar to the parse tree we will get if we parse the mathematical expression for the area formula using grammar for the regular arithmetic expressions.
When we run the program in Listing 3-5, the following output is printed to the console:
For A(0,0), B(6,0), C(4,3), the area of triangle ABC is 9.0
For A(1,0), B(2,2), C(0,1), the area of triangle ABC is 1.5
Aside from the arithmetic methods, the Bindings
class also has the following factory methods.
and
, or
, not
min
, max
, negate
isNull
, isNotNull
equal
equalIgnoreCase
greaterThan
greaterThanOrEqual
lessThan
lessThanOrEqual
notEqual
notEqualIgnoreCase
select
selectBoolean
selectInteger
selectLong
selectFloat
selectDouble
selectString
Except for the selection operators, the preceding operators all do what you think they will do. The object operators are meaningful only for observable string values and observable object values. All relational operators except for the IgnoreCase
ones apply to numeric values. There are versions of the equal
and notEqual
operators for numeric values that have a third double
parameter for the tolerance when comparing float
or double
values. The equal
and notEqual
operators also apply to boolean
, string, and object values. For string and object values, the equal
and notEqual
operator compares their values using the equals()
method.
The selection operators operate on what are called JavaFX beans, Java classes constructed according to the JavaFX Beans specification. We talk about JavaFX Beans in the “Understanding JavaFX Beans” section later in this chapter.
That covers all methods in Bindings
that return a binding object. There are seven methods in Bindings
that do not return a binding object. The bindBidirectional()
and unbindBidirectional()
methods create bidirectional bindings. As a matter of fact, the bindBidirectional()
and unbindBidirectional()
methods in the various properties classes simply call the corresponding ones in the Bindings
class. Four of the other five methods, convert()
, concat()
, and a pair of overloaded format()
, return StringExpression
objects. And finally the when()
method returns a When
object.
The When
and the StringExpression
classes are part of the fluent interface API for creating bindings, which we cover in the next subsection.
If you asked the question: “Why would anybody name a method when()
? And what kind of information would the When
class encapsulate?” Welcome to the club. While you were not looking, the object-oriented programming community invented a brand new way of API design that totally disregards the decades-old principles of object-oriented practices. Instead of encapsulating data and distributing business logic into relevant domain objects, this new methodology produces a style of API that encourages method chaining and uses the return type of one method to determine what methods are available for the next car of the choo-choo train. Method names are chosen not to convey complete meaning but to make the entire method chain read like a fluent sentence. This style of APIs is called fluent interface APIs.
Note You can find a more through exposition of fluent interfaces on Martin Fowler’s web site, referenced at the end of this chapter.
The fluent interface APIs for creating bindings are defined in the IntegerExpression
series of classes. IntegerExpression
is a superclass of both IntegerProperty
and IntegerBinding
, making the methods of IntegerExpression
also available in the IntegerProperty
and IntegerBinding
classes. The four numeric expression classes share a common superinterface NumberExpression
, where all the methods are defined. The type-specific expression classes override some of the methods that yield a NumberBinding
to return a more appropriate type of binding.
The methods thus made available for the seven kinds of properties and bindings are listed here:
BooleanProperty
and BooleanBinding
BooleanBinding and(ObservableBooleanValue b)
BooleanBinding or(ObservableBooleanValue b)
BooleanBinding not
()BooleanBinding isEqualTo(ObservableBooleanValue b)
BooleanBinding isNotEqualTo(ObservableBooleanValue b)
StringBinding asString()
BooleanBinding isEqualTo(ObservableNumberValue m)
BooleanBinding isEqualTo(ObservableNumberValue m, double err)
BooleanBinding isEqualTo(double d, double err)
BooleanBinding isEqualTo(float f, double err)
BooleanBinding isEqualTo(long l)
BooleanBinding isEqualTo(long l, double err)
BooleanBinding isEqualTo(int i)
BooleanBinding isEqualTo(int i, double err)
BooleanBinding isNotEqualTo(ObservableNumberValue m)
BooleanBinding isNotEqualTo(ObservableNumberValue m, double err)
BooleanBinding isNotEqualTo(double d, double err)
BooleanBinding isNotEqualTo(float f, double err)
BooleanBinding isNotEqualTo(long l)
BooleanBinding isNotEqualTo(long l, double err)
BooleanBinding isNotEqualTo(int i)
BooleanBinding isNotEqualTo(int i, double err)
BooleanBinding greaterThan(ObservableNumberValue m)
BooleanBinding greaterThan(double d)
BooleanBinding greaterThan(float f)
BooleanBinding greaterThan(long l)
BooleanBinding greaterThan(int i)
BooleanBinding lessThan(ObservableNumberValue m)
BooleanBinding lessThan(double d)
BooleanBinding lessThan(float f)
BooleanBinding lessThan(long l)
BooleanBinding lessThan(int i)
BooleanBinding greaterThanOrEqualTo(ObservableNumberValue m)
BooleanBinding greaterThanOrEqualTo(double d)
BooleanBinding greaterThanOrEqualTo(float f)
BooleanBinding greaterThanOrEqualTo(long l)
BooleanBinding greaterThanOrEqualTo(int i)
BooleanBinding lessThanOrEqualTo(ObservableNumberValue m)
BooleanBinding lessThanOrEqualTo(double d)
BooleanBinding lessThanOrEqualTo(float f)
BooleanBinding lessThanOrEqualTo(long l)
BooleanBinding lessThanOrEqualTo(int i)
StringBinding asString()
StringBinding asString(String str)
StringBinding asString(Locale locale, String str)
IntegerProperty
and IntegerBinding
IntegerBinding negate()
NumberBinding add(ObservableNumberValue n)
DoubleBinding add(double d)
FloatBinding add(float f)
LongBinding add(long l)
IntegerBinding add(int i)
NumberBinding subtract(ObservableNumberValue n)
DoubleBinding subtract(double d)
FloatBinding subtract(float f)
LongBinding subtract(long l)
IntegerBinding subtract(int i)
NumberBinding multiply(ObservableNumberValue n)
DoubleBinding multiply(double d)
FloatBinding multiply(float f)
LongBinding multiply(long l)
IntegerBinding multiply(int i)
NumberBinding divide(ObservableNumberValue n)
DoubleBinding divide(double d)
FloatBinding divide(float f)
LongBinding divide(long l)
IntegerBinding divide(int i)
LongProperty
and LongBinding
LongBinding negate()
NumberBinding add(ObservableNumberValue n)
DoubleBinding add(double d)
FloatBinding add(float f)
LongBinding add(long l)
LongBinding add(int i)
NumberBinding subtract(ObservableNumberValue n)
DoubleBinding subtract(double d)
FloatBinding subtract(float f)
LongBinding subtract(long l)
LongBinding subtract(int i)
NumberBinding multiply(ObservableNumberValue n)
DoubleBinding multiply(double d)
FloatBinding multiply(float f)
LongBinding multiply(long l)
LongBinding multiply(int i)
NumberBinding divide(ObservableNumberValue n)
DoubleBinding divide(double d)
FloatBinding divide(float f)
LongBinding divide(long l)
LongBinding divide(int i)
FloatProperty
and FloatBinding
FloatBinding negate()
DoubleProperty
and DoubleBinding
DoubleBinding negate()
DoubleBinding add(ObservableNumberValue n)
DoubleBinding add(double d)
DoubleBinding add(float f)
DoubleBinding add(long l)
DoubleBinding add(int i)
DoubleBinding subtract(ObservableNumberValue n)
DoubleBinding subtract(double d)
DoubleBinding subtract(float f)
DoubleBinding subtract(long l)
DoubleBinding subtract(int i)
DoubleBinding multiply(ObservableNumberValue n)
DoubleBinding multiply(double d)
DoubleBinding multiply(float f)
DoubleBinding multiply(long l)
DoubleBinding multiply(int i)
DoubleBinding divide(ObservableNumberValue n)
DoubleBinding divide(double d)
DoubleBinding divide(float f)
DoubleBinding divide(long l)
DoubleBinding divide(int i)
StringProperty
and StringBinding
StringExpression concat(Object obj)
BooleanBinding isEqualTo(ObservableStringValue str)
BooleanBinding isEqualTo(String str)
BooleanBinding isNotEqualTo(ObservableStringValue str)
BooleanBinding isNotEqualTo(String str)
BooleanBinding isEqualToIgnoreCase(ObservableStringValue str)
BooleanBinding isEqualToIgnoreCase(String str)
BooleanBinding isNotEqualToIgnoreCase(ObservableStringValue str)
BooleanBinding isNotEqualToIgnoreCase(String str)
BooleanBinding greaterThan(ObservableStringValue str)
BooleanBinding greaterThan(String str)
BooleanBinding lessThan(ObservableStringValue str)
BooleanBinding lessThan(String str)
BooleanBinding greaterThanOrEqualTo(ObservableStringValue str)
BooleanBinding greaterThanOrEqualTo(String str)
BooleanBinding lessThanOrEqualTo(ObservableStringValue str)
BooleanBinding lessThanOrEqualTo(String str)
BooleanBinding isNull()
BooleanBinding isNotNull()
ObjectProperty
and ObjectBinding
BooleanBinding isEqualTo(ObservableObjectValue<?> obj)
BooleanBinding isEqualTo(Object obj)
BooleanBinding isNotEqualTo(ObservableObjectValue<?> obj)
BooleanBinding isNotEqualTo(Object obj)
BooleanBinding isNull()
BooleanBinding isNotNull()
With these methods, you can create an infinite variety of bindings by starting with a property and calling one of the methods that is appropriate for the type of the property to get a binding, and calling one of the methods that is appropriate for the type of the binding to get another binding, and so on. One fact that is worth pointing out here is that all the methods for the type-specific numeric expressions are defined in the NumberExpression
base interface with a return type of NumberBinding
, and are overridden in the type-specific expression classes with an identical parameter signature but a more specific return type. This way of overriding a method in a subclass with an identical parameter signature but a more specific return type is called covariant return-type overriding, and has been a Java language feature since Java 5. One of the consequences of this fact is that numeric bindings built with the fluent interface API have more specific types than those built with factory methods in the Bindings
class.
The program in Listing 3-6 is a modification of the triangle area example in Listing 3-5 that uses the fluent interface API instead of calling factory methods in the Bindings
class.
import javafx.beans.binding.Bindings;
import javafx.beans.binding.NumberBinding;
import javafx.beans.binding.StringExpression;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
public class TriangleAreaFluentExample {
public static void main(String[] args) {
IntegerProperty x1 = new SimpleIntegerProperty(0);
IntegerProperty y1 = new SimpleIntegerProperty(0);
IntegerProperty x2 = new SimpleIntegerProperty(0);
IntegerProperty y2 = new SimpleIntegerProperty(0);
IntegerProperty x3 = new SimpleIntegerProperty(0);
IntegerProperty y3 = new SimpleIntegerProperty(0);
final NumberBinding area = x1.multiply(y2)
.add(x2.multiply(y3))
.add(x3.multiply(y1))
.subtract(x1.multiply(y3))
.subtract(x2.multiply(y1))
.subtract(x3.multiply(y2))
.divide(2.0D);
StringExpression output = Bindings.format(
"For A(%d,%d), B(%d,%d), C(%d,%d), the area of triangle ABC is %3.1f",
x1, y1, x2, y2, x3, y3, area);
x1.set(0); y1.set(0);
x2.set(6); y2.set(0);
x3.set(4); y3.set(3);
System.out.println(output.get());
x1.set(1); y1.set(0);
x2.set(2); y2.set(2);
x3.set(0); y3.set(1);
System.out.println(output.get());
}
}
Notice how the 13 lines of code and 12 intermediate variables used in Listing 3-5 to build up the area binding are reduced to the 7 lines of code with no intermediate variables used in Listing 3-6. We also used the Bindings.format()
method to build up a StringExpression
object called output
. There are two overloaded Bindings.format()
methods with signatures:
StringExpression format(Locale locale, String format, Object... args)
StringExpression format(String format, Object... args)
They work similarly to the corresponding String.format()
methods in that they format the values args
according to the format specification format
and the Locale
locale
, or the default Locale
. If any of the args
is an ObservableValue
, its change is reflected in the StringExpression
.
When we run the program in Listing 3-6, the following output is printed to the console:
For A(0,0), B(6,0), C(4,3), the area of triangle ABC is 9.0
For A(1,0), B(2,2), C(0,1), the area of triangle ABC is 1.5
Next we unravel the mystery of the When
class and the role it plays in constructing bindings that are essentially if/then/else expressions. The When class has a constructor that takes an ObservableBooleanValue
argument:
public When(ObservableBooleanValue b)
It has the following 11 overloaded then()
methods.
When.NumberConditionBuilder then(ObservableNumberValue n)
When.NumberConditionBuilder then(double d)
When.NumberConditionBuilder then(float f)
When.NumberConditionBuilder then(long l)
When.NumberConditionBuilder then(int i)
When.BooleanConditionBuilder then(ObservableBooleanValue b)
When.BooleanConditionBuilder then(boolean b)
When.StringConditionBuilder then(ObservableStringValue str)
When.StringConditionBuilder then(String str)
When.ObjectConditionBuilder<T> then(ObservableObjectValue<T> obj)
When.ObjectConditionBuilder<T> then(T obj)
The type of object returned from the then()
method depends on the type of the argument. If the argument is a numeric type, either observable or unobservable, the return type is a nested class When.NumberConditionBuilder
. Similarly, for Boolean arguments, the return type is When.BooleanConditionBuilder
; for string arguments, When.StringConditionBuilder
; and for object arguments, When.ObjectConditionBuilder
.
These condition builders in turn have the following otherwise()
methods.
When.NumberConditionBuilder
NumberBinding otherwise(ObservableNumberValue n)
DoubleBinding otherwise(double d)
NumberBinding otherwise(float f)
NumberBinding otherwise(long l)
NumberBinding otherwise(int i)
When.BooleanConditionBuilder
BooleanBinding otherwise(ObservableBooleanValue b)
BooleanBinding otherwise(boolean b)
When.StringConditionBuilder
StringBinding otherwise(ObservableStringValue str)
StringBinding otherwise(String str)
When.ObjectConditionBuilder
ObjectBinding<T> otherwise(ObservableObjectValue<T> obj)
ObjectBinding<T> otherwise(T obj)
The net effect of these method signatures is that you can build up a binding that resembles an if/then/else expression this way:
new When(b).then(x).otherwise(y)
where b
is an ObservableBooleanValue
, and x
and y
are of similar types and can be either observable or unobservable. The resulting binding will be of a type similar to that of x
and y
.
The program in Listing 3-7 uses the fluent interface API from the When
class to calculate the area of a triangle with given sides a
, b
, and c
. Recall that to form a triangle, the three sides must satisfy the following conditions.
a + b > c, b + c > a, c + a > b,
When the preceding conditions are satisfied, the area of the triangle can be calculated using Heron’s formula:
Area = sqrt(s * (s – a) * (s – b) * (s – c))
where s
is the semiperimeter:
s = (a + b + c) / 2.
import javafx.beans.binding.DoubleBinding;
import javafx.beans.binding.When;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
public class HeronsFormulaExample {
public static void main(String[] args) {
DoubleProperty a = new SimpleDoubleProperty(0);
DoubleProperty b = new SimpleDoubleProperty(0);
DoubleProperty c = new SimpleDoubleProperty(0);
DoubleBinding s = a.add(b).add(c).divide(2.0D);
final DoubleBinding areaSquared = new When(
a.add(b).greaterThan(c)
.and(b.add(c).greaterThan(a))
.and(c.add(a).greaterThan(b)))
.then(s.multiply(s.subtract(a))
.multiply(s.subtract(b))
.multiply(s.subtract(c)))
.otherwise(0.0D);
a.set(3);
b.set(4);
c.set(5);
System.out.printf("Given sides a = %1.0f, b = %1.0f, and c = %1.0f," +
" the area of the triangle is %3.2f
", a.get(), b.get(), c.get(),
Math.sqrt(areaSquared.get()));
a.set(2);
b.set(2);
c.set(2);
System.out.printf("Given sides a = %1.0f, b = %1.0f, and c = %1.0f," +
" the area of the triangle is %3.2f
", a.get(), b.get(), c.get(),
Math.sqrt(areaSquared.get()));
}
}
Inasmuch as there is no ready-made binding method in DoubleExpression
that calculates the square root, we create a DoubleBinding
for areaSquared
instead. The constructor argument for When()
is a BooleanBinding
built out of the three conditions on a
, b
, and c
. The argument for the then()
method is a DoubleBinding
that calculates the square of the area of the triangle. And because the then()
argument is numeric, the otherwise()
argument also has to be numeric. We choose to use 0.0D
to signal that an invalid triangle is encountered.
Note Instead of using the When()
constructor, you can also use the factory method when()
in the Bindings
utility class to create the When
object.
When we run the program in Listing 3-7, the following output is printed to the console:
Given sides a = 3, b = 4, and c = 5, the area of the triangle is 6.00.
Given sides a = 2, b = 2, and c = 2, the area of the triangle is 1.73.
If the binding defined in Listing 3-7 makes your head spin a little, you are not alone. We choose this example simply to illustrate the use of the fluent interface API offered by the When
class. As a matter of fact, this example may be better served with a direct subclassing approach we first introduced in the “Understanding the Binding Interface” section earlier in this chapter.
The program in Listing 3-8 solves the same problem as Listing 3-7 by using the direct extension method.
import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
public class HeronsFormulaDirectExtensionExample {
public static void main(String[] args) {
final DoubleProperty a = new SimpleDoubleProperty(0);
final DoubleProperty b = new SimpleDoubleProperty(0);
final DoubleProperty c = new SimpleDoubleProperty(0);
DoubleBinding area = new DoubleBinding() {
{
super.bind(a, b, c);
}
@Override
protected double computeValue() {
double a0 = a.get();
double b0 = b.get();
double c0 = c.get();
if ((a0 + b0 > c0) && (b0 + c0 > a0) && (c0 + a0 > b0)) {
double s = (a0 + b0 + c0) / 2.0D;
return Math.sqrt(s * (s - a0) * (s - b0) * (s - c0));
} else {
return 0.0D;
}
}
};
a.set(3);
b.set(4);
c.set(5);
System.out.printf("Given sides a = %1.0f, b = %1.0f, and c = %1.0f," +
" the area of the triangle is %3.2f
", a.get(), b.get(), c.get(),
area.get());
a.set(2);
b.set(2);
c.set(2);
System.out.printf("Given sides a = %1.0f, b = %1.0f, and c = %1.0f," +
" the area of the triangle is %3.2f
", a.get(), b.get(), c.get(),
area.get());
}
}
The direct extension method is preferred for complicated expressions and for expressions that go beyond the available operators.
Now that you have mastered all the APIs in the javafx.beans
, javafx.beans.binding
, javafx.beans.property
, and javafx.beans.value
packages, you are ready step beyond the details of the JavaFX 2.0 properties and bindings framework and learn how these properties are organized into bigger components called JavaFX Beans.
JavaFX 2.0 introduces the concept of JavaFX Beans, a set of conventions that provide properties support for Java objects. In this section, we talk about the naming conventions for specifying JavaFX Beans properties, several ways of implementing JavaFX Beans properties, and finally the use of selection bindings.
For many years Java has used the JavaBeans API to represent a property of an object. A JavaBeans property is represented by a pair of getter and setter methods. Property changes are propagated to property change listeners through the firing of property change events in the setter code.
JavaFX 2.0 introduces the JavaFX Beans specification that adds properties support to Java objects through the help of the properties classes from the JavaFX 2.0 properties and bindings framework.
Caution The word property is used here with two distinct meanings. When we say JavaFX Beans properties, it should be understood to mean a higher-level concept similar to JavaBeans properties. When we say JavaFX 2.0 properties and bindings framework properties, it should be understood to mean the various implementations of the Property
or ReadOnlyProperty
interfaces, such as IntegerProperty
, StringProperty
, and so on. JavaFX Beans properties are specified using JavaFX 2.0 properties and bindings framework properties.
Like its JavaBeans counterparts, JavaFX Beans properties are specified by a set of methods in a Java class. To define a JavaFX Beans property in a Java class, you provide three methods: the getter, the setter, and the property getter. For a property named height
of type double
, the three methods are:
public final double getHeight();
public final void setHeight(double h);
public DoubleProperty heightProperty();
The names of the getter and setter methods follow the JavaBeans convention. They are obtained by concatenating “get
” and “set
” with the name of the property with the first character capitalized. For boolean
type properties, the getter name can also start with “is
”. The name of the property getter is obtained by concatenating the name of the property with “Property
”. To define a read only JavaFX Beans property, you can either remove the setter method or change it to a private method and change the return type of the property getter to be a ReadOnlyProperty
.
This specification speaks only about the interface of JavaFX Beans properties and does not impose any implementation constraints. Depending on the number of properties a JavaFX Bean may have, and the usage patterns of these properties, there are several implementation strategies. Not surprisingly, all of them use the JavaFX 2.0 properties and bindings framework properties as the backing store for the values of the JavaFX Beans properties. We show you these strategies in the next two subsections.
The eagerly instantiated properties strategy is the simplest way to implement JavaFX Beans properties. For every JavaFX Beans property you want to define in an object, you introduce a private field in the class that is of the appropriate JavaFX 2.0 properties and bindings framework property type. These private fields are instantiated at bean construction time. The getter and setter methods simply call the private field’s get()
and set()
methods. The property getter simply returns the private field itself.
The program in Listing 3-9 defines a JavaFX Bean with an int
property i
, a String
property str
, and a Color
property color
.
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.paint.Color;
public class JavaFXBeanModelExample {
private IntegerProperty i = new SimpleIntegerProperty(this, "i", 0);
private StringProperty str = new SimpleStringProperty(this, "str", "Hello");
private ObjectProperty<Color> color = new SimpleObjectProperty<Color>(this, "color",
Color.BLACK);
public final int getI() {
return i.get();
}
public final void setI(int i) {
this.i.set(i);
}
public IntegerProperty iProperty() {
return i;
}
public final String getStr() {
return str.get();
}
public final void setStr(String str) {
this.str.set(str);
}
public StringProperty strProperty() {
return str;
}
public final Color getColor() {
return color.get();
}
public final void setColor(Color color) {
this.color.set(color);
}
public ObjectProperty<Color> colorProperty() {
return color;
}
}
This is a straightforward Java class. There are only two things we want to point out in this implementation. First, the getter and setter methods are declared final
by convention. Secondly, when the private fields are initialized, we called the simple properties constructors with the full context information, supplying them with this
as the first parameter. In all of our previous examples in this chapter, we used null
as the first parameter for the simple properties constructors because those properties are not part of a higher-level JavaFX Bean object.
The program in Listing 3-10 defines a view class that watches over an instance of the JavaFX Bean defined in Listing 3-9. It observes changes to the i
, str
, and color
properties of the bean by hooking up change listeners that print out any changes to the console.
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.paint.Color;
public class JavaFXBeanViewExample {
private JavaFXBeanModelExample model;
public JavaFXBeanViewExample(JavaFXBeanModelExample model) {
this.model = model;
hookupChangeListeners();
}
private void hookupChangeListeners() {
model.iProperty().addListener(new ChangeListener<Number>() {
@Override
public void changed(ObservableValue<? extends Number> observableValue, Number
oldValue, Number newValue) {
System.out.println("Property i changed: old value = " + oldValue + ", new
value = " + newValue);
}
});
model.strProperty().addListener(new ChangeListener<String>() {
@Override
public void changed(ObservableValue<? extends String> observableValue, String
oldValue, String newValue) {
System.out.println("Property str changed: old value = " + oldValue + ", new
value = " + newValue);
}
});
model.colorProperty().addListener(new ChangeListener<Color>() {
@Override
public void changed(ObservableValue<? extends Color> observableValue, Color
oldValue, Color newValue) {
System.out.println("Property color changed: old value = " + oldValue + ",
new value = " + newValue);
}
});
}
}
The program in Listing 3-11 defines a controller that can modify a model object.
import javafx.scene.paint.Color;
public class JavaFXBeanControllerExample {
private JavaFXBeanModelExample model;
private JavaFXBeanViewExample view;
public JavaFXBeanControllerExample(JavaFXBeanModelExample model, JavaFXBeanViewExample
view) {
this.model = model;
this.view = view;
}
public void incrementIPropertyOnModel() {
model.setI(model.getI() + 1);
}
public void changeStrPropertyOnModel() {
final String str = model.getStr();
if (str.equals("Hello")) {
model.setStr("World");
} else {
model.setStr("Hello");
}
}
public void switchColorPropertyOnModel() {
final Color color = model.getColor();
if (color.equals(Color.BLACK)) {
model.setColor(Color.WHITE);
} else {
model.setColor(Color.BLACK);
}
}
}
Notice that this is not a full-blown controller and does not do anything with its reference to the view object. The program in Listing 3-12 provides a main program that assembles and test drives the classes in Listings 3-9 to 3-11 in a typical model–view–controller pattern.
public class JavaFXBeanMainExample {
public static void main(String[] args) {
JavaFXBeanModelExample model = new JavaFXBeanModelExample();
JavaFXBeanViewExample view = new JavaFXBeanViewExample(model);
JavaFXBeanControllerExample controller = new JavaFXBeanControllerExample(model, view);
controller.incrementIPropertyOnModel();
controller.changeStrPropertyOnModel();
controller.switchColorPropertyOnModel();
controller.incrementIPropertyOnModel();
controller.changeStrPropertyOnModel();
controller.switchColorPropertyOnModel();
}
}
When we run the program in Listings 3-9 to 3-12, the following output is printed to the console:
Property i changed: old value = 0, new value = 1
Property str changed: old value = Hello, new value = World
Property color changed: old value = Color[red=0,green=0,blue=0,opacity=1.0], new value =
Color[red=255,green=255,blue=255,opacity=1.0]
Property i changed: old value = 1, new value = 2
Property str changed: old value = World, new value = Hello
Property color changed: old value = Color[red=255,green=255,blue=255,opacity=1.0], new value
= Color[red=0,green=0,blue=0,opacity=1.0]
If your JavaFX Bean has many properties, instantiating all the properties objects up front at bean creation time may be too heavy an approach. The memory for all the properties objects is truly wasted if only a few of the properties are actually used. In such situations, you can use one of several lazily instantiated properties strategies. Two typical such strategies are the half-lazy instantiation strategy and the full-lazy instantiation strategy.
In the half-lazy strategy, the property object is instantiated only if the setter is called with a value that is different from the default value, or if the property getter is called. The program in Listing 3-13 illustrates how this strategy is implemented.
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class JavaFXBeanModelHalfLazyExample {
private static final String DEFAULT_STR = "Hello";
private StringProperty str;
public final String getStr() {
if (str != null) {
return str.get();
} else {
return DEFAULT_STR;
}
}
public final void setStr(String str) {
if ((this.str != null) || !(str.equals(DEFAULT_STR))) {
strProperty().set(str);
}
}
public StringProperty strProperty() {
if (str == null) {
str = new SimpleStringProperty(this, "str", DEFAULT_STR);
}
return str;
}
}
In this strategy, the client code can call the getter many times without the property object being instantiated. If the property object is null, the getter simply returns the default value. As soon as the setter is called with a value that is different from the default value, it will call the property getter which lazily instantiates the property object. The property object is also instantiated if the client code calls the property getter directly.
In the full-lazy strategy, the property object is instantiated only if the property getter is called. The getter and setter go through the property object only if it is already instantiated. Otherwise they go through a separate field.
The program in Listing 3-14 shows an example of a full-lazy property.
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class JavaFXBeanModelFullLazyExample {
private static final String DEFAULT_STR = "Hello";
private StringProperty str;
private String _str = DEFAULT_STR;
public final String getStr() {
if (str != null) {
return str.get();
} else {
return _str;
}
}
public final void setStr(String str) {
if (this.str != null) {
this.str.set(str);
} else {
_str = str;
}
}
public StringProperty strProperty() {
if (str == null) {
str = new SimpleStringProperty(this, "str", DEFAULT_STR);
}
return str;
}
}
Caution The full-lazy instantiation strategy incurs the cost of an extra field in order to stave off the need for property instantiation a little longer. Similarly, both the half-lazy and the full-lazy instantiation strategies incur costs of implementation complexity and runtime performance to gain the benefit of a potentially reduced runtime memory footprint. This is a classical trade-off situation in software engineering. Which strategy you choose will depend on the circumstance of your application. Our advice is to introduce optimization only if there is a need.
As you have seen in the "Understanding the Bindings Utility Class" subsection of the "Creating Bindings" section, the Bindings
utility class contains seven selection operators. The method signatures of these operators are:
select(ObservableValue<?> root, String... steps)
selectBoolean(ObservableValue<?> root, String... steps)
selectDouble(ObservableValue<?> root, String... steps)
selectFloat(ObservableValue<?> root, String... steps)
selectInteger(ObservableValue<?> root, String... steps)
selectLong(ObservableValue<?> root, String... steps)
selectString(ObservableValue<?> root, String... steps)
These selection operators allow you to create bindings that observe deeply nested JavaFX Beans properties. Suppose that you have a JavaFX bean that has a property, whose type is a JavaFX bean that has a property, whose type is a JavaFX bean that has a property, and so on. Suppose also that you are observing the root of this properties chain through an ObjectProperty
. Then you can create a binding that observes the deeply nested JavaFX Beans property by calling one of the select methods whose type matches the type of the deeply nested JavaFX Beans property with the ObjectProperty
as the root, and the successive JavaFX Beans property names that reach into the deeply nested JavaFX Beans property as the rest of the arguments.
In the following example, we use a few classes from the javafx.scene.effect
package—Lighting
and Light
—to illustrate how the selection operator works. We teach you how to apply lighting to a JavaFX scene graph in a later chapter of the book. For now, our interest lies in the fact that Lighting
is a JavaFX bean that has a property named light
whose type is Light
, and that Light
is also a JavaFX bean that has a property named color
whose type is Color
(in javafx.scene.paint
).
The program in Listing 3-15 illustrates how to observe the color of the light of the lighting.
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.effect.Light;
import javafx.scene.effect.Lighting;
import javafx.scene.paint.Color;
public class SelectBindingExample {
public static void main(String[] args) {
ObjectProperty<Lighting> root = new SimpleObjectProperty<>();
final ObjectBinding<Color> selectBinding = Bindings.select(root, "light", "color");
selectBinding.addListener(new ChangeListener<Color>() {
@Override
public void changed(ObservableValue<? extends Color> observableValue, Color
oldValue, Color newValue) {
System.out.println(" The color changed:
old color = " +
oldValue + ",
new color = " + newValue);
}
});
System.out.println("firstLight is black.");
Light firstLight = new Light.Point();
firstLight.setColor(Color.BLACK);
System.out.println("secondLight is white.");
Light secondLight = new Light.Point();
secondLight.setColor(Color.WHITE);
System.out.println("firstLighting has firstLight.");
Lighting firstLighting = new Lighting();
firstLighting.setLight(firstLight);
System.out.println("secondLighting has secondLight.");
Lighting secondLighting = new Lighting();
secondLighting.setLight(secondLight);
System.out.println("Making root observe firstLighting.");
root.set(firstLighting);
System.out.println("Making root observe secondLighting.");
root.set(secondLighting);
System.out.println("Changing secondLighting's light to firstLight");
secondLighting.setLight(firstLight);
System.out.println("Changing firstLight's color to red");
firstLight.setColor(Color.RED);
}
}
In this example, the root
is an ObjectProperty
that observes Lighting
objects. The binding colorBinding
observes the color
property of the light
property of the Lighting
object that is the value of root
. We then created some Light and Lighting objects and changed their configuration in various ways.
When we run the program in Listing 3-15, the following output is printed to the console:
firstLight is black.
secondLight is white.
firstLighting has firstLight.
secondLighting has secondLight.
Making root observe firstLighting.
The color changed:
old color = null,
new color = Color[red=0,green=0,blue=0,opacity=1.0]
Making root observe secondLighting.
The color changed:
old color = Color[red=0,green=0,blue=0,opacity=1.0],
new color = Color[red=255,green=255,blue=255,opacity=1.0]
Changing secondLighting's light to firstLight
The color changed:
old color = Color[red=255,green=255,blue=255,opacity=1.0],
new color = Color[red=0,green=0,blue=0,opacity=1.0]
Changing firstLight's color to red
The color changed:
old color = Color[red=0,green=0,blue=0,opacity=1.0],
new color = Color[red=255,green=0,blue=0,opacity=1.0]
As expected, a change event is fired for every change in the configuration of the object being observed by root
; and the value of colorBinding always reflects the color of the light of the current Lighting object in root
.
Caution The JavaFX 2.0 properties and bindings framework does not issue any warnings if the supplied property names do not match any property names in a JavaFX bean. It will simply have the default value for the type: null
for object type, zero for numeric types, false
for boolean
type, and the empty string for string type.
In this chapter, you learned the fundamentals of the JavaFX 2.0 properties and bindings framework and the JavaFX Beans specification. You now understand the following important principles.
ChangeListener
to them.Bindings
utility class, using the fluent interface API, or directly extending the IntegerBinding
series of abstract classes.Here are some useful resources for understanding properties and bindings:
http://www.martinfowler.com/bliki/FluentInterface.html
http://download.oracle.com/javafx/2.0/binding/jfxpub-binding.htm
http://blog.netopyr.com/
18.119.103.204