Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

8290040: Provide simplified deterministic way to manage listeners #830

Closed
wants to merge 18 commits into from
Closed
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.sun.javafx.binding;

import java.util.Objects;

import javafx.beans.value.ObservableValue;

public class ConditionalBinding<T> extends LazyObjectBinding<T> {

private final ObservableValue<T> source;
private final ObservableValue<Boolean> nonNullCondition;

private Subscription subscription;

public ConditionalBinding(ObservableValue<T> source, ObservableValue<Boolean> condition) {
this.source = Objects.requireNonNull(source, "source");
this.nonNullCondition = Objects.requireNonNull(condition, "condition").orElse(false);

// condition is always observed and never unsubscribed
Subscription.subscribe(nonNullCondition, current -> {
invalidate();

if (!current) {
getValue();
}
});
}

/**
* This binding is valid whenever it is observed, or it is currently inactive.
* When inactive, the binding has the value of its source at the time it became
* inactive.
*/
@Override
protected boolean allowValidation() {
return super.allowValidation() || !isActive();
}

@Override
protected T computeValue() {
if (isObserved() && isActive()) {
if(subscription == null) {
subscription = Subscription.subscribeInvalidations(source, this::invalidate);
}
}
else {
unsubscribe();
}

return source.getValue();
}

@Override
protected Subscription observeSources() {
return this::unsubscribe;
}

private boolean isActive() {
return nonNullCondition.getValue();
}

private void unsubscribe() {
if (subscription != null) {
subscription.unsubscribe();
subscription = null;
}
}
}
Original file line number Diff line number Diff line change
@@ -180,7 +180,15 @@ public final void invalidate() {
valid = false;
onInvalidating();
ExpressionHelper.fireValueChangedEvent(helper);
value = null; // clear cached value to avoid hard reference to stale data

/*
* Cached value should be cleared to avoid a strong reference to stale data,
* but only if this binding didn't become valid after firing the event:
*/

if (!valid) {
value = null;
}
}
}

Original file line number Diff line number Diff line change
@@ -27,6 +27,7 @@

import java.util.function.Function;

import com.sun.javafx.binding.ConditionalBinding;
import com.sun.javafx.binding.FlatMappedBinding;
import com.sun.javafx.binding.MappedBinding;
import com.sun.javafx.binding.OrElseBinding;
@@ -251,4 +252,63 @@ default ObservableValue<T> orElse(T constant) {
default <U> ObservableValue<U> flatMap(Function<? super T, ? extends ObservableValue<? extends U>> mapper) {
return new FlatMappedBinding<>(this, mapper);
}

/**
* Returns an {@code ObservableValue} that holds this value whenever the given
* condition evaluates to {@code true}, otherwise holds the last value when
* {@code condition} became {@code false}. The value is updated whenever this
* {@code ObservableValue} changes, unless the condition currently evaluates
* to {@code false}.
* <p>
* The returned {@code ObservableValue} only observes this value when the given
* {@code condition} evaluates to {@code true}. This allows this {@code ObservableValue}
* and the conditional {@code ObservableValue} to be garbage collected if neither is
* otherwise strongly referenced when {@code condition} becomes {@code false}.
* <p>
* A currently observed binding will observe its source, which means it will not be eligible
* for garbage collection while source isn't. However, using {@code when} this {@code ObservableValue}
* can still be eligible for garbage collection when the condition is {@code false} and the
* conditional itself is also eligible for garbage collection.
* <p>
* Returning {@code null} from the given condition is treated the same as
* returning {@code false}.
* <p>
* For example:
* <pre>{@code
* ObservableValue<Boolean> condition = new SimpleBooleanProperty(true);
* ObservableValue<String> globalProperty = new SimpleStringProperty("A");
* ObservableValue<String> whenProperty = property.when(condition);
*
* // observe whenProperty, which will in turn observe globalProperty
* whenProperty.addChangeListener((ov, old, current) -> System.out.println(current));
*
* globalProperty.setValue("B"); // "B" is printed
*
* condition.setValue(false);
*
* // After condition becomes false, whenProperty stops observing globalProperty; condition
* // and whenProperty may now be eligible for GC despite being observed by the ChangeListener
*
* globalProperty.setValue("C"); // nothing is printed
* globalProperty.setValue("D"); // nothing is printed
*
* condition.setValue(true); // globalProperty is observed again, and "D" is printed
* }</pre>
* Another example:
* <pre>{@code
* Label label = ... ;
* ObservableValue<String> globalProperty = new SimpleStringProperty("A");
*
* // bind label's text to a global property only when it is shown:
* label.textProperty().bind(globalProperty.when(label::isShownProperty));
* }</pre>
* @param condition a boolean {@code ObservableValue}, cannot be {@code null}
* @return an {@code ObservableValue} that holds this value whenever the given
* condition evaluates to {@code true}, otherwise holds the last seen value;
* never returns {@code null}
* @since 20
*/
default ObservableValue<T> when(ObservableValue<Boolean> condition) {
return new ConditionalBinding<>(this, condition);
}
}
Original file line number Diff line number Diff line change
@@ -52,6 +52,24 @@ void shouldBeInvalidInitially() {
assertFalse(binding.isValid());
}

@Test
void invalidationWhichBecomesValidDuringCallbacksShouldReturnCorrectValue() {
LazyObjectBindingStub<String> binding = new LazyObjectBindingStub<>() {
@Override
protected String computeValue() {
return "A";
}
};

binding.addListener(obs -> {
assertEquals("A", binding.get());
});

binding.invalidate(); // becomes valid again immediately

assertEquals("A", binding.get());
}

@Nested
class WhenObservedWithInvalidationListener {
private InvalidationListener invalidationListener = obs -> {};
Loading