ObservableList bind content with elements conversion
Asked Answered
I

3

8

Is there a method to bind the content of two observable lists with conversion of elements between them? For example, something like this:

ObservableList<Model> models = FXCollections.observableArrayList();
ObservableList<TreeItem<Model>> treeItemModels = FXCollections.observableArrayList();
Bindings.bindContent(treeItemModels, models, m -> new TreeItem<Model>(m));
Isodimorphism answered 10/5, 2017 at 10:59 Comment(0)
I
10

As James_D say, this functionality is absent in standard API and the one can use ReactFX framework for doing so. But if the excess dependents is not the option, such functionality can be easy implemented without them. For this it is enough to go throught the JDK sources of Bindings#bindContent to ContentBinding#bind to ListContentBinding class and copy/paste with the neccessary modifications. As a result we get a binding which work like standard content binding:

ObservableList<Model> models = FXCollections.observableArrayList();
ObservableList<TreeItem<Model>> treeItemModels = FXCollections.observableArrayList();
BindingUtil.mapContent(treeItemModels, models, m -> new TreeItem<Model>(m));

The sources of this BindingUtil:

public class BindingUtil {

    public static <E, F> void mapContent(ObservableList<F> mapped, ObservableList<? extends E> source,
            Function<? super E, ? extends F> mapper) {
        map(mapped, source, mapper);
    }

    private static <E, F> Object map(ObservableList<F> mapped, ObservableList<? extends E> source,
            Function<? super E, ? extends F> mapper) {
        final ListContentMapping<E, F> contentMapping = new ListContentMapping<E, F>(mapped, mapper);
        mapped.setAll(source.stream().map(mapper).collect(toList()));
        source.removeListener(contentMapping);
        source.addListener(contentMapping);
        return contentMapping;
    }

    private static class ListContentMapping<E, F> implements ListChangeListener<E>, WeakListener {
        private final WeakReference<List<F>> mappedRef;
        private final Function<? super E, ? extends F> mapper;

        public ListContentMapping(List<F> mapped, Function<? super E, ? extends F> mapper) {
            this.mappedRef = new WeakReference<List<F>>(mapped);
            this.mapper = mapper;
        }

        @Override
        public void onChanged(Change<? extends E> change) {
            final List<F> mapped = mappedRef.get();
            if (mapped == null) {
                change.getList().removeListener(this);
            } else {
                while (change.next()) {
                    if (change.wasPermutated()) {
                        mapped.subList(change.getFrom(), change.getTo()).clear();
                        mapped.addAll(change.getFrom(), change.getList().subList(change.getFrom(), change.getTo())
                                .stream().map(mapper).collect(toList()));
                    } else {
                        if (change.wasRemoved()) {
                            mapped.subList(change.getFrom(), change.getFrom() + change.getRemovedSize()).clear();
                        }
                        if (change.wasAdded()) {
                            mapped.addAll(change.getFrom(), change.getAddedSubList()
                                    .stream().map(mapper).collect(toList()));
                        }
                    }
                }
            }
        }

        @Override
        public boolean wasGarbageCollected() {
            return mappedRef.get() == null;
        }

        @Override
        public int hashCode() {
            final List<F> list = mappedRef.get();
            return (list == null) ? 0 : list.hashCode();
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }

            final List<F> mapped1 = mappedRef.get();
            if (mapped1 == null) {
                return false;
            }

            if (obj instanceof ListContentMapping) {
                final ListContentMapping<?, ?> other = (ListContentMapping<?, ?>) obj;
                final List<?> mapped2 = other.mappedRef.get();
                return mapped1 == mapped2;
            }
            return false;
        }
    }
}
Isodimorphism answered 11/5, 2017 at 11:55 Comment(1)
Nice solution, I only had to change 3 occurrences of toList() to Collectors.toList() to make it compile.Fair
H
2

This functionality is not available in the standard API. However, the ReactFX framework provides a mechanism for doing this:

ObservableList<Model> models = FXCollections.observableArrayList();
ObservableList<TreeItem<Model>> treeItemModels 
    = LiveList.map(models, m -> new TreeItem<Model>(m));
Helterskelter answered 10/5, 2017 at 12:42 Comment(1)
Thanks for answer. It is good to know about ReactFX and its possibilities. But it does not seem like a good idea to have unnecessary dependents if the project don't based on this framework initially. I was sure that this can be done with the standard API. Fortunatelly, implement this functionality is not big deal, as it turned out.Isodimorphism
E
0

One additional proposal to temporarly cache elements to be removed: In the code of sdorof there is a problem when re-sequencing the original list. The provided Change will contain a "removal" of all affected mapped elements (here TreeItems) followed by an "add" in the new sequence. The code above creates therefore a lot of new TreeItems in this case. Assuming situations where you have other mapped elements which contain "important" information, that you don't want to loose, it is not a good idea to put new "empty" elements in the target list instead. It makes sense here to cache elements that are about to be removed until the whole onChanged() method is processed. see also a similar thread (Best practice to decorate an ObservableList and retain change events).
The updated code would look like:

....
private IdentityHashMap<E, F> cache = null;

@Override
public void onChanged(Change<? extends E> change) {
    final List<F> mapped = mappedRef.get();
    if (mapped == null) {
        change.getList().removeListener(this);
    } else {
        while (change.next()) {
            if (change.wasPermutated()) {
                List<? extends E> orig = change.getList().subList(change.getFrom(), change.getTo());
                List<F> sub = mapped.subList(change.getFrom(), change.getTo());
                cache(orig, sub);
                sub.clear();
                mapped.addAll(change.getFrom(), orig.stream().map(e -> computeIfAbsent(e)).collect(Collectors.toList()));
            } else {
                if (change.wasRemoved()) {
                    List<F> sub = mapped.subList(change.getFrom(), change.getFrom() + change.getRemovedSize());
                    if (change.wasAdded()) {
                        List<? extends E> orig = change.getRemoved();
                        cache(orig, sub);
                    }
                    sub.clear();
                }
                if (change.wasAdded())
                    mapped.addAll(change.getFrom(),change.getAddedSubList().stream().map(e -> computeIfAbsent(e)).collect(Collectors.toList()));
            }
        }
        cache = null;
    }
}

private void cache(List<? extends E> orig, List<F> mapped) {
    if (cache == null)
        cache = new IdentityHashMap<>();
    for (int i = 0; i < orig.size(); i++)
        cache.put(orig.get(i), mapped.get(i));
}

private F computeIfAbsent(E e) {
    F f = null;
    if (cache != null)
        f = cache.get(e);
    if (f == null)
        f = mapper.apply(e);
    return f;
}
....
Elasticize answered 5/4, 2018 at 20:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.