1

I stumbled upon a problem using a slider in JavaFX.
I create a fxml-file with a Slider and add a controller to it.
Inside the controller I have a DoubleProperty, which binds to the Slider's valueProperty. Then, when I want to bind to this property from somewhere else, I bind to the property, which is inside the controller (some kind of a middle-man approach see Figure).
enter image description here
But when I do so, it does not work properly.

When I use the slider, the values get updated accordingly for a while, but when I wiggle it around, at some point it seems to stop updating the binding and refuses to do so again even after releasing and pressing it again.
When I delete the middle-man property in the controller and just pipe through the valueProperty from the slider directly, it is working.

Program:
Main.java

public class Main extends Application{
    private MainController controller;

    @Override
    public void start(Stage primaryStage) throws Exception {
        FXMLLoader loader = new FXMLLoader();
        loader.setLocation(getClass().getResource("main.fxml"));
        Parent root = loader.load();
        controller = loader.getController();

        Scene scene = new Scene(root);
        primaryStage.setScene(scene);

        primaryStage.show();

        showSlider();
    }

    private void showSlider() {
        SliderShower sliderShower = new SliderShower();
        sliderShower.show();
        sliderShower.getSliderValueProp().addListener(((observable, oldValue, newValue) -> {
            controller.setText(Double.toString((double)newValue));
        }));
    }

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

main.fxml

<?import javafx.scene.control.Label?>
 <?import javafx.scene.layout.ColumnConstraints?>
 <?import javafx.scene.layout.GridPane?>
 <?import javafx.scene.layout.RowConstraints?>

 <GridPane prefHeight="100.0" prefWidth="100.0" xmlns="http://javafx.com/javafx/8.0.112" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sliderBug.main.MainController">
   <columnConstraints>
     <ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
   </columnConstraints>
   <rowConstraints>
     <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
   </rowConstraints>
    <children>
       <Label fx:id="label" text="Label" />
    </children>
 </GridPane>

MainController.java

public class MainController {
    @FXML
    private Label label;

    public void setText(String text) {
        label.setText(text);
    }
}

SliderShower.java

public class SliderShower {
    private Parent root;
    private SliderShowerController controller;
    private Stage stage;
    private DoubleProperty sliderValueProp;

    public SliderShower() {
        sliderValueProp = new SimpleDoubleProperty(0);
        FXMLLoader loader = new FXMLLoader();
        loader.setLocation(getClass().getResource("sliderShower.fxml"));
        try {
            root = loader.load();
            controller = loader.getController();
            stage = new Stage();
            stage.initModality(Modality.APPLICATION_MODAL);
            stage.setScene(new Scene(root));

            sliderValueProp.bind(controller.getSliderValueProp());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public DoubleProperty getSliderValueProp() {
        return sliderValueProp; // This does not work
//        return controller.getSliderValueProp(); // This would work
    }

    public void show() {
        stage.show();
    }
}

sliderShowerController.java

public class SliderShowerController {
    @FXML
    private Slider sliderUI;

    DoubleProperty getSliderValueProp() {
        return sliderUI.valueProperty();
    }
}

sliderShower.fxml

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

<?import javafx.scene.control.Slider?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.RowConstraints?>

<GridPane prefHeight="100.0" prefWidth="300.0" xmlns="http://javafx.com/javafx/8.0.112" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sliderBug.sliderShower.SliderShowerController">
  <columnConstraints>
    <ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
  </columnConstraints>
  <rowConstraints>
    <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
  </rowConstraints>
   <children>
      <Slider fx:id="sliderUI" max="200.0" min="1.0" />
   </children>
</GridPane>

Here is a link to a repository depicting the problem:
https://github.com/Chrisss50/sliderBug

Am I doing something wrong or is this just a bug?

Greetings

Chriz
  • 129
  • 2
  • 10
  • Can you create an example which you can post directly in the question, instead of posting a link to the code? If link goes stale the question will be useless to anyone else. – James_D Feb 16 '17 at 17:06
  • Yeah, you are right. I added the whole program. – Chriz Feb 16 '17 at 17:15

1 Answers1

2

Bindings in JavaFX use weak listeners under the hood. This is intended to prevent memory leaks, but has the side effect that if the properties to which something is bound go out of scope, the binding will not prevent them being garbage collected and will cease to work if they are. See this question and this blog post.

You can verify this is the issue by adding

root.setOnMouseClicked(e -> {
    if (e.getClickCount() == 2) {
        System.out.println("GC");
        System.gc();
    }
});

to the SliderShower constructor. After doing that, double-clicking near the slider will force garbage collection, and the binding will immediately cease to work.

In your case, Main instantiates SliderShower as a local variable in the showSlider method, and consequently it goes out of scope as soon as the method completes. One (somewhat unnatural) fix is to forcibly retain a reference to the SliderShower:

package sliderBug.main;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import sliderBug.sliderShower.SliderShower;

/**
 * Created by Christopher Juerges on 16/02/17.
 */
public class Main extends Application{
    private MainController controller;

    private SliderShower sliderShower ;

    @Override
    public void start(Stage primaryStage) throws Exception {
        FXMLLoader loader = new FXMLLoader();
        loader.setLocation(getClass().getResource("main.fxml"));
        Parent root = loader.load();
        controller = loader.getController();

        Scene scene = new Scene(root);
        primaryStage.setScene(scene);

        primaryStage.show();

        showSlider();
    }

    private void showSlider() {
        sliderShower = new SliderShower();
        sliderShower.show();
        sliderShower.getSliderValueProp().addListener(((observable, oldValue, newValue) -> {
            controller.setText(Double.toString((double)newValue));
        }));
    }

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

Another fix is to use a listener instead of the binding:

public SliderShower() {
    sliderValueProp = new SimpleDoubleProperty(0);
    FXMLLoader loader = new FXMLLoader();
    loader.setLocation(getClass().getResource("sliderShower.fxml"));
    try {
        root = loader.load();

        root.setOnMouseClicked(e -> {
            if (e.getClickCount() == 2) {
                System.out.println("GC");
                System.gc();
            }
        });

        controller = loader.getController();
        stage = new Stage();
        stage.initModality(Modality.APPLICATION_MODAL);
        stage.setScene(new Scene(root));

        // sliderValueProp.bind(controller.getSliderValueProp());

        controller.getSliderValueProp().addListener((obs, oldValue, newValue) -> 
            sliderValueProp.set(newValue.doubleValue()));

    } catch (IOException e) {
        e.printStackTrace();
    }
}
Community
  • 1
  • 1
James_D
  • 201,275
  • 16
  • 291
  • 322