Restrict which values will be settleable in an javafx property?
Asked Answered
A

3

6

What's the best way to keep the value of a javafx Property within specific bounds?

(Or - is this bad practice, existing any reason to never filter values wrapped by javafx properties?)

Example1: avoid negative values in an IntegerProperty
Example2: keep the value of an IntegerProperty within the bounds of a List

First idea: - override IntegerPropertyBase.set(int). It's safe? Actually setValue(int) only calls set(int), but - if this implementation one day changes - the control over the values set goes lost.

Second idea: - override IntegerPropertyBase.invalidate(). But at this point the value already was set.

Will it fit better to javafx properties throw an IllegalArgumentException (or an ArrayIndexOutOfBoundsException, if the wrapped value is the index of an array), or better refuse the value out of bounds, setting back the last value in bounds?

Maybe like this:

    class BoundedIntegerProperty extends IntegerPropertyBase {
        (...)
        int oldValue = defaultValueInBounds;
        boolean settingOldValue = false;
        public void invalidated() {
            if(!settingOldValue){
                if(outOfBounds(get())){
                    settingOldValue = true;
                    set(oldValue);
                } else {
                    oldValue = get();
                }
            } else
                settingOldValue = false;
        }
    }

Only throw an Exception in invalidated() for values out of bounds may keep the value of the property out of bounds.

Have I overlooked anything in javafx properties provided to filter values?

(If necessary, please help me improving the possibly bad english of this text...)

Azine answered 11/7, 2013 at 10:19 Comment(2)
It might be simpler to validate the input before setting the property e.g. validate the text field, DB result or data from a file. I can't see any nice way of handling this, would you really want any #set() call to throw an exception?Cermet
You are true: it doesn't make sense to set, for example, an index for a list without have access to this list. But if I have access to the list, the setters for the index property may be in the list itself, and the property may be read only. In the List the setter of the index may throw exceptions if the index is out of bounds.Azine
S
2

In both your examples, there seemed to be a logical default value (eg. if it's required to be positive, negative numbers turn into 0). Assuming you document that well (what the defaults are if the value is invalid), I think your first approach seems like it's on the right path.

I'd recommend starting with a concrete class like SimpleIntegerProperty as the class you're extending (unless there's some reason you chose IntegerPropertyBase instead.

I would then overwrite both the set(int) method and the setValue(Number) method, wrapping the parent in your logic:

    /**
     * Explanation that values under 0 are set to 0
     */
    @Override
    public void set(int value){
        super.set(value > 0 ? value : 0);
    }

    /**
     * Explanation that values under 0 are set to 0
     */
    @Override
    public void setValue(Number value){
        super.setValue(value.intValue() > 0 ? value : 0);
    }

There may be a case where there isn't logical default values (or you just want to reject invalid values). That case makes it a bit harder - you'd actually want to use a method signature of like this so the caller knows if the value changed:

public boolean set(int value)

In order to do that, you'll have to go back quite a few classes - all the way back to ReadOnlyIntegerProperty and implement the setting / invalidating structure yourself.

I would hesitate to use Exceptions to handle the invalid input. It is a legitimate use of exceptions, but my fear is that the Exception would be relied on for validation. Exceptions are very resource intensive, and should only be hit if there's something that needs to be fixed. So it's really about your intentions and how much you trust people using your class to do the right thing (and validate before sending to you).

Spitter answered 17/7, 2013 at 17:8 Comment(1)
Of course: if input may be illegal, I need a boolean function to check if an input would be illegal. That's not contained in Javafx properties.Azine
S
1

I believe I understand what you're shooting for better now. You're looking to perform user input validation.

When you're doing your user validation, there's really two ways to approach it:

  1. Validate immediately after any change takes place and provide feedback
  2. Validate when the focus leaves the input area

With both, you'll be using property listeners - it's just a matter of what property listener you're dealing with.

In the first case you'll listen directly to the property you're validating:

    TextField field = new TextField();
    field.textProperty().addListener(new ChangeListener<String>(){
        @Override
        public void changed(ObservableValue<? extends String> value,
                String oldValue, String newValue) {
                //Do your validation or revert the value
        }});

In the second case, you'll listen to the focused property, and validate when focus is lost (you can maintain the last validated value in this listener to help revert the value if necessary):

    TextField field = new TextField();
    field.focusedProperty().addListener(new ChangeListener<Boolean>(){
        String lastValidatedValue = "";
        @Override
        public void changed(ObservableValue<? extends Boolean> value,
                Boolean oldValue, Boolean newValue) {
            if(newValue == false && oldValue == true){
                //Do your validation and set `lastValidatedValue` if valid
            }
        }});

Note: I was assuming you just wanted to put in a fail safe for system code updating the UI. I'll leave my previous answer as I believe it provides useful information as well.

Spitter answered 30/7, 2013 at 20:34 Comment(1)
If the validation returns false and I revert the Property to oldValue, the validation will restart with the old value as "new". If the validation of oldValue returns false too (maybe a value contained in the Property before the validation listener was added), we get into a loop. In this case it will be safer to validate oldValue too before revert to them.Azine
L
0

I know this is an old post, but I was looking for something like this, so I checked out the source code for javafx.scene.control.Slider which has a value property that is always kept within [min,max]. It does this by clipping the value in the method invalidated, which is called when the property is marked as invalid, just before invalidation listeners are notified. My original answer was to suggest this method. However, I realized that there is a problem with this approach: bindings. Long story short, they make it difficult to always make sure that the property stays valid without breaking expected behavior for other methods in the Property class/interface hierarchy.

There are two kinds of bindings, unidirectional bindings and bidirectional bindings. If you bind the value property with a unidirectional binding (method bind() in interface Property) it can no longer be changed directly, and thus no longer validated, unless you remove the binding as soon as you detect it. While this might technically work, it would break the contract of the Property interface as you are implicitly disabling one of its methods. From a client's perspective this would cause strange behavior. Indeed, the Slider class doesn't remove unidirectional bindings from the value property, but opts for allowing the value to become invalid instead (so it doesn't solve the original problem).

With bidirectional bindings (method bindDirectional() in several classes including DoubleProperty) it's arguably worse, since you can't remove the binding unless you have a reference to the second property of the binding, which you can't obtain from the first. That leaves you with two options. The first, which is the case for Slider, is to resist the binding and force the value to stay valid. In the current implementation of bindDirectional() this causes the second property to diverge whenever it goes outside the bounds (again breaking expected behavior of a supertype). I think this choice is really bad, it basically means that bindDirectional() can just stop working without throwing any errors or otherwise notifying the caller. The second option is to allow the binding to work and possibly make the value invalid (again not solving the problem).

Under the circumstances, I think the safest solution is use a read-only property whenever you need to validate the value. There are convenience classes (such as ReadOnlyDoubleWrapper) that lets you keep a writable version of your property in your class and expose a read-only version (that can't be set or bound in any way) to outside code. You can then make a custom setter that validates before setting. Alternatively, if you want to support some binding functionallity, you could make a second property, named something like requestedValue which can take any value (and be bound). Then, whenever requestedValue becomes valid you could update value using a change listener.

Lempira answered 23/10 at 10:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.