A DESCRIPTION OF THE PROBLEM :
When removing an invalidation listener from a property during invalidation a subsequent change listener might not be called if the change listener is (after the removal) the last listener for that property. I noticed that issue when adding a change listener to the showingProperty of a window. By calling hide I expected my change listener to fire but it didn't. I found that the issue lies with the ExpressionHelper when there is a switch from Generic to SingleChange. The currentValue is discarded in favor of the value currently held by the property.
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Run the attached test case. When you add another change listener as described in the workaround, the code runs without error.
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
A registered change listener fires, when the value of the property has changed.
ACTUAL -
A registered change listener does not always fire even if the value of the property has changed.
---------- BEGIN SOURCE ----------
import javafx.beans.InvalidationListener;
import javafx.beans.property.BooleanPropertyBase;
import javafx.beans.value.ChangeListener;
import org.junit.Test;
import java.util.concurrent.atomic.AtomicInteger;
public class JFXPropertyTest {
@Test
public void testJFXProperty() {
AtomicInteger test = new AtomicInteger(0);
ChangeListener<Boolean> testListener = (observable, oldValue, newValue) -> test.incrementAndGet();
assert test.get() == 0;
TestProperty testProperty = new TestProperty(true, (object) -> {});
testProperty.addListener(testListener);
testProperty.setValue(false);
assert test.get() == 1;
}
private static class TestProperty extends BooleanPropertyBase {
private final InvalidationListener invalidationListener;
private TestProperty(boolean value, InvalidationListener invalidationListener) {
super(value);
this.invalidationListener = invalidationListener;
addListener(invalidationListener);
}
@Override
protected void invalidated() {
if (!getValue()) {
removeListener(invalidationListener);
}
super.invalidated();
}
@Override
public Object getBean() {
return null;
}
@Override
public String getName() {
return "test";
}
}
}
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
- add another dummy change listener so that there are always at least two listeners present.
FREQUENCY : always
When removing an invalidation listener from a property during invalidation a subsequent change listener might not be called if the change listener is (after the removal) the last listener for that property. I noticed that issue when adding a change listener to the showingProperty of a window. By calling hide I expected my change listener to fire but it didn't. I found that the issue lies with the ExpressionHelper when there is a switch from Generic to SingleChange. The currentValue is discarded in favor of the value currently held by the property.
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Run the attached test case. When you add another change listener as described in the workaround, the code runs without error.
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
A registered change listener fires, when the value of the property has changed.
ACTUAL -
A registered change listener does not always fire even if the value of the property has changed.
---------- BEGIN SOURCE ----------
import javafx.beans.InvalidationListener;
import javafx.beans.property.BooleanPropertyBase;
import javafx.beans.value.ChangeListener;
import org.junit.Test;
import java.util.concurrent.atomic.AtomicInteger;
public class JFXPropertyTest {
@Test
public void testJFXProperty() {
AtomicInteger test = new AtomicInteger(0);
ChangeListener<Boolean> testListener = (observable, oldValue, newValue) -> test.incrementAndGet();
assert test.get() == 0;
TestProperty testProperty = new TestProperty(true, (object) -> {});
testProperty.addListener(testListener);
testProperty.setValue(false);
assert test.get() == 1;
}
private static class TestProperty extends BooleanPropertyBase {
private final InvalidationListener invalidationListener;
private TestProperty(boolean value, InvalidationListener invalidationListener) {
super(value);
this.invalidationListener = invalidationListener;
addListener(invalidationListener);
}
@Override
protected void invalidated() {
if (!getValue()) {
removeListener(invalidationListener);
}
super.invalidated();
}
@Override
public Object getBean() {
return null;
}
@Override
public String getName() {
return "test";
}
}
}
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
- add another dummy change listener so that there are always at least two listeners present.
FREQUENCY : always