Search code examples
javajavafxpropertieschangelistener

ChangeListener not triggering in Javafx


I'm trying to learn JavafX and hence how to think with properties.

I have a bean which I use to update a TableView, like this:

public class DDTTabController extends DefaultTabController implements Initializable {
    ....
    @FXML
    private TableView<DDTTableView> ddtTable;
    ....
    @FXML
    private TableColumn<DDTTableView, String> rifColumn;
    ....
}

and so on. I init my Controller like this:

@Override
public void initialize(URL url, ResourceBundle rb) {
....
    rifColumn.setCellValueFactory(cellData -> cellData.getValue().getRifProperty());
....
}

This is the bean I use for the View:

private class DDTTableView {

    private ObjectProperty<DDT> ddt;
    private ObjectProperty<Contact> contact;
    private StringProperty rif;

    public DDTTableView() {
        this.ddt = new SimpleObjectProperty<>();
        this.contact = new SimpleObjectProperty<>();
        this.rif = new SimpleStringProperty("");
    }

    public DDTTableView(DDT o) {
        this();

        this.setDdt(o);
        this.setContact(dataManager.getContactForCodeType(o.getAnaTipe(), o.getAnaCode().trim()));

        this.ddt.get().getRowsProperty().addListener(new ChangeListener() {
            @Override
            public void changed(ObservableValue observable, Object oldValue, Object newValue) {
                System.out.println("bip!");
                rif.set(...... buildString ......);
            }
        });

    }

    public StringProperty getRifProperty() {
        return this.rif;
    }

    public String getRif() {
        return this.rif.get();
    }

    public void setRif(String r) {
        this.rif.set(r);
    }

    public ObjectProperty<DDT> getDdtProperty() {
        return ddt;
    }

    public DDT getDdt() {
        return ddt.get();
    }

    public void setDdt(DDT ddt) {
        this.ddt.set(ddt);
    }

    public ObjectProperty<Contact> getContactProperty() {
        return contact;
    }

    public Contact getContact() {
        return contact.get();
    }

    public void setContact(Contact contact) {
        this.contact.set(contact);
    }

    @Override
    public int hashCode() {
        int hash = 5;
        hash = 89 * hash + Objects.hashCode(this.ddt);
        return hash;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final DDTTableView other = (DDTTableView) obj;
        if (!Objects.equals(this.ddt, other.ddt)) {
            return false;
        }
        return true;
    }
}

the DDT bean:

public class DDT {
....
    private ObjectProperty<ObservableList<DDTItem>> rows;
....
}

public DDT() {
....
    this.rows = new SimpleObjectProperty<>(FXCollections.observableArrayList());
....
}

    public ObjectProperty<ObservableList<DDTItem>> getRowsProperty() {
        return rows;
    }

    public ObservableList<DDTItem> getRows() {
        return rows.get();
    }

    public void setRighe(ObservableList<DDTItem> r) {
        this.rows.set(r);
    }
....
}

and finally the entry point in which I pass my data to the Controller:

public void setMainApp(AppWindow mainApp, MDIWindow win, MDICanvas can) {
    super.setMainApp(mainApp, win, can);

    dataManager.getDDT().stream().forEach((ddt) -> {
        actualDDT.add(new DDTTableView(ddt));
    });
}

And you can see I only use the 2nd contructor (the one with the parameter).

Now the problem is that even if the Rows property is updated in the DDT bean, the Rif property desn't get rebuilt because the ChangeListener does not trigger, and I cannot understand why.

Can anybody please shed some light?

Thanks.


Solution

  • In the DDTTableView constructor, you set the value of the ddt property to the value passed as a parameter, and then you add a listener to the rowsProperty of the current value of ddt:

    this.ddt.get().getRowsProperty().addListener(new ChangeListener() {
        @Override
        public void changed(ObservableValue observable, Object oldValue, Object newValue) {
            System.out.println("bip!");
            rif.set(...... buildString ......);
        }
    });
    

    There are three problems that I can see here:

    1. ddt is mutable (can be set to a new value), and if it is changed, the listener will still be attached to the rowsProperty of the original value, not the current value.
    2. You are only listening for changes to the entire list of rows. I.e. your listener will respond to ddt.setRows(/* another entire list of DDTItems */), but will not respond to changes to the current list. So it won't respond to ddt.getRows().add(/* some DDTItem */);, etc.
    3. Your default DDTTableView constructor does not add the listener (only the constructor taking a parameter does).

    For the first problem, you need to observe your ddt property and move the listener if its value changes.

    For the second problem, I recommend not making the list mutable, but simply modifiable. Instead of ddt.setRows(someOtherList) (if you ever need it), you can always do ddt.getRows().setAll(someOtherList), which has essentially the same effect1. Then just register a ListChangeListener will the list.

    The third problem is easily fixed by moving the code that attaches the listener to the default constructor (which is invoked by the other constructor).

    You should also fix your method names so they match the JavaFX properties pattern.

    I.e.:

    public class DDT {
    
        //...
    
        private final ObservableList<DDTItem> rows;
    
        // ...
    
    
        public DDT() {
    
            // ...
    
            this.rows = FXCollections.observableArrayList();
    
            // ...
        }
    
    
        public ObservableList<DDTItem> getRows() {
            return rows.get();
        }
    
        // ...
    }
    

    and now

    private class DDTTableView {
    
        private ObjectProperty<DDT> ddt;
        private ObjectProperty<Contact> contact;
        private StringProperty rif;
    
        public DDTTableView() {
            this.ddt = new SimpleObjectProperty<>();
            this.contact = new SimpleObjectProperty<>();
            this.rif = new SimpleStringProperty("");
    
            this.setContact(dataManager.getContactForCodeType(o.getAnaTipe(), o.getAnaCode().trim()));
    
            ListChangeListener<DDTItem> rowsListener = (ListChangeListener.Change<? extends DDTItem> change) -> {
                rif.set(/* buildString */);
            };
    
            this.ddt.addListener((obs, oldDdt, newDdt) -> {
                if (oldDdt != null) {
                    oldDdt.getRows().removeListener(rowsListener);
                }
                if (newDdt != null) {
                    newDdt.getRows().addListener(rowsListener);
                }
            });
    
    
        }
    
        public DDTTableView(DDT o) {
            this();
    
            this.setDdt(o);
    
        }
    
        public StringProperty rifProperty() {
            return this.rif;
        }
    
        public String getRif() {
            return this.rif.get();
        }
    
        public void setRif(String r) {
            this.rif.set(r);
        }
    
        public ObjectProperty<DDT> ddtProperty() {
            return ddt;
        }
    
        public DDT getDdt() {
            return ddt.get();
        }
    
        public void setDdt(DDT ddt) {
            this.ddt.set(ddt);
        }
    
        public ObjectProperty<Contact> contactProperty() {
            return contact;
        }
    
        public Contact getContact() {
            return contact.get();
        }
    
        public void setContact(Contact contact) {
            this.contact.set(contact);
        }
    
        @Override
        public int hashCode() {
            int hash = 5;
            hash = 89 * hash + Objects.hashCode(this.ddt);
            return hash;
        }
    
        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final DDTTableView other = (DDTTableView) obj;
            if (!Objects.equals(this.ddt, other.ddt)) {
                return false;
            }
            return true;
        }
    }
    

    1 If you really need setRows(...) functionality, you can use a ListProperty<DDTItem> instead of a ObjectProperty<ObservableList<DDTItem>>. This will notify ListChangeListeners if either the underlying list is changed via setRows(...) or if the current list is modified via getRows().add(...) etc. The use cases for this are very rare, though, and usually it is enough just to have an immutable modifiable list as in the code above.