javajavafxlistener

Listen to the change of userData in JavaFX


I want to listen to the change of userData of a stage in JavaFX. I have tried to wrap the Object which is returned from getUserData method inside a SimpleObjectProperty, then add a listener to it but it didn't work.

This is my attempt:

SimpleObjectProperty<Object> userDataProperty = new SimpleObjectProperty<>(stage.getUserData());
    userDataProperty.addListener((observable, oldValue, newValue) -> {
    // print when userData is changed
    System.out.println("new userdata:" + stage.getUserData());
});

// change the userData to test if the listener work
stage.setUserData(2);
System.out.println(stage.getUserData());
stage.setUserData(3);
System.out.println(stage.getUserData());

Output:

2
3

How to do it properly?


Solution

  • The Window class does not expose a JavaFX property for the user data, which means it is not directly observable. However, the Window#setUserData(Object) method has this somewhat cryptic documentation:

    Convenience method for setting a single Object property that can be retrieved at a later date. This is functionally equivalent to calling the getProperties().put(Object key, Object value) method. This can later be retrieved by calling getUserData().

    That would seem to indicate the user data is held in the ObservableMap returned by Window#getProperties(). And if you look at the implementation, you will see that is indeed the case. Unfortunately, the key for the user data entry is private. But you can use reflection to get the key or, possibly better, use a MapChangeListener to capture it.

    Once you have the key you can use Bindings#valueAt(ObservableMap, K) to create an ObjectBinding that will always report the latest user data value.

    Here is an example where a MapChangeListener is used to capture the key.

    import javafx.application.Application;
    import javafx.beans.binding.Bindings;
    import javafx.beans.binding.ObjectBinding;
    import javafx.collections.MapChangeListener;
    import javafx.scene.Scene;
    import javafx.scene.control.TextField;
    import javafx.scene.layout.Region;
    import javafx.scene.layout.StackPane;
    import javafx.stage.Stage;
    import javafx.stage.Window;
    
    public class Main extends Application {
    
      // Make sure to keep a strong reference to the binding for as long as you need it.
      private ObjectBinding<Object> userData;
    
      @Override
      public void start(Stage primaryStage) {
        userData = userDataBinding(primaryStage);
        // Note: Using _ for unused parameters was standardized in Java 22.
        userData.addListener(
            (_, oldVal, newVal) -> System.out.printf("User data: %s -> %s%n", oldVal, newVal));
    
        // Type something into the TextField then press ENTER to see the example at work.
        var field = new TextField();
        field.setMaxWidth(Region.USE_PREF_SIZE);
        field.setOnAction(
            e -> {
              e.consume();
              primaryStage.setUserData(field.getText());
              field.clear();
            });
    
        primaryStage.setScene(new Scene(new StackPane(field), 500, 300));
        primaryStage.show();
      }
    
      private ObjectBinding<Object> userDataBinding(Window window) {
        // Use array because local variables must be (effectively) final when used inside
        // a lambda expression or anonymous class.
        var keyHolder = new Object[1];
        window
            .getProperties()
            .addListener(
                new MapChangeListener<>() {
                  @Override
                  public void onChanged(Change<? extends Object, ? extends Object> change) {
                    keyHolder[0] = change.getKey();
                    // Only need to capture the key once.
                    change.getMap().removeListener(this);
                  }
                });
        var oldUserData = window.getUserData(); // save current value
        window.setUserData(new Object());       // invoke the listener
        window.setUserData(oldUserData);        // restore to old value
        return Bindings.valueAt(window.getProperties(), keyHolder[0]);
      }
    }
    

    From looking at the documentation, the same approach can be used with Scene and Node.


    Of course, you could use your own key and manipulate the Window.getProperties() map directly. That will likely be easier.