ObservableList 绑定内容与元素转换

ObservableList bind content with elements conversion

有没有一种方法可以绑定两个可观察列表的内容,并在它们之间进行元素转换?例如,像这样:

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

此功能在标准 API 中不可用。但是,ReactFX 框架提供了一种机制来执行此操作:

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

正如 所说,标准 API 中没有此功能,可以使用 ReactFX 框架来实现。但是,如果过多的受抚养人不是一个选项,那么在没有他们的情况下也可以轻松实现此类功能。为此,通过 Bindings#bindContentContentBinding#bindListContentBinding class 和 copy/paste 的 JDK 来源进行必要的修改就足够了。结果我们得到了一个像标准内容绑定一样工作的绑定:

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

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;
        }
    }
}

临时 缓存元素 的附加提议被移除:在 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 () 的代码中。
更新后的代码如下所示:

....
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;
}
....