从后台线程更新可观察列表的正确方法
Correct Way to update Observable List from background thread
我正在尝试遵循 MVC 进行测试项目,所以我的模型应该完全独立于我的观点,但是我不确定我应该如何更新在后台线程中更新的可观察列表(它是被赋予有关通过 FTP 上传文件的字符串),以便消息显示在 ListView 的 UI 上。
我正在使用 JavaFX 并试图让我的程序尽可能松耦合。目前,视图包中的 GUI 取决于我的模型使用 Platform.runLater(...) 更新我的列表这一事实——据我所知,我的模型应该完全独立地工作从视图来看,不应该必须符合视图的需要。
现在下面的代码实际上 "works as intended" 它只是没有正确建模,我不确定如何才能正确建模。一些初步研究提出我可能必须使用 Observer 和 observable - 并且在中间有另一个 class 作为我的 observable 列表 - 但我不确定我将如何设置它。
所以我有一个在后台线程上更新的 Observable 列表:
private ObservableList<String> transferMessages;
public FTPUtil(String host, int port, String user, String pass) {
this.host = host;
this.port = port;
this.username = user;
this.password = pass;
transferMessages = FXCollections.observableArrayList();
connect();
}
public void upload(File src) {
System.out.println("Uploading: " + src.getName());
try {
if (src.isDirectory()) {
ftpClient.makeDirectory(src.getName());
ftpClient.changeWorkingDirectory(src.getName());
for (File file : src.listFiles()) {
upload(file);
}
ftpClient.changeToParentDirectory();
} else {
InputStream srcStream = null;
try {
addMessage("Uploading: " + src.getName());
srcStream = src.toURI().toURL().openStream();
ftpClient.storeFile(src.getName(), srcStream);
addMessage("Uploaded: " + src.getName() + " - Successfully.");
} catch (Exception ex) {
System.out.println(ex);
addMessage("Error Uploading: " + src.getName() + " - Speak to Administrator.");
}
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
private void addMessage(String message){
Platform.runLater(() -> transferMessages.add(0, message));
}
FTPUtil class是我的模型。
我还有一个模型管理器 class 控制这个 FTPUtil class:
public class ModelManager {
private ObservableList<String> fileAndFolderLocations;
private FTPUtil ftpUtil;
public ModelManager(String host, int port, String user, String pass) {
ftpUtil = new FTPUtil(host, port, user, pass);
fileAndFolderLocations = FXCollections.observableArrayList();
}
public boolean startBackup() {
Task task = new Task() {
@Override
protected Object call() throws Exception {
System.out.println("I started");
ftpUtil.clearMessages();
for(String location : fileAndFolderLocations){
File localDirPath = new File(location);
ftpUtil.upload(localDirPath);
}
return null;
}
};
new Thread(task).start();
return true;
}
public void addFileOrFolder(String fileOrFolder){
if(!fileAndFolderLocations.contains(fileOrFolder)){
fileAndFolderLocations.add(fileOrFolder);
}
}
public boolean removeFileOrFolder(String fileOrFolder){
return fileAndFolderLocations.remove(fileOrFolder);
}
public ObservableList<String> getFilesAndFoldersList() {
return fileAndFolderLocations;
}
public ObservableList<String> getMessages() {
return ftpUtil.getMessages();
}
}
终于是我的G了UI:
public class BackupController {
private Main main;
private ModelManager mm;
@FXML
private ListView<String> messagesList;
@FXML
void forceBackup(ActionEvent event) {
mm.startBackup();
}
public void initController(Main main, ModelManager mm) {
this.main = main;
this.mm = mm;
messagesList.setItems(mm.getMessages());
}
}
基本设置:
- 不要在模型中使用Platform.runLater
- 不要将 model/manager 的消息列表直接设置为列表视图的项目
- 保留一个单独的 observableList 项目并将其设置到列表中
- 在管理器的列表上安装一个侦听器,使项目与消息保持同步:将这些修改包装在 Platform.runLater
中
一个非常原始的片段来说明设置:
private Parent getContent() {
ModelManager manager = new ModelManager();
ObservableList<String> uploading = FXCollections.observableArrayList("one", "two", "three");
ObservableList<String> items = FXCollections.observableArrayList();
manager.getMessages().addListener((ListChangeListener) c -> {
while (c.next()) {
if (c.wasAdded()) {
Platform.runLater(() ->
items.addAll(c.getFrom(), c.getAddedSubList()));
}
if (c.wasRemoved()) {
Platform.runLater(() ->
items.removeAll(c.getRemoved()));
}
}
});
ListView<String> list = new ListView<>(items);
Button button = new Button("start");
button.setOnAction(ev -> {
uploading.stream().forEach(e -> manager.addFile(e));
manager.startBackup();
});
BorderPane pane = new BorderPane(list);
pane.setBottom(button);
return pane;
}
@Override
public void start(Stage stage) throws Exception {
Scene scene = new Scene(getContent());
stage.setScene(scene);
stage.show();
}
您需要对列表进行包装,以在正确的线程上发布更改。
ObservableList<String> source = FXCollections.observableArrayList();
ObservableList<String> items = new UiThreadList(source);
ListView<String> list = new ListView<>(items);
和
import javafx.application.Platform;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.transformation.TransformationList;
class UiThreadList<T> extends TransformationList<T, T> {
public UiThreadList(ObservableList<? extends T> source) {
super(source);
}
@Override
protected void sourceChanged(ListChangeListener.Change<? extends T> change) {
Platform.runLater(() -> fireChange(change));
}
@Override
public int getSourceIndex(int index) {
return index;
}
@Override
public T get(int index) {
return getSource().get(index);
}
@Override
public int size() {
return getSource().size();
}
}
这个解决方案的想法与上面@kleopatra 的想法相似。
我正在尝试遵循 MVC 进行测试项目,所以我的模型应该完全独立于我的观点,但是我不确定我应该如何更新在后台线程中更新的可观察列表(它是被赋予有关通过 FTP 上传文件的字符串),以便消息显示在 ListView 的 UI 上。
我正在使用 JavaFX 并试图让我的程序尽可能松耦合。目前,视图包中的 GUI 取决于我的模型使用 Platform.runLater(...) 更新我的列表这一事实——据我所知,我的模型应该完全独立地工作从视图来看,不应该必须符合视图的需要。
现在下面的代码实际上 "works as intended" 它只是没有正确建模,我不确定如何才能正确建模。一些初步研究提出我可能必须使用 Observer 和 observable - 并且在中间有另一个 class 作为我的 observable 列表 - 但我不确定我将如何设置它。
所以我有一个在后台线程上更新的 Observable 列表:
private ObservableList<String> transferMessages;
public FTPUtil(String host, int port, String user, String pass) {
this.host = host;
this.port = port;
this.username = user;
this.password = pass;
transferMessages = FXCollections.observableArrayList();
connect();
}
public void upload(File src) {
System.out.println("Uploading: " + src.getName());
try {
if (src.isDirectory()) {
ftpClient.makeDirectory(src.getName());
ftpClient.changeWorkingDirectory(src.getName());
for (File file : src.listFiles()) {
upload(file);
}
ftpClient.changeToParentDirectory();
} else {
InputStream srcStream = null;
try {
addMessage("Uploading: " + src.getName());
srcStream = src.toURI().toURL().openStream();
ftpClient.storeFile(src.getName(), srcStream);
addMessage("Uploaded: " + src.getName() + " - Successfully.");
} catch (Exception ex) {
System.out.println(ex);
addMessage("Error Uploading: " + src.getName() + " - Speak to Administrator.");
}
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
private void addMessage(String message){
Platform.runLater(() -> transferMessages.add(0, message));
}
FTPUtil class是我的模型。
我还有一个模型管理器 class 控制这个 FTPUtil class:
public class ModelManager {
private ObservableList<String> fileAndFolderLocations;
private FTPUtil ftpUtil;
public ModelManager(String host, int port, String user, String pass) {
ftpUtil = new FTPUtil(host, port, user, pass);
fileAndFolderLocations = FXCollections.observableArrayList();
}
public boolean startBackup() {
Task task = new Task() {
@Override
protected Object call() throws Exception {
System.out.println("I started");
ftpUtil.clearMessages();
for(String location : fileAndFolderLocations){
File localDirPath = new File(location);
ftpUtil.upload(localDirPath);
}
return null;
}
};
new Thread(task).start();
return true;
}
public void addFileOrFolder(String fileOrFolder){
if(!fileAndFolderLocations.contains(fileOrFolder)){
fileAndFolderLocations.add(fileOrFolder);
}
}
public boolean removeFileOrFolder(String fileOrFolder){
return fileAndFolderLocations.remove(fileOrFolder);
}
public ObservableList<String> getFilesAndFoldersList() {
return fileAndFolderLocations;
}
public ObservableList<String> getMessages() {
return ftpUtil.getMessages();
}
}
终于是我的G了UI:
public class BackupController {
private Main main;
private ModelManager mm;
@FXML
private ListView<String> messagesList;
@FXML
void forceBackup(ActionEvent event) {
mm.startBackup();
}
public void initController(Main main, ModelManager mm) {
this.main = main;
this.mm = mm;
messagesList.setItems(mm.getMessages());
}
}
基本设置:
- 不要在模型中使用Platform.runLater
- 不要将 model/manager 的消息列表直接设置为列表视图的项目
- 保留一个单独的 observableList 项目并将其设置到列表中
- 在管理器的列表上安装一个侦听器,使项目与消息保持同步:将这些修改包装在 Platform.runLater 中
一个非常原始的片段来说明设置:
private Parent getContent() {
ModelManager manager = new ModelManager();
ObservableList<String> uploading = FXCollections.observableArrayList("one", "two", "three");
ObservableList<String> items = FXCollections.observableArrayList();
manager.getMessages().addListener((ListChangeListener) c -> {
while (c.next()) {
if (c.wasAdded()) {
Platform.runLater(() ->
items.addAll(c.getFrom(), c.getAddedSubList()));
}
if (c.wasRemoved()) {
Platform.runLater(() ->
items.removeAll(c.getRemoved()));
}
}
});
ListView<String> list = new ListView<>(items);
Button button = new Button("start");
button.setOnAction(ev -> {
uploading.stream().forEach(e -> manager.addFile(e));
manager.startBackup();
});
BorderPane pane = new BorderPane(list);
pane.setBottom(button);
return pane;
}
@Override
public void start(Stage stage) throws Exception {
Scene scene = new Scene(getContent());
stage.setScene(scene);
stage.show();
}
您需要对列表进行包装,以在正确的线程上发布更改。
ObservableList<String> source = FXCollections.observableArrayList();
ObservableList<String> items = new UiThreadList(source);
ListView<String> list = new ListView<>(items);
和
import javafx.application.Platform;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.transformation.TransformationList;
class UiThreadList<T> extends TransformationList<T, T> {
public UiThreadList(ObservableList<? extends T> source) {
super(source);
}
@Override
protected void sourceChanged(ListChangeListener.Change<? extends T> change) {
Platform.runLater(() -> fireChange(change));
}
@Override
public int getSourceIndex(int index) {
return index;
}
@Override
public T get(int index) {
return getSource().get(index);
}
@Override
public int size() {
return getSource().size();
}
}
这个解决方案的想法与上面@kleopatra 的想法相似。