8

I build a custom property and add it to a observable list. But no listener is called if property content is changed. The following code snippets shows you the 'building':

public static final class TestObject {
    private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper();
    private final BooleanProperty selected = new SimpleBooleanProperty(false);

    public TestObject(String title) {
        this.title.set(title);
    }

    public String getTitle() {
        return title.get();
    }

    public ReadOnlyStringProperty titleProperty() {
        return title.getReadOnlyProperty();
    }

    public boolean getSelected() {
        return selected.get();
    }

    public BooleanProperty selectedProperty() {
        return selected;
    }

    public void setSelected(boolean selected) {
        this.selected.set(selected);
    }

    @Override
    public int hashCode() {
        return Objects.hash(title.get());
    }

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

    @Override
    public String toString() {
        return "TestObject{" +
                "title=" + title.get() +
                ", selected=" + selected.get() +
                '}';
    }
}

This is my POJO class with my internal property values like name and selected.

public static final class TestProperty extends SimpleObjectProperty<TestObject> {
    public TestProperty(String name) {
        super(new TestObject(name));
        init();
    }

    public TestProperty(TestObject testObject) {
        super(testObject);
        init();
    }

    public String getTitle() {
        return getValue().getTitle();
    }

    public void setSelected(boolean selected) {
        getValue().setSelected(selected);
    }

    public boolean getSelected() {
        return getValue().getSelected();
    }

    public BooleanProperty selectedProperty() {
        return getValue().selectedProperty();
    }

    public ReadOnlyStringProperty titleProperty() {
        return getValue().titleProperty();
    }

    @Override
    public void set(TestObject testObject) {
        super.set(testObject);
        init();
    }

    @Override
    public void setValue(TestObject testObject) {
        super.setValue(testObject);
        init();
    }

    private void init() {
        if (get() == null)
            return;

        get().titleProperty().addListener((v, o, n) -> fireValueChangedEvent());
        get().selectedProperty().addListener((v, o, n) -> {
            fireValueChangedEvent();
        });
    }
}

This is my custom property based on the POJO. All property changes will fire a change event for my custom property.

@Test
public void testSimple() {
    final AtomicInteger counter = new AtomicInteger(0);
    final TestProperty testProperty = new TestProperty("Test");
    testProperty.addListener(observable -> {
        System.out.println("New state: " + testProperty.get().toString());
        counter.incrementAndGet();
    });

    testProperty.setSelected(true);
    testProperty.setSelected(false);

    Assert.assertEquals(2, counter.intValue());
}

In this test you can see that the property change event works fine.

@Test
public void testList() {
    final AtomicInteger counter = new AtomicInteger(0);
    final ObservableList<TestProperty> observableList = new ObservableListWrapper<>(new ArrayList<>());
    observableList.add(new TestProperty("Test 1"));
    observableList.add(new TestProperty("Test 2"));
    observableList.add(new TestProperty("Test 3"));

    observableList.addListener(new ListChangeListener<TestProperty>() {
        @Override
        public void onChanged(Change<? extends TestProperty> change) {
            System.out.println("**************");
        }
    });
    observableList.addListener((Observable observable) -> {
        System.out.println("New state: " + ((TestProperty) observable).get().toString());
        counter.incrementAndGet();
    });

    observableList.get(1).setSelected(true);
    observableList.get(2).setSelected(true);
    observableList.get(1).setSelected(false);
    observableList.get(2).setSelected(false);

    Assert.assertEquals(4, counter.intValue());
}

But in this code you see that the observable list not called the invalidation listener nor the change listener if a property value has changed in list.

What is wrong?

Thanks.

