After Swing, JavaFX 1.x and the iPad, now JavaFX 2.0 is candidate for the calendar picker shake down. Using MigLayout I’ve setup the basic day picking logic, but currently am stuck in build b21, because the events on the selected property in ToggleButton do not tell me which togglebutton was actually pressed, which is quite handy if you have 42 of them on screen. So I decided to focus a bit on binding instead.
My binding experience comes from JGoodies used in connecting Swing components to business model bean properties, usually using JGoodies’ BeanAdapter (which takes away the need to rebind every single property when the business model bean is changed underneath a Swing screen). Since I’m primary focussed on JavaFX’s controls at the moment, I wanted to see if I could bind my calendar picker to a business model. I’m also going to assume future 2.0 in which the properties on the business model also are using JavaFX properties. So I constructed a dummy business model with one property:
class BusinessModelBean { final public ObjectProperty iCalendarObjectProperty= new ObjectProperty(); }
This class is then used in a stage:
public class XCalendarPickerTest1 extends Application { public static void main(String[] args) { Launcher.launch(XCalendarPickerTest1.class, args); } @Override public void start(Stage stage) { // add a node XCalendarPicker lXCalendarPicker = new XCalendarPicker(); // create scene Scene scene = new Scene(lXCalendarPicker, 600, 300); // bind Picker to BusinessModelBean BusinessModelBean lBusinessModelBean = new BusinessModelBean(); // binding here... // create stage stage.setTitle("XCalendarPicker"); stage.setScene(scene); stage.setVisible(true); }
Initially I attempted to uses JavaFX’s binding features and bound the UI property to the BM property:
lXCalendarPicker.calendar().bind( lBusinessModelBean.iCalendarObjectProperty );
This immediately gave an exception: “A bound value cannot be set.”, because JavaFX’s binding is one way and in this case the picker’s property is bound to the BM’s. Clicking on a button in the picker, sets the picker’s property and this binding doesn’t allow that. Reversing the binding makes things better:
lBusinessModelBean.iCalendarObjectProperty.bind(lXCalendarPicker.calendar());
Now clicking in the picker updates the BM’s property. However, this means that the BM’s property is blocking any direct changes. I do not know how your business model works, but in my business model it is fairly natural behavior that the BM changes its own properties. For example if a time range was selected by the user, and he changes the duration, then one of the dates at the end would need to change as well and that in fact should update the UI again.
This leads me to the conclusion that business binding requires two way binding and that the binding in JavaFX, being one way, is not well suited for this kind of binding. So this meant the creation of a Binding class which uses the InvalidationListener to bind two properties:
public class Binding { /** * Setup two way binding * @param v1 * @param v2 */ static public void bind(final ObjectProperty v1, final ObjectProperty v2) { // listen to changes on the "left" side v1.addListener(new InvalidationListener() { @Override public void invalidated(ObservableValue<!--? extends Calendar--> arg0) { sync(v1, v2); } }); // listen to changes on the "right" side v2.addListener(new InvalidationListener() { @Override public void invalidated(ObservableValue<!--? extends Calendar--> arg0) { sync(v2, v1); } }); } /** * Execute a one way binding, v1 to v2, if required. * * @param v1 * @param v2 * @return */ static public boolean sync(final ObjectProperty v1, final ObjectProperty v2) { // if there is a change if ( (v1.getValue() == null && v2.getValue() != null ) || (v1.getValue() != null && v2.getValue() == null ) || (v1.getValue() != null && v2.getValue() != null && !v1.getValue().equals(v2.getValue())) ) { // sync v2 with v1 System.out.println("Syncing " + quickFormatCalendar(v1.getValue())); v2.set(v1.get()); return true; } return false; } }
And this works quite well, setting either side of the binding will update the other side. Great! But as you can see, the code is written specifically for the ObjectProperty type, because the sync logic requires access to three main features to work its magic:
- get value
- set value
- register as a listener for value changes
It turns out that the actual property classes do not extend any common interface that provides all three. As of now it is not possible to write a method accepting a generic property, and the code above would need to use reflection to access the methods. Not a big problem, but unexpected. I also believe that this will cause more confusing in the future.
Let’s continue, again I don’t know how you implement your business model, but mine is supposed to be unaware of whatever is using it. So if you have restrictions on a property; say min should be <= max, or in this test case where I will not allow odd dates to be selected, the BM can only fire an exception telling the other side that what it is trying to do is not alright. Normally, using Java beans, you’d implement these restrictions in the setter of the property. However, JavaFX’s properties are accessed directly and the setters are mere compatibility cosmetics, so you need a different way to get between the call and the actual setting of the value. One way would be to use an anonymous inner class:
class BusinessModelBean { final public ObjectProperty iCalendarObjectProperty = new ObjectProperty() { @Override public void set(Calendar value) { if (value != null && value.get(Calendar.DATE) % 2 == 1) { throw new IllegalArgumentException("odd date"); } super.set(value); } }; }
Using this together with my binding code naturally will fail, because the binding logic does not know how to deal with the exception. The idea is that if the setting of a property is not successful, a sync-back is done, resetting the original value. Extending the binding code to do this, is not very hard. In fact, it is wise to always start a sync-back, just in case the setter did something to the value. In 99% of the cases the equals check will make sure no sync is performed.
/** * Setup two way binding * @param v1 * @param v2 */ static public void bind(final ObjectProperty v1, final ObjectProperty v2) { // listen to changes on the "left" side v1.addListener(new InvalidationListener() { @Override public void invalidated(ObservableValue<!--? extends Calendar--> arg0) { try { // sync left to right sync(v1, v2); } finally { // always sync back sync(v2, v1); } } }); // listen to changes on the "right" side v2.addListener(new InvalidationListener() { @Override public void invalidated(ObservableValue<!--? extends Calendar--> arg0) { try { // sync right to left sync(v2, v1); } finally { // always sync back sync(v1, v2); } } }); }
This is working fine; the exception is caught and the original value is restored. There is one catch still, JavaFX’s binding does no go through the set method. So if you’d bind the BM’s property using JavaFX’s binding (which of course is totally legal to do), the restriction is not enforced. Not good. Another approach would be to attempt some kind of vetoable listener:
iCalendarObjectProperty.addListener(new InvalidationListener() { @Override public void invalidated(ObservableValue<!--? extends Calendar--> observableValue) { Calendar value = observableValue.getValue(); if (value != null && value.get(Calendar.DATE) % 2 == 1) { System.out.println("odd date"); throw new IllegalArgumentException("odd date"); } } });
The problem here is that this listener is called after the value is set, so throwing the exception would not have any effect. The same goes for overriding the invalidated method.
There does not seem to be a way to prevent JavaFX’s binding to set a value. This has to do with the approach JavaFX is taking on binding; Richard Bair explained that they never prevent setting a value on a property. This means that properties can have wrong values (min > max for example) and that the application has to deal with that. Even if I understand why they chose that approach, I personally I do not like it, because it forces the code to deal with all kinds of combinations of situations which actually should not occur.
Summarizing the findings so far:
- JavaFX’s binding is one way while business model binding should be two way
- JavaFX’s binding has no formal way to prevent a value to be set
- JavaFX’s properties have no generic superclass or interface
This leads me to say that using JavaFX’s properties in a business model with binding is not going to work, JavaFX properties and bindings are intended to work within the UI. Luckily ony minor changes would be required to make it work, so maybe Oracle can be persuaded to make them.
As always I’d love to be smacked on the head and be proven wrong.
Update 1:
Smacked on the head by Richard himself, gotta love it; JavaFX supports a method “Bindings.bindWithInverse” which apparently supports two way binding with honoring exceptions. Naturally I will test this as soon as I have time. This does not solve my second issue of preventing a value to be set.
Update 2: The bindWithReverse indeed allows to way binding, but the issue on when to throw the exception (and actually acting on it) remains.
I don’t think it’s a big problem if JavaFX’s binding does not prevent a value to be set if you consider using JSR 303 (Bean Validation) in your model.
Java FX writes the (possibly wrong values) into your model and you validate it using something like this:
Set<ConstraintViolation> constraintViolations = validator.validate(yourBean);
// Display the violations in the UI as needed...
My biggest issues is that if you use JavaFX’s properties and event mechanism in a business model and react to property changes, then there are moment where properties have values that fall outside the correct / expected ranges. You naturally can then go in and validate the bean, maybe even resetting properties to valid ranges, but all listeners to the BM must be able to deal with these incorrect values.
Just cause it’s simple doesn’t mean it’s not super heflupl.
Pingback: Książki o JavaFX | Java-FX.pl
could you show a JavaFX example like this
http://javafx.me/files/themedgui.jpg
or this
http://www.youtube.com/watch?v=VQy1qTMc8JM
or this
http://www.youtube.com/watch?v=4Jrb5sVxkKo
or this
http://www.youtube.com/watch?v=AD2iNENEtHY
or this
http://javafx.me/files/jfx/surikov2.html#zMy
?
No I cannot, I’m only testing JFX 2.0 EA. Please start bugging someone else with these links.
Pingback: JavaFX links of the week, April 11 // JavaFX News, Demos and Insight // FX Experience
tbee,
Regarding a way to do validations before or after the “Invalidation Listener” have you thought of using the Proxy API?. You could wrap validations around a listener and your bind method won’t really change. The proxy at Pre or Post invocation could throw some Throwable. In the past I’ve separated my validation (scripting) and/or Java bean dot notation either via XML or annotations.
I can’t help but think of Griffon for these scenarios.
Since I do not have insight in the actual property code, I cannot see how exactly the JavaFX binding writes it value. It’s not via the set-method. So possibly maybe by wrapping the whole thing, you can get a finger in between.