3

I have a Spinner in controller:

@FXML
private Spinner<Integer> spnMySpinner;

and a SimpleIntegerPropertyin controller:

private static final SimpleIntegerProperty myValue = 
new SimpleIntegerProperty(3); //load a default value

I have bound them together in controller's initialize method:

spnMySpinner.getValueFactory().valueProperty().bindBidirectional(myValueProperty().asObject());

But the bindings work correctly only after second time the controller initializes. Here's how I can reproduce it:

  1. I open the stage with the associated controller, it loads the default value, specified in the myValue property correctly (a number 3).
  2. I click the increment button on the spinner to make it a 4. It changes the value in the spinner's value property, but the bound property myValue is left intact with the number 3.
  3. I close the stage/window.
  4. I reopen it, the spinner has again a value of 3.
  5. I increment it again. Boom now the binding works and I have a "4" both inside the spinner and the bound property.

Entire minimalistic, but launchable/reproducible code:

Main.java:

package spinnerpoc;

import java.io.IOException;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class Main extends Application {

    @Override
    public void start(Stage stage) throws IOException {
        Parent root  = FXMLLoader.load(getClass().getResource("MainWindow.fxml"));
        Scene scene = new Scene(root);
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }

}

MainWindow.fxml:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.Button?>
<?import javafx.scene.layout.AnchorPane?>


<AnchorPane fx:id="myRoot" id="AnchorPane" prefHeight="231.0" prefWidth="337.0" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/8.0.60" fx:controller="spinnerpoc.MainWindowController">
    <children>
        <Button fx:id="btnOpenSpinnerWindow" layoutX="102.0" layoutY="103.0" mnemonicParsing="false" text="Open SpinnerWindow" onAction="#onOpenSpinnerWindow"/>
    </children>
</AnchorPane>

MainWindowController.java:

package spinnerpoc;

import java.io.IOException;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.AnchorPane;
import javafx.stage.Modality;
import javafx.stage.Stage;

public class MainWindowController implements Initializable {

    @FXML
    private Button btnOpenSpinnerWindow;
    @FXML
    private AnchorPane myRoot;

    @Override
    public void initialize(URL url, ResourceBundle rb) {
    }

    @FXML
    private void onOpenSpinnerWindow(ActionEvent event) throws IOException{
        FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("SpinnerWindow.fxml"));
        Parent root = (Parent) fxmlLoader.load();
        Stage stage = new Stage();
        stage.initOwner(myRoot.getScene().getWindow());
        stage.initModality(Modality.WINDOW_MODAL);
        stage.setTitle("SpinnerWindow");
        stage.setScene(new Scene(root));
        stage.show();
    }

}

SpinnerWindow.fxml:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.RadioButton?>
<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.control.Slider?>
<?import javafx.scene.control.Spinner?>
<?import javafx.scene.control.SpinnerValueFactory.DoubleSpinnerValueFactory?>
<?import javafx.scene.control.TitledPane?>
<?import javafx.scene.control.ToggleGroup?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>


<ScrollPane xmlns:fx="http://javafx.com/fxml/1" fitToWidth="true" prefHeight="200.0" prefWidth="200.0" xmlns="http://javafx.com/javafx/8.0.60" fx:controller="spinnerpoc.SpinnerWindowController">
    <content>
        <VBox maxWidth="1.7976931348623157E308">
            <children>
                <Spinner fx:id="spnMySpinner" editable="true" prefWidth="50.0" max="10" min="1" />
            </children>
        </VBox>
    </content>
</ScrollPane>

SpinnerWindowController.java:

package spinnerpoc;

import java.net.URL;
import java.util.ResourceBundle;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Spinner;

public class SpinnerWindowController implements Initializable {

    private static final SimpleIntegerProperty myValue = new SimpleIntegerProperty(3);

    public static SimpleIntegerProperty myValueProperty() {
        return myValue;
    }

    public static Integer getMyValue() {
        return myValue.getValue();
    }

    public static void setMyValue(int value) {
        myValue.set(value);
    }

    @FXML
    private Spinner<Integer> spnMySpinner;

    @Override
    public void initialize(URL url, ResourceBundle rb) {
        spnMySpinner.getValueFactory().valueProperty().bindBidirectional(myValueProperty().asObject());
    }

}