user1770962
  • 211
  • 1
  • 2
  • 11
  • As an aside (though it's closely related to your question), `ObservableListWrapper` is not part of the public API and shouldn't really be used. There is no guarantee it will exist in future releases of JavaFX. To create `ObservableList`s (and other observable collections), use the factory methods in [`FXCollections`](http://docs.oracle.com/javase/8/javafx/api/javafx/collections/FXCollections.html). – James_D Nov 04 '14 at 11:57

3 Answers3

24

To create an observable list that will send "list updated" notifications if properties of elements of the list change, you need to create the list with an extractor. The extractor is a Callback that maps each element of the list to an array of Observables. If any of the Observables changes, InvalidationListeners and ListChangeListeners registered with the list will be notified.

So in your testList() method, you can do

final ObservableList<TestProperty> observableList = FXCollections.observableList(
    new ArrayList<>(),
    (TestProperty tp) -> new Observable[]{tp.selectedProperty()});

If the title were able to change, and you also wanted the list to receive notifications when that happened, you could do that too:

final ObservableList<TestProperty> observableList = FXCollections.observableList(
    new ArrayList<>(),
    (TestProperty tp) -> new Observable[]{tp.selectedProperty(), tp.titleProperty()});

Note that because the extractor is a Callback (essentially a function), the implementation can be arbitrarily complex (observe one property conditionally based on the value of another, etc).

James_D
  • 201,275
  • 16
  • 291
  • 322
  • 3
    Can you make an example where you listen to changes in an ObservableList for the class Person (listen for name, surname, etc?) This example does not provide a simple example for 90% of the cases :) – El Mac Apr 06 '16 at 08:36
  • Looks promising. Please provide some clarification if you can: http://stackoverflow.com/q/43745418/546476 – Daniel Node.js May 02 '17 at 19:07
  • Is there a reason this doesn't work with a linked list instead of an array list? – Ruben9922 Dec 13 '17 at 22:37
  • @Ruben9922 Tried this with a linked list and it worked fine. – James_D Dec 14 '17 at 02:50
  • @James_D Actually the issue seems to be with the `ChoiceBox` I was using this with, rather than the `ObservableList` itself; the list itself updates fine. – Ruben9922 Dec 17 '17 at 17:49
  • But the problem is it doesn't tell you what changed in the object or am I missing something? – lfmunoz Jun 25 '18 at 23:32
1

The following code shows a simple implementation for a observable list with observable values:

public class ObservableValueListWrapper<E extends ObservableValue<E>> extends ObservableListWrapper<E> {
 public ObservableValueListWrapper(List<E> list) {
  super(list, o -> new Observable[] {o});}}

Or you must create your list with a POJO:

final ObservableList<MyPOJO> list = new ObservableListWrapper<>(new ArrayList(), o -> new Observable[] { new MyPOJOProperty(o) });

Or you use it so:

final ObservableList<MyPOJO> list = new ObservableListWrapper<>(new ArrayList(), o -> { return new Observable[] {
o.value1Property(),
o.value2Property(),
...};});

That is it! Thanks.

user1770962
  • 211
  • 1
  • 2
  • 11
  • 2
    You're replicating existing functionality in the API though, and also using classes that aren't part of the public API. – James_D Nov 04 '14 at 11:45
0

The ObservableList isn't notifying the listeners whenever a property contained within the list is modified, it notifies when the list is notified.

This can be seen when you modify your test:

@Test
public void testList() {
    final AtomicInteger counter = new AtomicInteger(0);
    final ObservableList<TestProperty> observableList = new ObservableListWrapper<>(new ArrayList<>());

    observableList.addListener(new ListChangeListener<TestProperty>() {
        @Override
        public void onChanged(Change<? extends TestProperty> change) {
            System.out.println("**************");
            counter.incrementAndGet();
        }
    });

    observableList.add(new TestProperty("Test 1"));
    observableList.add(new TestProperty("Test 2"));
    observableList.add(new TestProperty("Test 3"));

    observableList.get(1).setSelected(true);
    observableList.get(2).setSelected(true);
    observableList.get(1).setSelected(false);
    observableList.get(2).setSelected(false);

    Assert.assertEquals(3, counter.intValue());
}

EDIT: Added an example ObserverListener decorator which provides the auto registration/deregistration of the ObservableValue change listener as desired by the OP.

/**
 * Decorates an {@link ObservableList} and auto-registers the provided
 * listener to all new observers, and auto-unregisters listeners when the
 * item is removed from the list.
 *
 * @param <T>
 */
public class ObservableValueList<T extends ObservableValue> implements ObservableList<T> {

    private final ObservableList<T> list;
    private final ChangeListener<T> valueListener;

    public ObservableValueList(ObservableList<T> list, ChangeListener<T> valueListener) {
        this.list = list;
        //list to existing contents of list
        this.list.stream().forEach((item) -> item.addListener(valueListener));

        //register listener which will add/remove listner on change to list
        this.list.addListener((Change<? extends T> change) -> {
            change.getAddedSubList().stream().forEach(
                    (item) -> item.addListener(valueListener));

            change.getRemoved().stream().forEach(
                    (item) -> item.removeListener(valueListener));
        });
        this.valueListener = valueListener;
    }

    /*  What follows is all the required delegate methods */

    @Override
    public int size() {
        return list.size();
    }

    @Override
    public boolean isEmpty() {
        return list.isEmpty();
    }

    @Override
    public boolean contains(Object o) {
        return list.contains(o);
    }

    @Override
    public Iterator<T> iterator() {
        return list.iterator();
    }

    @Override
    public Object[] toArray() {
        return list.toArray();
    }

    @Override
    public <T> T[] toArray(T[] ts) {
        return list.toArray(ts);
    }

    @Override
    public boolean add(T e) {
        return list.add(e);
    }

    @Override
    public boolean remove(Object o) {
        return list.remove(o);
    }

    @Override
    public boolean containsAll(Collection<?> clctn) {
        return list.containsAll(clctn);
    }

    @Override
    public boolean addAll(Collection<? extends T> clctn) {
        return list.addAll(clctn);
    }

    @Override
    public boolean addAll(int i, Collection<? extends T> clctn) {
        return list.addAll(i, clctn);
    }

    @Override
    public boolean removeAll(Collection<?> clctn) {
        return list.removeAll(clctn);
    }

    @Override
    public boolean retainAll(Collection<?> clctn) {
        return list.retainAll(clctn);
    }

    @Override
    public void replaceAll(UnaryOperator<T> uo) {
        list.replaceAll(uo);
    }

    @Override
    public void sort(Comparator<? super T> cmprtr) {
        list.sort(cmprtr);
    }

    @Override
    public void clear() {
        list.clear();
    }

    @Override
    public T get(int i) {
        return list.get(i);
    }

    @Override
    public T set(int i, T e) {
        return list.set(i, e);
    }

    @Override
    public void add(int i, T e) {
        list.add(i, e);
    }

    @Override
    public T remove(int i) {
        return list.remove(i);
    }

    @Override
    public int indexOf(Object o) {
        return list.indexOf(o);
    }

    @Override
    public int lastIndexOf(Object o) {
        return list.lastIndexOf(o);
    }

    @Override
    public ListIterator<T> listIterator() {
        return list.listIterator();
    }

    @Override
    public ListIterator<T> listIterator(int i) {
        return list.listIterator(i);
    }

    @Override
    public List<T> subList(int i, int i1) {
        return list.subList(i, i1);
    }

    @Override
    public Spliterator<T> spliterator() {
        return list.spliterator();
    }

    @Override
    public void addListener(ListChangeListener<? super T> ll) {
        list.addListener(ll);
    }

    @Override
    public void removeListener(ListChangeListener<? super T> ll) {
        list.removeListener(ll);
    }

    @Override
    public boolean addAll(T... es) {
        return list.addAll(es);
    }

    @Override
    public boolean setAll(T... es) {
        return list.setAll(es);
    }

    @Override
    public boolean setAll(Collection<? extends T> clctn) {
        return list.setAll(clctn);
    }

    @Override
    public boolean removeAll(T... es) {
        return list.removeAll(es);
    }

    @Override
    public boolean retainAll(T... es) {
        return list.retainAll(es);
    }

    @Override
    public void remove(int i, int i1) {
        list.remove(i, i1);
    }

    @Override
    public FilteredList<T> filtered(Predicate<T> prdct) {
        return list.filtered(prdct);
    }

    @Override
    public SortedList<T> sorted(Comparator<T> cmprtr) {
        return list.sorted(cmprtr);
    }

    @Override
    public SortedList<T> sorted() {
        return list.sorted();
    }

    @Override
    public void addListener(InvalidationListener il) {
        list.addListener(il);
    }

    @Override
    public void removeListener(InvalidationListener il) {
        list.removeListener(il);
    }

}
Steve Siebert
  • 1,874
  • 12
  • 18
  • But what means 'change.wasUpdated' in listener method parameter class? – user1770962 Nov 04 '14 at 09:29
  • Yes, the javadoc is a bit confusing here! This indicates if the item linked to in the observablelist (ie at index value 0,1,2,etc) was updated (ie a new object was placed at that index), not that the object itself changed. The purpose of this is to allow you, within your ListChangeListener to decide if you want to register a ChangeListener on the (probably) ObservableProperty in the list. It should be noted that the ObservableList **does not** require an ObservableProperty as a member, it can hold Object types. – Steve Siebert Nov 04 '14 at 09:39
  • But there are implementations to do that what I want to do: To get a callback if a property value in list has changed? – user1770962 Nov 04 '14 at 09:54
  • Hmmm...I should have expected that questions =) I would say no. I have not come across it, and I just went through the javadoc - nothing notable there. However, I think it would be rather trivial to decorate an ObservableList to provide that functionality. I'll add a quick example above. – Steve Siebert Nov 04 '14 at 10:27
  • That is crazy :) The list has an invalidation listener like the invalidation listener of an observable value. If you have a property the invalidation listener is called if the property changed one of its values. I think that the observable list must have the same behavior like a property: The invalidation listener must called if a property in list has changed. The list must be check that the added object is an observable value, I think. – user1770962 Nov 04 '14 at 11:19
  • If you create the observable list with an extractor, it will notify your changes via a list updated notification. I'm on a phone now so I can't post code but will do so in a while. – James_D Nov 04 '14 at 11:19
  • @SteveSiebert that works, but it is already [implemented in the API](http://docs.oracle.com/javase/8/javafx/api/javafx/collections/FXCollections.html#observableList-java.util.List-javafx.util.Callback-) (To be fair, it's extremely poorly documented.) – James_D Nov 04 '14 at 11:55
  • @James_D ah sneaky little method! Good one. – Steve Siebert Nov 04 '14 at 12:50