Search code examples
javafxchangelistener

How to use ListenerHandle in JavaFX?


I read this article on ListenerHandles, but I couldn't understand how to implement it in my code. I have a ChangeListener which I assign to my CheckBoxes. I need to change value of CheckBoxes without Listeners noticing it.

My code for creating CheckBoxes:

for (int i = 0; i<2; i++) {
    CheckBox checkBox = new CheckBox();
    checkBox.setText("CheckBox "+(i+1));
    checkBox.setAlignment(Pos.TOP_LEFT);

    checkBox.selectedProperty().addListener(checkBoxListener(checkBox));

    myVBox.getChildren().add(checkBox);
    }

ChangeListener code:

private ChangeListener<Boolean> checkBoxListener(CheckBox checkBox) {
        return new ChangeListener<Boolean>() {
            @Override
            public void changed(ObservableValue<? extends Boolean> arg0, Boolean oldPropertyValue, Boolean selected) {
                if (selected) {
                    System.out.println("CheckBox got selected");
                } else {
                    System.out.println("CheckBox got deselected");
                }
            }
        };
    }

Code that needs to get done without Listener noticing:

public void deselectCheckBoxes(){
      for(Node node : myVBox.getChildren()){
            CheckBox checkBox = (CheckBox) node;
            System.out.println("Removing listener");
            checkBox.selectedProperty().removeListener(checkBoxListener(checkBox));

            checkBox.setSelected(false);

            System.out.println("Adding listener back");
            checkBox.selectedProperty().addListener(checkBoxListener(checkBox));
       }
}

Listener still noticing that I'm deselecting CheckBox through this function. Can someone explain how I could implement ListenerHandle way in my code?

Thank you very much for your answer. I adjusted ChangeListener, so that I can get object from my CheckBox:

ChangeListener<Boolean> listener = (obs, wasSelected, isNowSelected) -> {
            if (isNowSelected) {
                BooleanProperty booleanProperty = (BooleanProperty) obs;
                CheckBox checkBox = (CheckBox)booleanProperty.getBean();

                System.out.println("Selected: "+(Bank)checkBox.getUserData());
            } else {
                System.out.println("Not selected");
            }
        };

Solution

  • First, a few notes:

    1. Your code doesn't work, because your checkBoxListener() creates a different listener every time you invoke it. So the listener you attempt to remove is not the listener you initially added, and so the call to removeListener(...) does nothing. Worse, you add a different listener later when you try to restore the original. Consequently, you should find that checking the check box after the deselectCheckBoxes() method has been called invokes the listener multiple times.
    2. To me, needing to do this at all is usually the sign of a poor design. You really shouldn't care how the check box is deselected (i.e. programmatically or by the user). The controller should really be invoking methods on the model so that the model is in sync with the state of the UI. From this perspective, the cause of the check box becoming deselected should not matter.
    3. Note that this functionality is implemented "out of the box" by Tomas Mikula's ReactFX framework. (Basically you can subscribe to "event streams", which can include streams of changes to properties. You can make those streams "suspendible" and suspend them during specific operations.)

    Anyway, assuming you really need this for some reason, as I understand it, the intention is that you would do something like this:

    import javafx.beans.value.ChangeListener;
    import javafx.beans.value.ObservableValue;
    
    public class ListenerHandle<T>  {
    
        private final ObservableValue<T> observable ;
        private final ChangeListener<? super T> listener ;
    
        private boolean attached ;
    
        public ListenerHandle(ObservableValue<T> observable, ChangeListener<? super T> listener) {
            this.observable = observable ;
            this.listener = listener ;
        }
    
        public void attach() {
            if (! attached) {
                observable.addListener(listener);
                attached = true ;
            }
        }
    
        public void detach() {
            if (attached) {
                observable.removeListener(listener);
                attached = false ;
            }
        }
    
    
    }
    

    Then you can use this as

        ChangeListener<Boolean> listener = (obs, wasSelected, isNowSelected) -> {
            if (isNowSelected) {
                System.out.println("Selected");
            }
            else {
                System.out.println("Not selected");
            }
    
        };
    
        ListenerHandle<Boolean> handle = new ListenerHandle<>(checkBox.selectedProperty(), listener);
        handle.attach();
    

    and to "turn off" handling, you would do

    // ignore this change:
    handle.detach();
    checkBox.setSelected(false);
    handle.attach();
    

    Note that to make this work, you still need to keep a reference to the handle (you just no longer necessarily need to keep a separate reference to the property or node, and keep them "connected"). In your case, since you have multiple check boxes, since you have multiple check boxes and multiple listeners, you would need something like:

    private List<ListenerHandle<Boolean>> checkBoxListenerHandles = new ArraryList<>();
    
    // ...
    
    ChangeListener<Boolean> listener = (obs, wasSelected, isNowSelected) -> {
        if (isNowSelected) {
            System.out.println("CheckBox got selected");
        } else {
            System.out.println("CheckBox got deselected");
        }
    }
    
    for (int i = 0; i<2; i++) {
        CheckBox checkBox = new CheckBox();
        checkBox.setText("CheckBox "+(i+1));
        checkBox.setAlignment(Pos.TOP_LEFT);
    
        ListenerHandle<Boolean> handle = new ListenerHandle<>(checkBox.selectedProperty(), listener);
        handle.attach();
        checkBoxListenerHandles.add(handle);
    
        myVBox.getChildren().add(checkBox);
    }
    

    and then

    public void deselectCheckBoxes(){
    
       checkBoxListenerHandles.forEach(ListenerHandle::detach);
    
       for(Node node : myVBox.getChildren()){
            CheckBox checkBox = (CheckBox) node;
            checkBox.setSelected(false);
       }
    
       checkBoxListenerHandles.forEach(ListenerHandle::attach);
    
    }
    

    Note that if you wanted to deselect (and ignore) a specific check box, you would have to know which handle belonged to that particular check box. You could do that with a Map, or similar, but at that point you have pretty much gained nothing compared to simply keeping references to the listeners and removing them.