0

I am trying to practice my binding. I want to make a window with two spinners and just get them to show the same value.

The problem is that the binding is unreliable: the spinners seem to stop updating each other if they are clicked too fast. Both spinners still respond to clicking, and clicks are counted correctly on the spinner being clicked, but the other spinner stops updating and never recovers. And it doesn’t have to be very fast clicking at all to cause it.

My code is based on this answer (currently scoring 8) showing how to bind a spinner to an IntegerProperty. That answer is enough to produce the behaviour I am struggling with, but I followed the brief discussion that ensued under that answer. It has other options shown and resources linked, including a hint to use .asObject() and strong references.

I am on oracle jdk 1.8.0_251, only because I was unable to get javaFX going under other JDKs I’ve tried. If it matters I am in eclipse 2019-12 on linux. Also, the spinners don't update each other at all in Debug mode, but they work (to start with) if I Run my code.

Why does it happen? My guesses are:

  1. I'm still suffering from garbage collection etc
  2. Something isn't keeping up with the fast clicking: my laptop's hardware, the linux OS, Eclipse, my jdk, javafx?

Can I somehow protect my code from this? Can I limit the clicking so that the spinner clicked only reacts to clicks if they are slow enough to keep the binding?

Please let me know if the behaviour described is unclear and I will upload a screen recording to youtube or something.

public class BindTwoSpinners extends Application {

@Override
public void start(Stage primaryStage) throws IOException {
    
    // Set up the topSpinner
    Spinner<Integer> topSpinner = new Spinner<Integer> (1,12,1);
    topSpinner.setEditable(true);
    ObjectProperty<Integer> topObjectProp = new SimpleObjectProperty<>(1);
    IntegerProperty topIntegerProperty = IntegerProperty.integerProperty(topObjectProp);
    topSpinner.getValueFactory().valueProperty().bindBidirectional(topObjectProp);
    
    // Set up the bottomSpinner
    Spinner<Integer> bottomSpinner = new Spinner<Integer> (1,12,1);
    bottomSpinner.setEditable(true);
    ObjectProperty<Integer> bottomObjectProp = new SimpleObjectProperty<>(1);
    IntegerProperty bottomIntegerProperty = IntegerProperty.integerProperty(bottomObjectProp);
    bottomSpinner.getValueFactory().valueProperty().bindBidirectional(bottomObjectProp);         

    // Need to keep the reference as bidirectional binding uses weak references
    // https://docs.oracle.com/javase/8/javafx/api/javafx/beans/property/IntegerProperty.html#bindBidirectional-javafx.beans.property.Property-

    ObjectProperty<Integer> topIntegerPropertyAsObject = topIntegerProperty.asObject();
    ObjectProperty<Integer> bottomIntegerPropertyAsObject = bottomIntegerProperty.asObject();

    // Bind the two spinners
    bottomSpinner.getValueFactory().valueProperty(). bindBidirectional(topIntegerPropertyAsObject);

    VBox root = new VBox(10, topSpinner, bottomSpinner);
    root.setAlignment(Pos.CENTER);
    primaryStage.setScene(new Scene(root));
    primaryStage.show();
}

public static void main(String[] args) {
    launch(args);
}
pateksan
  • 160
  • 1
  • 12
  • 1
    Bidirectional bindings do not keep strong references to each other. It's possible, since you're using intermediate properties, that something is being garbage collected and that's why the bindings stop working. You seem to understand this based on a code comment; however, you don't appear to keep the necessary strong references. – Slaw Dec 27 '20 at 05:40
  • @Slaw _You seem to understand this_ - well, _seem_ is the right word! From my understanding, every property I ever used is already strongly referenced in my code, is that not the case? I will read up some more, but it would be great if you were able to spot and tell me what is missing. – pateksan Dec 27 '20 at 10:33
  • Your intermediate properties are local variables, which fall out of scope once the `start` method exits. When that happens the objects they referenced are eligible for garbage collection (since bidirectional bindings use _weak_ references). – Slaw Dec 27 '20 at 10:35
  • @Slaw Ok, now I'm puzzled. I think I know what you mean but I thought all my code is inside the brackets of the `start` method and it only exits when the window is closed? – pateksan Dec 27 '20 at 10:47
  • 1
    The `start` method exits after the `primaryStage.show()` call returns in your code. The `show()` method does not wait for the window to be closed. Now, the _application_ won't exit until the last window is closed (assuming `implicitExit` is `true`, as it is by default), but that is different from the `start` method. The `start` method is only there to "start" the application. – Slaw Dec 27 '20 at 10:50
  • @Slaw I already learnt a lot today then! Are you able to tell how to fix the problem though? Is it a matter of using modifiers? I came across [someone using the `static` modifier and saying it was a dirty workaround](https://stackoverflow.com/a/39438466/8957293) which I didn't fully understand tbh so I didn't try it for fear of unexpected results. – pateksan Dec 27 '20 at 11:16
  • For your example in your question, simply storing the property references in instance fields will probably work for you. JavaFX maintains a strong reference to your application class (it has to in order to call `stop()` when appropriate). [You want to avoid `static` because that encourages "global state" which is "evil"](https://softwareengineering.stackexchange.com/questions/148108/why-is-global-state-so-evil). If using instance fields does not work in your real code then consider providing a [mre] more representative of what you're trying to do. – Slaw Dec 27 '20 at 11:17
  • 4
    Can’t you achieve what you’re trying to do simply with `topSpinner.getValueFactory().valueProperty().bindBidirectional(bottomSpinner.getValueFactory().valueProperty())`? I think that should not suffer from the listeners getting prematurely garbage collected. – James_D Dec 27 '20 at 13:36
  • @James_D So simple and it does seem to work! I tried to "break" the binding, for a few times longer than previous attempts, they stay bound. As a bonus they stay bound in debug too: in previous attempts they never bound in debugging. And that's before I even looked into why it played up in debugging. I need to look into how exactly to use Slaw's idea with instance fields, if I can get it going than I would probably need to call that the solution (because it might helps with some of my other aches). But for now thanks man! If you fancy the rep from my upvote then please do make it answer. – pateksan Dec 28 '20 at 01:40
  • 2
    @pateksan The best way i know to test breaking them is to call `System.gc()` and see if they still work after. – James_D Dec 28 '20 at 01:43
  • @Slaw In addition to comment for James_D above, thanks! I will need to look into how to use your idea of instance fields. It seems to be another one of those basic concepts I need to grasp soon anyway, and might solve some of my other problems. So I will definitely read up, but if you are able to provide even a simple example snippet of how I would apply this here then it would keep my project going in the meantime. – pateksan Dec 28 '20 at 01:46
  • 1
    @James_D I added this and it didn't seem to break it: `topSpinner.valueProperty().addListener((obs, oldValue, newValue) -> {System.gc();});` but please let me know if that doesn't capture all opportunities to enforce garbage collection – pateksan Dec 28 '20 at 01:52

1 Answers1

0

Once the start() method is finished, your two local variables topIntegerPropertyAsObject and bottomIntegerPropertyAsObject will be garbage collected (sooner or later) and the binding won't work anymore.

These two variables should be declared as attributes in your BindTwoSpinners class and not as local variables inside the start() method.

Gilles
  • 1