(Code also available at a BitBucket repo.)

What am I missing?

Leprechaun
  • 852
  • 6
  • 25
  • It's impossible to know from the information you have given. Create a [MCVE] that reproduces the problem and [edit] your question to include it. – James_D Sep 11 '16 at 15:09
  • @James_D I wrote a simple example, but it has more than a hundred lines, so I pushed it to bitbucket. – Leprechaun Sep 11 '16 at 16:26
  • There's not too much code there to reasonably include in a question: I moved the example to your question. – James_D Sep 11 '16 at 16:44
  • For some reason I couldn't @tag you in a comment under the answer from James_D, where he suggested you had a scoping issue. Did you work out what your issue was? – pateksan Dec 19 '20 at 11:38
  • 1
    @pateksan No sorry. I think I just used static fields as a dirty & good enough solution and moved on. However this was more than 4 years ago and I have only vague recollection of the problem now, maybe the issue was my scoping, or it may have been a bug in JFX, I don't know... – Leprechaun Dec 19 '20 at 22:01
  • Thanks. I'm trying to understand your code to see if your question and the answer could help with my own problem. One thing I'm struggling with is why you have two separate properties: myValue which is final and myValueProperty which is not. Is this normal practice that I'm ignorant about, or is there a reason to have them two? – pateksan Dec 27 '20 at 02:49
  • See also this possibly related [issue](https://stackoverflow.com/q/55427306/230513). – trashgod Sep 30 '21 at 15:48

1 Answers1

5

You are running into the "premature garbage collection" problem. See a description of this here. You will likely find that it's not always every other time you show the spinner that it fails, but is just sporadic, and that the behavior will vary from one machine to another. If you limit the memory available to the JVM, you might find that it never works.

When you call IntegerProperty.asObject(), it

Creates an ObjectProperty that bidirectionally bound to this IntegerProperty.

Now note that a bidirectional binding has this feature to prevent accidental memory leaks:

JavaFX bidirectional binding implementation use weak listeners. This means bidirectional binding does not prevent properties from being garbage collected.

So the bidirectional binding you explicitly create does not prevent the thing it is bound to (the ObjectProperty<Integer> created by asObject()) from being garbage collected. Since you keep no references to it, it is eligible for garbage collections as soon as you exit the initialize() method in the SpinnerWindow Controller. Obviously, once the value to which your spinner value is bidirectionally bound is garbage collected, the binding will not work any more.

Just for demonstration purposes, you can see this by putting in a hook to force garbage collection. E.g. do

<ScrollPane onMouseClicked="#gc" xmlns:fx="http://javafx.com/fxml/1" ...>

in SpinnerWindow.fxml and

@FXML
private void gc() {
    System.out.println("Invoking GC");
    System.gc();
}

in SpinnerWindowController. If you do this, then clicking in the scroll pane will force garbage collection, and changing the spinner value will not update the property.

To fix this, retain a reference to the property you get from asObject():

public class SpinnerWindowController implements Initializable {

    private static final SimpleIntegerProperty myValue = new SimpleIntegerProperty(3);

    public static SimpleIntegerProperty myValueProperty() {
        return myValue;
    }

    public static Integer getMyValue() {
        return myValue.getValue();
    }

    public static void setMyValue(int value) {
        myValue.set(value);
    }

    @FXML
    private Spinner<Integer> spnMySpinner;

    private ObjectProperty<Integer> spinnerValue = myValueProperty().asObject();

    @Override
    public void initialize(URL url, ResourceBundle rb) {
        spnMySpinner.getValueFactory().valueProperty().bindBidirectional(spinnerValue);
    }

}
James_D
  • 201,275
  • 16
  • 291
  • 322
  • Thanks. This explains a lot, however, only helps in some of the cases. What seems to help to get rid of the issue completely is to have the `spinnerValue` as a static field, so it holds the binding property even after I close the window. I guess. – Leprechaun Sep 15 '16 at 12:03
  • Actually, no, that makes no sense. I have no idea why it works only when I make the field static, lol. – Leprechaun Sep 15 '16 at 12:15
  • That just sounds like you have other scoping problems elsewhere. It's probably a very bad idea to make that field static (you don't want the spinners in all instances of the controller to be bound to the same value). – James_D Sep 15 '16 at 12:18