Reputation: 184
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...)
Upvotes: 6
Views: 1487
Reputation: 155
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.
Upvotes: 0
Reputation: 6475
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:
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.
Upvotes: 1
Reputation: 6475
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).
Upvotes: 2