带有提取器更新事件的 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
的值。没有 Person
和 PersonUI
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));
});
一点背景:
在我遇到问题之前 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
的值。没有 Person
和 PersonUI
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));
});