damat-perdigannat
damat-perdigannat

Reputation: 5960

Strange TableView.itemsProperty Behavior?

Let's say I'll add a ChangeListener to a TableView's itemsProperty. When would the ChangeListener's changed method be called?

I tried adding to the empty List where the TableView's items points. The result - The ChangeListener's changed method didn't get called.

tableView.itemsProperty().addListener(new ChangeListener() {
    @Override
    public void changed(ObservableValue ov, Object t, Object t1) {
        System.out.println("Changed!");
    }
});

final ObservableList data = FXCollections.observableArrayList(new ArrayList());
data.clear();
//data.add(new Object()); don't call this yet
tableView.setItems(data);
data.clear();
data.add(new Object());
tableView.setItems(data);

However, I also tried adding to an empty List and then let TableView's items point on it. The result - The ChangeListener's changed method got called.

tableView.itemsProperty().addListener(new ChangeListener() {
    @Override
    public void changed(ObservableValue ov, Object t, Object t1) {
        System.out.println("Changed!");
    }
});

final ObservableList data = FXCollections.observableArrayList(new ArrayList());
data.clear();
data.add(new Object()); 
tableView.setItems(data);
data.clear();
data.add(new Object());
tableView.setItems(data);

I looked it up on http://docs.oracle.com/javafx/2/api/javafx/scene/control/TableView.html#itemsProperty() but it only says "The underlying data model for the TableView. Note that it has a generic type that must match the type of the TableView itself."

I'm asking this because I might miss out on some other important circumstances.

Upvotes: 3

Views: 855

Answers (2)

kleopatra
kleopatra

Reputation: 51535

A not fully documented fact (aka: implementation detail) is that ObjectProperty only fires on

!oldValue.equals(newValue); // modulo null checking 

That's critical for a list-valued object property, as lists are specified to be equal if all their elements are equal. In particular, all empty lists are equal to each other, thus replacing one empty list by another empty list as in your first snippet will not make the property fire:

 // items empty initially
 TableView table = new TableView()
 table.itemsProperty().addListener(....)
 ObservableList empty = FXCollections.observableArrayList();
 // replace initial empty list by new empty list
 table.setItems(empty);
 // no change event was fired!

That's nasty if your code wants to listen to changes of the content - it would need to re-wire the ListChangeListeners whenever the identity of the items value changes but can't with a changeListener because that fires based on equality. BTW, even fx-internal code got that wrong and hot-fixed by a dirty hack

And no nice solution available, just a couple of suboptimal options

  • use an InvalidationListener instead of a changeListener
  • bind (unidirectionally!) a ListProperty to the list-valued object property and listen to the latter
  • use an adapter that combines the above to at least have it out off the way

A code snippet I use:

public static <T> ListProperty<T> listProperty(final Property<ObservableList<T>> property) {
    Objects.requireNonNull(property, "property must not be null");
    ListProperty<T> adapter = new ListPropertyBase<T>() {
        // PENDING JW: need weakListener?
        private InvalidationListener hack15793;
        {
            Bindings.bindBidirectional(this, property);
            hack15793 = o -> {
                ObservableList<T> newItems =property.getValue();
                ObservableList<T> oldItems = get();
                // force rewiring to new list if equals
                boolean changedEquals = (newItems != null) && (oldItems != null) 
                        && newItems.equals(oldItems);
                if (changedEquals) {
                    set(newItems);
                }
            };
            property.addListener(hack15793);
        }

        @Override
        public Object getBean() {
            return null; // virtual property, no bean
        }

        @Override
        public String getName() {
            return property.getName();
        }

        @Override
        protected void finalize() throws Throwable {
            try {
                Bindings.unbindBidirectional(property, this);
                property.removeListener(hack15793);
            } finally {
                super.finalize();
            }
        }

    };
    return adapter;
}

Upvotes: 4

Magcus
Magcus

Reputation: 866

TableView does not have implemented the method add view documentation

My aproach is the following:

To initialize the TableView itemList:

tableX.setItems(itemListX);

you could also initialize it by using the default list of the TableView:

tableX.getItems.addAll(itemListX);

in this case it will copy the list.

And the aproach to add items dynamically:

1-If you still have a reference to itemListX:

itemListX.add(item);

this you will update the TableView since the table observes the ObservableList itemListX.

2-Else, if you dont any more:

tableX.getItems().add(item);

Upvotes: 0

Related Questions