装饰ObservableList并保留更改事件的最佳实践

我的数据源提供了一个ObservableList ,但是对于我的ListView,我需要一个ObservableList

Warning基本上只是字符串的装饰器,添加一个布尔值来提供跟踪我的ListView复选框状态的方法,如本答案中所提出的。

 class Warning { private final ReadOnlyStringWrapper name; private final BooleanProperty checked; /* ... */ } 

目前,我正在观察原始列表中的更改事件,并手动添加/删除警告列表中的项目:

 ObservableList stringList = ...; ObservableList warningList = ...; stringList.addListener(new ListChangeListener() { @Override public void onChanged(ListChangeListener.Change change) { if (change.wasAdded()) { warningList.addAll(change.getAddedSubList().stream().map(Warning::new).collect(Collectors.toList())); } else if (change.wasRemoved()) { change.getRemoved().forEach(str -> { warningList.removeIf(w -> str.equals(w.name)); }); } } }); 

我的问题是:是否有更优雅的方式来装饰我的String类型列表,因此它可以用作警告类型列表而无需手动传递更改事件?

更确切地说:如果在原始列表中添加或删除字符串,我希望立即在警告列表中看到此更改,从而看到ListView。

自从发布以来,我一直在考虑这个问题。 正如我在评论中建议的那样使用EasyBind将无法工作,因为每次在映射列表上调用get(...)时它都会创建一个新的Warning 。 所以

 stringList.add("foo"); warningList.get(0).setChecked(true); assert warningList.get(0).isChecked(); 

会失败的。

此外,如果源列表( stringList )中有重复的条目,您的机制会出错(我认为),因为当从warningList中删除单个元素时,您将从stringList删除所有相应的条目。 事实上,删除元素是正确的是非常棘手的。

这是一个基于Tomas Mikula的MappedList的解决方案,它缓存源元素和映射元素之间的映射。 它使用IdentityHashMap来确保重复元素在两个列表中都能正常运行。 请注意,这仅适用于在将项目添加到源列表时要创建新对象的特定情况,因此不打算(并且不会)替换EasyBind中的机制。

 import java.util.ArrayList; import java.util.IdentityHashMap; import java.util.List; import java.util.function.Function; import javafx.application.Application; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ReadOnlyStringProperty; import javafx.beans.property.ReadOnlyStringWrapper; import javafx.beans.property.SimpleBooleanProperty; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener.Change; import javafx.collections.ObservableList; import javafx.collections.transformation.TransformationList; import javafx.geometry.Insets; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.ListView; import javafx.scene.control.TextField; import javafx.scene.control.cell.CheckBoxListCell; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import javafx.stage.Stage; public class WrappedObjectListExample extends Application { @Override public void start(Stage primaryStage) { ObservableList stringList = FXCollections.observableArrayList("One", "Two", "Three"); ObservableList warningList = new CachingMappedList(stringList, Warning::new); ListView stringListView = new ListView<>(stringList); ListView warningListView = new ListView<>(warningList); warningListView.setCellFactory(CheckBoxListCell.forListView(Warning::checkedProperty)); TextField textField = new TextField(); textField.setOnAction(e -> { if (! textField.getText().isEmpty()) { stringList.add(textField.getText()); textField.setText(""); } }); Button remove = new Button("Remove"); remove.setOnAction(e -> stringList.remove(stringListView.getSelectionModel().getSelectedIndex())); remove.disableProperty().bind(stringListView.getSelectionModel().selectedItemProperty().isNull()); HBox lists = new HBox(10, stringListView, warningListView); VBox root = new VBox(10, lists, textField, remove); root.setPadding(new Insets(20)); primaryStage.setScene(new Scene(root)); primaryStage.show(); } public static class Warning { private final ReadOnlyStringWrapper name = new ReadOnlyStringWrapper(); private final BooleanProperty checked = new SimpleBooleanProperty(); public Warning(String name) { this.name.set(name); } public final ReadOnlyStringProperty nameProperty() { return this.name.getReadOnlyProperty(); } public final String getName() { return this.nameProperty().get(); } public final BooleanProperty checkedProperty() { return this.checked; } public final boolean isChecked() { return this.checkedProperty().get(); } public final void setChecked(final boolean checked) { this.checkedProperty().set(checked); } @Override public String toString() { return getName(); } } public static class CachingMappedList extends TransformationList { private final Function mapper ; private final IdentityHashMap cache ; public CachingMappedList(ObservableList source, Function mapper) { super(source); this.mapper = mapper ; this.cache = new IdentityHashMap<>(); } @Override protected void sourceChanged(Change c) { fireChange(new Change(this) { @Override public boolean wasAdded() { return c.wasAdded(); } @Override public boolean wasRemoved() { return c.wasRemoved(); } @Override public boolean wasReplaced() { return c.wasReplaced(); } @Override public boolean wasUpdated() { return c.wasUpdated(); } @Override public boolean wasPermutated() { return c.wasPermutated(); } @Override public boolean next() { return c.next(); } @Override public void reset() { c.reset(); } @Override public int getFrom() { return c.getFrom(); } @Override public int getTo() { return c.getTo(); } @Override public List getRemoved() { List removed = new ArrayList<>(); c.getRemoved().forEach(t -> removed.add(cache.get(t))); return removed; } @Override public int getPermutation(int i) { return c.getPermutation(i); } @Override protected int[] getPermutation() { throw new AssertionError("Unreachable code"); } }); // clean up cache: c.reset(); while (c.next()) { if (c.wasRemoved()) { c.getRemoved().forEach(cache::remove); } } } @Override public int getSourceIndex(int index) { return index ; } @Override public S get(int index) { return cache.computeIfAbsent(getSource().get(index), mapper); } @Override public int size() { return getSource().size(); } } public static void main(String[] args) { launch(args); } }