简体   繁体   English

从后台线程更新可观察列表的正确方法

[英]Correct Way to update Observable List from background thread

I'm trying to follow MVC for a test project, so my model should be completely independant from my view, however I'm not sure how I should update an Observable List which gets updated in a background thread (It's being given Strings about uploading files through FTP) so that the messages appear on the UI in a ListView. 我正在尝试按照MVC进行测试项目,因此我的模型应完全独立于我的视图,但是我不确定如何更新在后台线程中更新的Observable List(正在为字符串提供有关上传的信息)文件(通过FTP)),以便消息显示在UI的ListView中。

I am using JavaFX and trying to get my program as loosely coupled as possible. 我正在使用JavaFX,并尝试使我的程序尽可能松散地耦合。 At this current moment, the GUI in the view package is depending on the fact that my model updates my list using Platform.runLater(...) - which to my knowledge, my model should work completely independent from the view, and shouldn't have to conform to the View's needs. 目前,视图包中的GUI取决于我的模型使用Platform.runLater(...)更新列表的事实-据我所知,我的模型应该完全独立于视图而工作,并且不应不必符合View的需求。

Now the following code actually "works as intended" it's just not modelled correctly, and I'm not sure how I can model it correctly. 现在,以下代码实际上“按预期工作”,只是建模不正确,而且我不确定如何正确建模。 Some initial research brought up that I might have to use Observer and observable - and have another class in the middle to act as my observable list - but I'm not sure how I would set this up. 一些初步研究提出,我可能必须使用Observer和observable-并在中间使用另一个类作为我的可观察列表-但我不确定如何设置。

So I have an Observable list which is updated on a background thread: 所以我有一个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));

}

The FTPUtil class is my model. FTPUtil类是我的模型。

I also have a Model Manager class which is what controls this FTPUtil class: 我也有一个Model Manager类,它控制此FTPUtil类:

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

}

Finally is my GUI: 最后是我的GUI:

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


}

The basic setup: 基本设置:

  • do not use Platform.runLater in the model 不要在模型中使用Platform.runLater
  • do not set the model/manager's list of messages directly as items to the listView 不要将模型/经理的消息列表直接设置为listView的项
  • do keep a separate observableList of items and set that to the list 确实保留一个单独的observableList项目并将其设置为列表
  • install a listener on the manager's list that keeps the items in sync with the messages: wrap those modifications in Platform.runLater 在经理的列表上安装一个侦听器,以使项目与消息保持同步:将这些修改包装在Platform.runLater中

A very raw snippet to illustrate the setup: 一个非常原始的片段来说明设置:

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

You need a wrapper around the list that posts changes on the correct thread. 您需要在列表周围包装一个包装,以便在正确的线程上发布更改。

ObservableList<String> source = FXCollections.observableArrayList();
ObservableList<String> items = new UiThreadList(source);
ListView<String> list = new ListView<>(items);

with

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

The idea of this solution is similar to the one of @kleopatra above. 此解决方案的想法类似于上面的@kleopatra。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM