JavaFX - bidirectional binding either doesn't work or stops working

Viewed 212

I'm working on a Sudoku game project. I have javafx buttons representing 81 boxes, stored in boardButtons list and userBoard which is an object of my other class (doesn't matter now I think). Method getField() returns object field which stores value of this sudoku field. I want to use bidirectional binding (it needs to be like that), so that when I select a box to and input some value, it not only shows in this box, but is also saved in userBoard's Field and vice versa.

When I run the app, after entering some values to the boxes, they stop being saved to userBoard's field. Sometimes it happens right at the beginning and sometimes I manage to enter a few numbers. I've searched for solution online but I have no idea why that happens.

StringConverter converter = new IntegerStringConverter();

    boardButtons.forEach((button) -> {
        try {
            index = boardButtons.indexOf(button);
            IntegerProperty fieldValueProperty = new JavaBeanIntegerPropertyBuilder()
                    .bean(userBoard.getField(index))
                    .name("fieldValue").build();

            button.textProperty().bindBidirectional(fieldValueProperty, converter);

            fieldValueProperty.addListener(new ChangeListener<Number>() {
                @Override
                public void changed(ObservableValue<? extends Number> ov, Number t, Number t1) {
                    if (gameStarted) {
                        System.out.println("fieldValueProperty changed: " + fieldValueProperty);
                    }
                }
            });
        } catch (NoSuchMethodException ex) {
            ex.printStackTrace();
        }
    });
1 Answers

While there's not really enough information to offer a proper solution, I can explain why your bidirectional binding will eventually stop working.

TL;DR: Your fieldValueProperty is being garbage collected.

The core libraries of JavaFX attempt to prevent memory leaks as much as possible. This typically involves the use of weak references in the implementation of listeners/bindings. Also, the current implementation of bidirectional bindings is done via a third, behind-the-scenes class to which you have no access. This behind-the-scenes class only maintains weak references to both properties involved in the binding. This prevents one property from holding the other in memory, which is important because you can't undo a bidirectional binding if you only have a reference to one of the properties (unlike unidirectional bindings).

Basically, this is a long-winded way to say that you are not maintaining a strong reference to fieldValueProperty and thus it is likely to be garbage collected at some point. You may think that:

fieldValueProperty.addListener(new ChangeListener<Number>() {
    @Override
    public void changed(ObservableValue<? extends Number> ov, Number t, Number t1) {
        if (gameStarted) {
            System.out.println("fieldValueProperty changed: " + fieldValueProperty);
        }
    }
});

Has a strong reference to fieldValueProperty via the listener, but notice that you're adding the listener to fieldValueProperty itself. In other words, it's a self-reference which does not prevent garbage collection. An obvious, though probably naive, solution is to simply add a strong reference to fieldValueProperty. But consider having your model class expose a JavaFX property—rather than using a JavaBean adapter—and binding to that property instead; as long as the model class is in memory so will be the property, and I believe that would be the more elegant solution.


Note that unidirectional bindings don't suffer the same problems. When you bind a Property to an ObservableValue the former maintains a strong reference to the latter, but not vice versa. This is okay because you can invoke unbind() to remove the reference yourself.

Related