MouseEvent MouseEvent - 2 months ago 6
Java Question

Reject changes out of bound range on DoubleProperty

I was recently trying to constrain the range my double property to

(0.0 - 1.0)
. Changes out of this range shouln't have any effect, or throw an error.

Currently I did it with either 2 ways:


  • Read only, and changes allowed on accesors only:

    private ReadOnlyDoubleWrapper numProperty = new ReadOnlyDoubleWrapper(0);

    public final double getNum() {
    return numProperty().get();
    }

    public final void setNum(double num) {
    if(num < 0)
    num = 0;
    if(num > 1)
    num = 1;
    numProperty.set(num);
    }

    public ReadOnlyDoubleProperty numProperty() {
    return numProperty.getReadOnlyProperty();
    }

  • Setting listeners to reset value:

    private DoubleProperty numProperty = new SimpleDoubleProperty(0);
    numProperty.addListener(new BoundRangeListener(numProperty, 0.0, 1.0));


    class BoundRangeListener implements ChangeListener<Number> {

    Property<Number> prop;
    double min, max;

    BoundRangeListener(Property<? extends Number> prop, double min, double max) {
    this.prop = prop;
    this.min = min;
    this.max = max;
    }

    public void changed(ObservableValue<? extends Number> o,
    Number oldValue, Number newValue) {
    if(newValue.doubleValue() < min)
    prop.set(min);
    if(newValue.doubleValue() > max)
    prop.set(max);

    // Optional: throw exception
    }

    }



Still, I'm not comfortable with neither of these solutions. The first won't allow my users to bind the property; not something I want, without a good reason. The second would fire 2 events to other listeners; no good.

So my question: Is there any out-of-the-box way to reject a change, or bound the number property to a range?

Answer

Consider subclassing SimpleDoubleProperty and overriding the set, setValue, and bind methods:

import javafx.beans.binding.Bindings;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.value.ObservableValue;

public class ClampedDoublePropertyTest {

    private DoubleProperty numProperty = new SimpleDoubleProperty(0) {

        @Override
        public void set(double value) {

            System.out.println("set("+value+")");
            if (value < 0) {
                value = 0 ;
            }
            if (value > 1) {
                value = 1 ;
            }
            super.set(value);
        }

        @Override
        public void setValue(Number value) {
            System.out.println("setValue("+value+")");
            if (value == null) {
                // depending on requirements, throw exception, set to default value, etc.
            } else {
                if (value.doubleValue() < 0) {
                    value = new Double(0);
                }
                if (value.doubleValue() > 1) {
                    value = new Double(1);
                }
                super.setValue(value);
            }
        }

        @Override
        public void bind(ObservableValue<? extends Number> obs) {
            super.bind(Bindings.createObjectBinding(
                    () -> Math.min(1, Math.max(0, obs.getValue().doubleValue())),
                    obs));
        }

    };

    public DoubleProperty numProperty() {
        return numProperty;
    }

    public final double getNum() {
        return numProperty().get();
    }

    public final void setNum(double num) {
        numProperty().set(num);
    }

    public static void main(String[] args) {
        ClampedDoublePropertyTest test = new ClampedDoublePropertyTest();
        test.numProperty().addListener((obs, oldValue, newValue) -> System.out.println(oldValue +" -> "+newValue));

        DoubleProperty value = new SimpleDoubleProperty();
        test.numProperty().bind(value);
        value.set(0.5);
        value.set(2);
        value.set(-5);
    }
}

This will allow client code to bind the property in the usual way:

myInstance.numProperty().bind(...);

though note that this will violate the usual binding contract, as if the observable value to which it is bound is out of range, the two values will not be equal.

As an aside, FWIW Oracle recommend (slide 29) the listener approach, though I agree with you that I don't like it (particularly as it allows an observer to observe an "invalid value").