Skip to content

Commit e204c63

Browse files
committed
PoC: Fluent bindings for ObservableValue
1 parent e23a2fe commit e204c63

File tree

6 files changed

+402
-3
lines changed

6 files changed

+402
-3
lines changed

modules/javafx.base/src/main/java/javafx/beans/binding/ObjectBinding.java

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,25 @@ public abstract class ObjectBinding<T> extends ObjectExpression<T> implements
6565
private boolean valid = false;
6666
private BindingHelperObserver observer;
6767
private ExpressionHelper<T> helper = null;
68+
private boolean remainInvalidWhenNotObserved;
6869

6970
/**
70-
* Creates a default {@code ObjectBinding}.
71+
* Constructs a new instance.
72+
*
73+
* @param remainInvalidWhenNotObserved when {@code true} creates a binding
74+
* which, whenever its value is computed, remains invalid when not
75+
* observed, otherwise always becomes valid.
76+
*/
77+
public ObjectBinding(boolean remainInvalidWhenNotObserved) {
78+
this.remainInvalidWhenNotObserved = remainInvalidWhenNotObserved;
79+
}
80+
81+
/**
82+
* Constructs a new instance which will always become valid when its value
83+
* is computed.
7184
*/
7285
public ObjectBinding() {
86+
this(false);
7387
}
7488

