Context:
I am newbie at JavaFX, but I'm trying to build an application which one of it's basic feature is to show the user all folders in certain directory and automatically update the view when there are new folders or folders deleted in that directory. These folders can be considered as objects (in a ListView for example) and the user should be able to interact with them.
I want to build that application using MVC architecture.
What I've done so far:
So I have a view (fxml), a controller class and my model which handles my app logic. I use the WatchDir example from Oracle as a helper class in my model and starting the WatchService in the controller like this:
@Override
public void initialize(URL location, ResourceBundle resources) {
this.config = loadConfig(configFile);
this.facade = new facade(config);
Path rootPath = Paths.get(config.getDlRootPath());
try {
// register WatchService
new WatchDir(rootPath, false).processEvents();
statusText(rootPath + "is now being watched for changes");
} catch (IOException e) {
statusError("Directory " + e.getLocalizedMessage() + " does not exist.");
}
}
In WatchDir method processEvents() I can do something like:
if (!recursive && (kind == ENTRY_CREATE)) {
// new folder was created
}
My Question:
What is the best/most elegant way of telling my controller that something has changed and to update the ListView? I want to keep it MVC style.
Different approach is also welcome :)
The approach I would use would be to provide methods in WatchDir
for registering callbacks. The easiest way to do this is just to use Consumer
properties for the callbacks:
public class WatchDir {
private Consumer<Path> onCreate ;
public void setOnCreate(Consumer<Path> onCreate) {
this.onCreate = onCreate ;
}
// other callbacks as needed...
// ...
void processEvents() {
// ...
Path child = ... ; // file/folder that was created
if (! recursive && kind == ENTRY_CREATE) {
if (onCreate != null) {
onCreate.accept(child);
}
}
// ...
}
// ...
}
Note that WatchDir.processEvents()
is a (non-terminating) blocking call, so you need to run it in a background thread. So from your controller you do:
WatchDir watchDir = new WatchDir(rootPath, false) ;
watchDir.setOnCreate(path ->
Platform.runLater(() -> listView.getItems().add(path)));
Thread watchThread = new Thread(watchDir::processEvents);
watchThread.setDaemon(true);
watchThread.start();
Note that since the callback will be invoked on a background thread, updates to the UI should be wrapped in a Platform.runLater(...)
. If you like, you can equip WatchDir
with an Executor
for executing callbacks, which would allow you to tell it just once to always execute them via Platform.runLater(...)
:
public class WatchDir {
// Executor for notifications: by default just run on the current thread
private Executor notificationExecutor = Runnable::run ;
public void setNotificationExecutor(Executor notificationExecutor) {
this.notificationExecutor = notificationExecutor ;
}
private Consumer<Path> onCreate ;
// etc...
void processEvents() {
// ...
if (! recursive && kind == ENTRY_CREATE) {
if (onCreate != null) {
notificationExecutor.execute(() -> onCreate.accept(child));
}
}
// ...
}
}
and then
WatchDir watchDir = new WatchDir(rootPath, false) ;
watchDir.setNotificationExecutor(Platform::runLater);
watchDir.setOnCreate(listView.getItems()::add); /* or path -> listView.getItems().add(path) */
Thread watchThread = new Thread(watchDir::processEvents);
watchThread.setDaemon(true);
watchThread.start();