带有提取器更新事件的 JavaFX ObservableList 在应该触发时未触发

JavaFX ObservableList with extractor update event not firing when it should

一点背景:

在我遇到问题之前 ObservableList 使用提取器,但是提取器正在侦听的属性是从 NOT JavaFX 线程更新的。我想出的解决方案是创建 "UI equivalent" classes 来复制原始 class 的值,只需在 JavaFX 线程上更新它们的属性。例如,如果我们有一个 Person:

public class Person {
    private final BooleanProperty adult;

    public Person(boolean adult) {
        this.adult = new SimpleBooleanProperty(adult);

        // Randomize "adult" value every 5 seconds
        ListViewExtractorTest.scheduledExecutorService.scheduleAtFixedRate(() -> {
            this.adult.set(Math.random() > 0.5);
            System.out.println("Updating adult: " + this);
        }, 5, 5, TimeUnit.SECONDS);

    // Getters setters
}

UI 等效于 class 将是:

public class PersonUI {

    private final Person person;
    private final BooleanProperty adult;

    public PersonUI(Person person) {
        this.person = person;
        adult = new SimpleBooleanProperty(person.isAdult());
        person.adultProperty().addListener((observableValue, oldValue, newValue) -> {
            Platform.runLater(() -> this.adult.set(newValue));
        });
    }

    // Getters and setters
}

我对 ObservableList 做了同样的事情 - 创建了一个方法,将 ListChangeListener 添加到源列表,每当源列表更新时,它都会更新目标列表 UI 等价物 class在 JavaFX 线程上:

public static <T, R> ObservableList<R> bindListContentPlatformRunLater(ObservableList<T> srcList, Function<T, R> function, ObservableList<R> dstList) {
    for (T item : srcList) {
        dstList.add(function.apply(item));
    }
    // Maybe should wrap the whole while loop in Platform.runLater()
    // Less runnables, but big changes might hang up the UI.
    srcList.addListener((ListChangeListener<? super T>) change -> {
        while (change.next()) {
            int from = change.getFrom();
            int to = change.getTo();
            if (change.wasPermutated()) {
                Platform.runLater(() -> dstList.subList(from, to).clear());
                List<? extends T> addItems = change.getList().subList(from, to);
                for (int i = 0; i < addItems.size(); i++) {
                    final int index = i;
                    T addItem = addItems.get(i);
                    Platform.runLater(() -> dstList.add(from + index, function.apply(addItem)));
                }
            } else {
                if (change.wasRemoved()) {
                    int removedSize = change.getRemovedSize();
                    Platform.runLater(() -> dstList.subList(from, from + removedSize).clear());
                }

                if (change.wasAdded()) {
                    List<? extends T> addedSubList = change.getAddedSubList();
                    for (int i = 0; i < addedSubList.size(); i++) {
                        final int index = i;
                        T item = addedSubList.get(i);
                        Platform.runLater(() -> dstList.add(from + index, function.apply(item)));
                    }
                }
            }
        }
    });
    return dstList;
}

所以现在如果我更新了 ObservableList<Person> 并且在 JavaFX 线程上更新了它的项目属性 NOT,我可以获得一个可以在 JavaFX 中显示的列表轻松:

ObservableList<PersonUI> secondList = FXCollections.observableArrayList(personUI -> new Observable[]{personUI.adultProperty()});
UIClassUtil.bindListContentPlatformRunLater(originalList, PersonUI::new, secondList);

所以现在的问题是:

我的期望是,当 secondList 显示在 ListView<PersonUI> 中时,它会在 Person#adultProperty 值更改时更新,但预期的行为仅持续几秒钟,之后 ListView停止更新,这是因为 secondList "update" 事件停止触发。我的猜测是 PersonUI#adultProperty 会在几秒钟后收集垃圾,因为它除了在提取器中没有在其他任何地方使用过??

为了重现该问题,我每隔几秒随机更改 Person#adultProperty 的值。没有 PersonPersonUI classes 的完整代码:

public final class ListViewExtractorTest extends Application {

    private static final ObservableList<Person> originalList = FXCollections.observableArrayList();
    private static final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();

    static {
        for ( int a = 0; a < 5; a++ ) {
            originalList.add(new Person(Math.random() > 0.5));
        }
    }

    @Override
    public void start(Stage stage) throws Exception {
        ObservableList<PersonUI> secondList = FXCollections.observableArrayList(personUI -> new Observable[]{personUI.adultProperty()});
        UIClassUtil.bindListContentPlatformRunLater(originalList, PersonUI::new, secondList);

        secondList.addListener((ListChangeListener<? super PersonUI>) change -> {
            while (change.next()) {
                if (change.wasUpdated()) {
                    System.out.println("List updated!");
                }
            }
        });
        ListView<PersonUI> listViewPerson = new ListView<>(secondList);
        ListView<PersonUI> listViewForceRefresh = new ListView<>(secondList);
        stage.setScene(new Scene(new HBox(5, listViewPerson, listViewForceRefresh)));
        stage.setTitle("Hello");
        stage.show();

        scheduledExecutorService.scheduleAtFixedRate(() -> {
            Platform.runLater(listViewForceRefresh::refresh);
        }, 0, 100, TimeUnit.MILLISECONDS);
    }


    public static void main(String[] args) {
        launch();
    }
}

所以我有两个 ListView:左边一个应该在 Person#adultProperty 值更改时更新,右边一个我每 100 毫秒强制刷新一次。起初,列表是同步的,但几秒钟后,只有右侧列表显示正确的值,左侧列表卡在相同的值上,"List updated" of ListChangeListener 仅在相同的前几秒内打印。因此,这意味着列表的 "update" 事件仅在相同的前几秒内触发。

然而,一旦我开始使用 PersonUI#adultProperty(使用另一个线程每隔几秒在控制台中打印一次),它就会开始按照预期的方式运行,所以我的猜测是如果不使用它就会被垃圾收集。

知道如何让 UI classes 的这些属性在 UI class 本身存在时起作用,或者以其他方式实现预期的行为吗?

防止 属性 过早被垃圾收集的一个简单修复是更新 PersonUI 构造函数中的侦听器:

person.adultProperty().addListener((observableValue, oldValue, newValue) -> {
    Platform.runLater(() -> this.adult.set(newValue));
});

至:

person.adultProperty().addListener((observableValue, oldValue, newValue) -> {
    this.adult.get()
    Platform.runLater(() -> this.adult.set(newValue));
});