I have recently discovered bindings and they seem great. I have however stumbled upon a binding I want to make that I cannot seem to figure out. I have a textfield which I want to bind to a double property in a bidirectional way. But, I only want the bind to be from the field to the double property if the text in the field can be converted to a double and if the double it converts to falls within some range. In the other direction I want the bind to be bound without restrictions (I also want to be able to do this for ints but this should be easy once the double one is fixed). I believe this has to be done with a low level bind, doesn't it? How can this be done?
I have just started using bindings and am not great with them so go easy on me.
Many thanks.
In JavaFX bindings simply add listeners and react accordingly. Thinking like that you could argue that the listeners are the "low-level" aspect of the API. To do what you want you will have to create your own listeners. I am not aware of anything that does what you want "out of the box".
An example that is not ready for "production use":
public static void bind(TextField field, DoubleProperty property) {
field.textProperty().addListener((observable, oldText, newText) -> {
try {
// If the text can't be converted to a double then an
// exception is thrown. In this case we do nothing.
property.set(Double.parseDouble(newText));
} catch (NumberFormatException ignore) {}
});
property.addListener((observable, oldNumber, newNumber) -> {
field.setText(Double.toString(newNumber.doubleValue()));
});
}
This will do what you want if I understood your requirements correctly. I believe this code opens up the possibility of a memory leak, however. Ideally, you'd want the listeners to not keep the other from being garbage collected. For instance, if property
is no longer strongly referenced then field
should not keep property
from being GC'd. Edit: This code, depending on the implementations of the ObservableValue
s, could also enter an infinite loop of updates as pointed out in the comments.
Edit: The first "robust" example I gave had some issues and didn't provide a way to unbind the properties from each other. I've changed the example to make it more correct and also provide said unbinding feature. This new example is based on how the developers of JavaFX handled bidirectional binding internally.
A more robust code example of what I gave above. This is heavily "inspired" by the code used by the standard JavaFX internal APIs. Specifically the class com.sun.javafx.binding.BidirectionalBinding
.
import javafx.beans.WeakListener;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import java.lang.ref.WeakReference;
import java.util.Objects;
public class CustomBindings {
// This code is based heavily on how the standard JavaFX API handles bidirectional bindings. Specifically,
// the class 'com.sun.javafx.binding.BidirectionalBinding'.
public static void bindBidirectional(StringProperty stringProperty, DoubleProperty doubleProperty) {
if (stringProperty == null || doubleProperty == null) {
throw new NullPointerException();
}
BidirectionalBinding binding = new BidirectionalBinding(stringProperty, doubleProperty);
stringProperty.addListener(binding);
doubleProperty.addListener(binding);
}
public static void unbindBidirectional(StringProperty stringProperty, DoubleProperty doubleProperty) {
if (stringProperty == null || doubleProperty == null) {
throw new NullPointerException();
}
// The equals(Object) method of BidirectionalBinding was overridden to take into
// account only the properties. This means that the listener will be removed even
// though it isn't the *same* (==) instance.
BidirectionalBinding binding = new BidirectionalBinding(stringProperty, doubleProperty);
stringProperty.removeListener(binding);
doubleProperty.removeListener(binding);
}
private static class BidirectionalBinding implements ChangeListener<Object>, WeakListener {
private final WeakReference<StringProperty> stringRef;
private final WeakReference<DoubleProperty> doubleRef;
// Need to cache it since we can't hold a strong reference
// to the properties. Also, a changing hash code is never a
// good idea and it needs to be "insulated" from the fact
// the properties can be GC'd.
private final int cachedHashCode;
private boolean updating;
private BidirectionalBinding(StringProperty stringProperty, DoubleProperty doubleProperty) {
stringRef = new WeakReference<>(stringProperty);
doubleRef = new WeakReference<>(doubleProperty);
cachedHashCode = Objects.hash(stringProperty, doubleProperty);
}
@Override
public boolean wasGarbageCollected() {
return stringRef.get() == null || doubleRef.get() == null;
}
@Override
public void changed(ObservableValue<?> observable, Object oldValue, Object newValue) {
if (!updating) {
StringProperty stringProperty = stringRef.get();
DoubleProperty doubleProperty = doubleRef.get();
if (stringProperty == null || doubleProperty == null) {
if (stringProperty != null) {
stringProperty.removeListener(this);
}
if (doubleProperty != null) {
doubleProperty.removeListener(this);
}
} else {
updating = true;
try {
if (observable == stringProperty) {
updateDoubleProperty(doubleProperty, (String) newValue);
} else if (observable == doubleProperty) {
updateStringProperty(stringProperty, (Number) newValue);
} else {
throw new AssertionError("How did we get here?");
}
} finally {
updating = false;
}
}
}
}
private void updateStringProperty(StringProperty property, Number newValue) {
if (newValue != null) {
property.set(Double.toString(newValue.doubleValue()));
} else {
// set the property to a default value such as 0.0?
property.set("0.0");
}
}
private void updateDoubleProperty(DoubleProperty property, String newValue) {
if (newValue != null) {
try {
property.set(Double.parseDouble(newValue));
} catch (NumberFormatException ignore) {
// newValue is not a valid double
}
}
}
@Override
public int hashCode() {
return cachedHashCode;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
StringProperty stringProperty1 = stringRef.get();
DoubleProperty doubleProperty1 = doubleRef.get();
if (stringProperty1 == null || doubleProperty1 == null) {
return false;
}
if (obj instanceof BidirectionalBinding) {
BidirectionalBinding other = (BidirectionalBinding) obj;
StringProperty stringProperty2 = other.stringRef.get();
DoubleProperty doubleProperty2 = other.doubleRef.get();
if (stringProperty2 == null || doubleProperty2 == null) {
return false;
}
return stringProperty1 == stringProperty2 && doubleProperty1 == doubleProperty2;
}
return false;
}
}
}