I was writing a class in javafx where I had two properties that were bound bidirectionally and was testing some edge cases. I found that if you change the value of one of the properties from inside an invalidation listener it caused the properties to be out of sync. Here's a small example:
static class A {
IntegerProperty x = new SimpleIntegerProperty(this, "x", 0);
IntegerProperty y = new SimpleIntegerProperty(this, "y", 0);
A() {
x.bindBidirectional(y);
}
}
static void testA() {
A a = new A();
a.x.addListener( o -> { //InvalidationListener
if (a.x.get() < 0) a.x.set(0);
});
// after y is set to a negative value, x and y hold different values
// until either y is set to a value that's >= 0 or x is set to any value
a.y.set(-2);
System.out.println(a.x.get());
System.out.println(a.y.get());
}
Output:
0
-2
I was assuming that when using a bidirectional binding, changing one property would always cause the other to be updated. It's seems rare (and possibly unwise) that one would write an invalidation listener like that, but I'm thinking defensively here. If at least one these properties were exposed, I don't want it to be possible to break any invariants of my class. I was thinking that there are three possible explanations here:
The contract on bidirectional bindings is not that they always are in sync (either they hold the same value or they are marked as invalid), it's just on a best-effort basis. Thus, class invariants shouldn't be based on this fact.
Changing the value inside an invalidation listener breaks the precondition of bidirectional binds and should be avoided. Otherwise they are always in sync. Thus, you might base a class invariant on this fact, since it's reasonable to demand that clients shouldn't be writing invalidation listeners like that.
It's a bug and they are really meant to be sync, no matter what. I guess if this were true, then you could easily produce an infinite loop of change notifications (like adding an invalidation listener to y that always sets the value to < 0). So, it would be up to the client to prevent such cases.
Is any of these explanations close to the truth, or am I missing something else here? Also, I'd be interested to know if there exist some other kind of bind operation that takes these kinds of situations into account.
I suggest your second theory is true:
Changing the value inside an invalidation listener breaks the precondition of bidirectional binds and should be avoided. Otherwise they are always in sync.
Thus, you might base a class invariant on this fact, since it's reasonable to demand that clients shouldn't be writing invalidation listeners like that.
To understand the bidirectional binding operation you can look at the source.
See for example the TypedNumberBidirectionBinding. It has a flag updating
it sets and checks when a property is invalidated. If the property is updated again inside the invalidation listener, the updating flag will be true, and synchronization of the property value will be skipped.
(invalidation code simplified for example purposes).
if (!updating) {
updating = true;
try {
if (property1 == sourceProperty) {
T newValue = property1.getValue();
property2.setValue(newValue);
property2.getValue();
oldValue = newValue;
} else {
T newValue = (T)property2.getValue();
property1.setValue(newValue);
property1.getValue();
oldValue = newValue;
}
} finally {
updating = false;
}
}
The InvalidationListener javadoc states:
In general, it is considered bad practice to modify the observed value in this method.
So I recommend you don't "change the value of one of the properties from inside an invalidation listener".
a bidirectional bind on the valueProperty from the Slider class will not sync properly when out of bounds
This is because the Slider class does what is not recommended. It modifies the value of the slider value property in an invalidation listener.
@Override protected void invalidated() {
adjustValues();
notifyAccessibleAttributeChanged(AccessibleAttribute.VALUE);
}
The modification is to clamp the change value to the slider range.
This leads to the case that a bidirectional bind on the valueProperty from the Slider class will not sync properly when out of bounds.
The slider documentation states:
This value must always be between min and max, inclusive. If it is ever out of bounds either due to min or max changing or due to itself being changed, then it will be clamped to always remain valid.
As suggested in this answer:
the solution was to not expose my bound property directly, but rather use a
ReadOnlyObjectWrapper
to make my property read-only outside my class.Since read-only properties can't be set, you can't attach the kind of invalidation listener that breaks binds to them. You can still allow modification, through a custom setter or a second property if need be.
Elsewhere in the JavaFX API for somewhat similar cases, the approach with twin property sets is used, e.g. the node read/write disable property and the read-only disabled property, or read-only property usage in the Window class, such as the output scale read-only and render scale read/write properties.