MVC: Best Way to Browse a Directory for Changes
Context:
I am new to JavaFX but I am trying to create an application, one of which is the basic function is to show the user all folders in a specific directory and automatically update the view when new folders or folders are deleted in that directory. These folders can be thought of as objects (for example, in a ListView) and the user should be able to interact with them. I want to create this application using MVC architecture.
What I have done so far:
So I have a view (fxml), a controller class, and my model that handles my application logic. I am using the WatchDir example from Oracle as a helper class in my model and starts 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's processEvents () method, I can do something like:
if (!recursive && (kind == ENTRY_CREATE)) {
// new folder was created
}
My question is:
What's the best / most elegant way to tell my controller that something has changed and update the ListView? I want to keep it MVC style.
Another approach is also welcome :)
source to share
The approach I would like to use would be to provide methods in WatchDir
for registering callbacks. The easiest way to do this is to use Consumer
properties for 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 this WatchDir.processEvents()
is a (non-terminating) blocking call, so you need to run it on 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();
Please note, since the callback will be called on a background thread, updates to the UI must be wrapped in Platform.runLater(...)
. If you like, you can equip WatchDir
Executor
to do callbacks, which allows you to tell about it only 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();
source to share