7589
@Override
@@ -155,7 +169,10 @@ public ObservableList<?> getDependencies() {
155169
public final T get() {
156170
if (!valid) {
157171
value = computeValue();
158-
valid = true;
172+
173+
if (!remainInvalidWhenNotObserved || helper != null) {
174+
valid = true;
175+
}
159176
}
160177
return value;
161178
}
@@ -182,6 +199,17 @@ public final boolean isValid() {
182199
return valid;
183200
}
184201

202+
/**
203+
* Returns {@code true} when this binding currently has one or more
204+
* listeners, otherwise {@code false}.
205+
*
206+
* @return {@code true} when this binding currently has one or more
207+
* listeners, otherwise {@code false}
208+
*/
209+
protected boolean isObserved() {
210+
return helper != null;
211+
}
212+
185213
/**
186214
* Calculates the current value of this binding.
187215
* <p>
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package javafx.beans.value;
2+
3+
import java.util.function.Function;
4+
5+
import javafx.beans.InvalidationListener;
6+
import javafx.beans.property.ObjectProperty;
7+
import javafx.beans.property.SimpleObjectProperty;
8+
9+
/**
10+
* Support class which supplies varies types of bindings.
11+
*/
12+
class Bindings {
13+
14+
public static <T, U> ObservableValue<U> mapping(ObservableValue<T> source, Function<? super T, ? extends U> mapper) {
15+
return nullableMapping(source, v -> v == null ? null : mapper.apply(v));
16+
}
17+
18+
public static <T> ObservableValue<T> conditional(ObservableValue<T> source, ObservableValue<Boolean> condition) {
19+
return new FlatMapBinding<>(condition, new ConditionalMapper<>(source), null);
20+
}
21+
22+
public static <T, U> ObservableValue<U> flatMapping(ObservableValue<T> source, Function<? super T, ? extends ObservableValue<? extends U>> mapper) {
23+
return new FlatMapBinding<>(source, mapper, () -> null);
24+
}
25+
26+
public static <T, U> ObservableValue<U> nullableMapping(ObservableValue<T> source, Function<? super T, ? extends U> mapper) {
27+
return new LazyObjectBinding<>() {
28+
@Override
29+
protected Subscription observeInputs() {
30+
InvalidationListener listener = obs -> invalidate();
31+
32+
source.addListener(listener); // start observing source
33+
34+
return () -> source.removeListener(listener);
35+
}
36+
37+
@Override
38+
protected U computeValue() {
39+
return mapper.apply(source.getValue());
40+
}
41+
};
42+
}
43+
44+
private static class ConditionalMapper<T> implements Function<Boolean, ObservableValue<? extends T>> {
45+
private final ObservableValue<T> source;
46+
47+
private ObjectProperty<T> lastValue;
48+
49+
ConditionalMapper(ObservableValue<T> source) {
50+
this.source = source;
51+
}
52+
53+
@Override
54+
public ObservableValue<? extends T> apply(Boolean v) {
55+
if(Boolean.TRUE.equals(v)) {
56+
lastValue = null;
57+
58+
return source;
59+
}
60+
61+
if(lastValue == null) {
62+
lastValue = new SimpleObjectProperty<>(source.getValue());
63+
}
64+
65+
return lastValue;
66+
}
67+
}
68+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package javafx.beans.value;
2+
3+
import java.util.Objects;
4+
import java.util.function.Function;
5+
import java.util.function.Supplier;
6+
7+
class FlatMapBinding<S, T> extends LazyObjectBinding<T> {
8+
private final ObservableValue<S> source;
9+
private final Function<? super S, ? extends ObservableValue<? extends T>> mapper;
10+
private final Supplier<? extends T> nullSupplier;
11+
12+
private Subscription mappedSubscription = Subscription.EMPTY;
13+
14+
public FlatMapBinding(
15+
ObservableValue<S> source,
16+
Function<? super S, ? extends ObservableValue<? extends T>> mapper,
17+
Supplier<? extends T> nullSupplier
18+
) {
19+
this.source = Objects.requireNonNull(source);
20+
this.mapper = Objects.requireNonNull(mapper);
21+
this.nullSupplier = nullSupplier;
22+
}
23+
24+
@Override
25+
protected T computeValue() {
26+
S value = source.getValue();
27+
boolean skipNull = value == null && nullSupplier != null;
28+
ObservableValue<? extends T> mapped = skipNull ? null : mapper.apply(value);
29+
30+
if(isObserved()) {
31+
mappedSubscription.unsubscribe();
32+
mappedSubscription = mapped == null ? Subscription.EMPTY : mapped.subscribeInvalidations(this::invalidate);
33+
}
34+
35+
return skipNull ? nullSupplier.get()
36+
: mapped == null ? null
37+
: mapped.getValue();
38+
}
39+
40+
@Override
41+
protected Subscription observeInputs() {
42+
Subscription subscription = source.subscribeInvalidations(this::invalidate);
43+
44+
return () -> {
45+
subscription.unsubscribe();
46+
mappedSubscription.unsubscribe();
47+
mappedSubscription = Subscription.EMPTY;
48+
};
49+
}
50+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package javafx.beans.value;
2+
3+
import javafx.beans.InvalidationListener;
4+
import javafx.beans.binding.ObjectBinding;
5+
6+
/**
7+
* Extends {@link ObjectBinding} with the ability to lazily register
8+
* and eagerly unregister listeners on its dependencies.
9+
*
10+
* @param <T> the type of the wrapped {@code Object}
11+
*/
12+
public abstract class LazyObjectBinding<T> extends ObjectBinding<T> {
13+
private Subscription subscription;
14+
private boolean wasObserved;
15+
16+
public LazyObjectBinding() {
17+
super(true); // this toggles a code path in ObjectBinding to make lazy listener registration possible; can be solved better if accepted
18+
}
19+
20+
@Override
21+
public void addListener(ChangeListener<? super T> listener) {
22+
super.addListener(listener);
23+
24+
updateSubcription();
25+
}
26+
27+
@Override
28+
public void removeListener(ChangeListener<? super T> listener) {
29+
super.removeListener(listener);
30+
31+
updateSubcription();
32+
}
33+
34+
@Override
35+
public void addListener(InvalidationListener listener) {
36+
super.addListener(listener);
37+
38+
updateSubcription();
39+
}
40+
41+
@Override
42+
public void removeListener(InvalidationListener listener) {
43+
super.removeListener(listener);
44+
45+
updateSubcription();
46+
}
47+
48+
private void updateSubcription() {
49+
boolean isObserved = isObserved();
50+
51+
if(!wasObserved && isObserved) { // was first observer registered?
52+
subscription = observeInputs(); // start observing source
53+
get(); // make binding valid as we haven't been tracking the source until now
54+
}
55+
if(wasObserved && !isObserved) { // was last observer unregistered?
56+
subscription.unsubscribe();
57+
subscription = null;
58+
}
59+
60+
wasObserved = isObserved;
61+
}
62+
63+
/**
64+
* Called when this binding was previously not observed and a new observer was added. Implementors
65+
* must return a {@link Subscription} which will be cancelled when this binding no longer has any
66+
* observers.
67+
*
68+
* @return a {@link Subscription} which will be cancelled when this binding no longer has any observers, never null
69+
*/
70+
protected abstract Subscription observeInputs();
71+
}

0 commit comments

Comments
 (0)