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.
I am using JavaFX and trying to get my program as loosely coupled as possible. 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.
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.
So I have an Observable list which is updated on a background thread:
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.
I also have a Model Manager class which is what controls this 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();
}
}
Finally is my 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:
A very raw snippet to illustrate the setup:
You need a wrapper around the list that posts changes on the correct thread.
with
The idea of this solution is similar to the one of @kleopatra above.