Search code examples
javajavafxchangelistener

Duplication of Property's ChangeListener after every window's appearance


Let's assume we have a root window with fx:include:

<?import javafx.scene.layout.VBox?>

<VBox fx:controller="sample.StartWindowController"
      xmlns:fx="http://javafx.com/fxml" alignment="center">
    <fx:include source="startwindow.fxml"/>
</VBox>

Code of startwindow.fxml:

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

<VBox fx:id="mainPane" fx:controller="sample.StartWindowController"
      xmlns:fx="http://javafx.com/fxml" alignment="center">
    <Button text="Go to New Window" onAction="#goToNewWindow"/>
</VBox>

Clicking Button changes window to the new new one. Its controller, StartWindowController:

package sample;

import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import java.io.IOException;

public class StartWindowController {

  @FXML
  VBox mainPane;

  @FXML
  private void goToNewWindow() {
    Pane parentPane = (Pane) mainPane.getParent();
    parentPane.getChildren().clear();
    try {
      parentPane.getChildren().add(FXMLLoader.load(getClass().getResource("newwindow.fxml")));
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}

Before I show you view and controller of a 'New Window', you must know that an application has a Singleton class with BooleanProperty field. Code of MySingleton:

package sample;

import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;

public class MySingleton {

  private static MySingleton instance;
  private BooleanProperty booleanProperty;

  private MySingleton() {
    booleanProperty = new SimpleBooleanProperty(false);
  }

  public static MySingleton getInstance() {
    if (instance == null) {
      instance = new MySingleton();
    }
    return instance;
  }

  public boolean isBooleanProperty() {
    return booleanProperty.get();
  }

  public BooleanProperty booleanPropertyProperty() {
    return booleanProperty;
  }
}

Code of newwindow.fxml:

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

<VBox fx:id="mainPane" fx:controller="sample.NewWindowController"
      xmlns:fx="http://javafx.com/fxml" alignment="center">
    <Button text="Change BooleanProperty" onAction="#changeBooleanProperty"/>
    <Button text="Back" onAction="#goBack"/>
</VBox>

During 'New Window' creation I add a listener to the MySingleton's BooleanProperty in controller's initialize method. New listener's code refers to the non-static, controller's private method, printMessage. NewWindowController's code:

package sample;

import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import java.io.IOException;

public class NewWindowController {

  private MySingleton mySingleton;
  @FXML VBox mainPane;
  private int duplicateCounter = 1;

  @FXML private void initialize() {
    System.out.println("Initializing New Window's Controller.");
    System.out.println("Duplicate counter: " + duplicateCounter);
    mySingleton = MySingleton.getInstance();
    mySingleton.booleanPropertyProperty().addListener(
            (observable, oldValue, newValue) -> printMessage());
    duplicateCounter++;
  }

  @FXML private void changeBooleanProperty() {
    mySingleton.booleanPropertyProperty().setValue(!mySingleton.booleanPropertyProperty().getValue());
  }

  @FXML private void goBack() {
    Pane parentPane = (Pane) mainPane.getParent();
    parentPane.getChildren().clear();
    try {
      parentPane.getChildren().add(FXMLLoader.load(getClass().getResource("startwindow.fxml")));
    } catch (IOException e) {
      e.printStackTrace();
    }
  }

  private void printMessage() {
    System.out.println("Boolean property changed!");
  }
}

Now, the problem. Assuming the first step is going to the 'New Window':
After every "Back" -> "Go to New Window" sequence, when I click "Change BooleanProperty" there are i+1 prints of "Boolean property changed!", where i is number of 'Start Window' -> 'New Window' transitions (starting from 0). Why isn't there only one print?

I'm aware of the fact that every time I launch 'New Window', application adds new listener to the BooleanProperty and problem is probably caused by a number of property's listeners. But how is it possible if in a new listener's code I refer to the non-static method of the object which is destroyed after window transition?

I thought "Maybe controller is not destroyed? Maybe initialize method works in a way I don't understand and controller's object is still there?" So as you probably see, I added an extra variable, duplicateCounter which increments at the end of the initialize method. But every time it's 1, so I assume the brand new NewWindowController object is created.

How can I prevent BooleanProperty from listeners' duplication?


Solution

  • But how is it possible if in a new listener's code I refer to the non-static method of the object which is destroyed after window transition?

    I thought "Maybe controller is not destroyed? Maybe initialize method works in a way I don't understand and controller's object is still there?" So as you probably see, I added an extra variable, duplicateCounter which increments at the end of the initialize method. But every time it's 1, so I assume the brand new NewWindowController object is created.

    Indeed a new controller is created every time the fxml is loaded, however the old one isn't "destroyed" (available for garbage collection), since there is still a reference to the object:

    MySingleton stores the instance in a static member, making it unavailable for garbage collection. This instance contains a reference to the BooleanProperty which contains a reference to the listener which contains a reference to the NewWindowController.

    To only print the message once, you have to unregister the listener:

    private final ChangeListener<Boolean> listener = (observable, oldValue, newValue) -> printMessage();
    
    @FXML private void initialize() {
        ...
        mySingleton.booleanPropertyProperty().addListener(listener);
        ...
    }
    
    ...
    
    @FXML private void goBack() {
        // remove listener
        MySingleton.getInstance().booleanPropertyProperty().removeListener(listener);        
        ...
    }