A toggle button in a JavaFX operation will be accessed by 2 separate threads.
1. One thread will be invoked as soon as user clicks (toggles button state) and will a) do something in the OS b) check if (a) succeeded c) exit on success / exit and return toggle button to previous state on failure
2 The other thread will monitor events asynchronous to the previous operation(s) and in case of a particular event it will change the button state.
Do I need to provide synchronization between threads 1 and 2 in terms of locking the button state?
EDIT: The idea proposed by James_D seems reasonable, but I just wanted to propose an alternative (whose effectiveness remains to be proved however). How about using synchronized code blocks, and using as lock the reference to the particular button, i.e. something like:
// getting the reference to the button
@FXML
private ToggleButton tButtonToBeSynchronized
// Thread1
synchronized(tButtonToBeSynchronized) {
// do stuff with button upon user click
}
// Thread2
synchronized(tButtonToBeSynchronized) {
// poll system every X seconds
// when asynchronous event occurs (not related to UI events)
// update tButtonToBeSynchronized state
}
Would that work in case these are called by different Controller classes? (assuming the reference to the tButtonToBeSynchronized
is passed by reference - and not by value by the FXML framework?
Like most UI toolkits, JavaFX assumes a single threaded model. You should only ever access the state of nodes that are part of a scene graph from the FX Application Thread. So, toggling a button is not an atomic operation, and the code you describe is not guaranteed to work as you currently have it set up. In Java 8, it will likely throw a RuntimeException
.
JavaFX provides functionality to enable interoperability with background threads. The lowest level of these is Platform.runLater(Runnable r)
, which executes r
on the FX Application Thread. So, your monitor thread (item 2 in your question) should change the state of the toggle button with
Platform.runLater( () -> toggleButton.setSelected(...) );
There is also a javafx.concurrent
API. This provides a Task
class, among others, which acts as both a Runnable
and a java.util.concurrent.FutureTask
, and additionally has a collection of callback methods for submitting code to be executed on the FX Application Thread at various points in the Task
's lifecycle.
So you should implement item 1 in your question as:
ExecutorService exec = ... ; // e.g. Executors.newCachedThreadPool();
toggleButton.selectedItemProperty().addListener((obs, wasSelected, isNowSelected) -> {
if (isNowSelected) {
Task<Void> task = new Task<Void>() {
@Override
public Void call() throws Exception {
// do something on OS
// throw exception if failed
return null ;
}
};
task.setOnFailed(event -> toggleButton.setSelected(wasSelected));
exec.submit(task);
}
});
If you prefer to return a value indicating success or failure, you can do
Task<Boolean> task = new Task<Boolean>() {
@Override
public Boolean call() {
// do work...
boolean successful = ... ;
return successful ;
}
};
task.setOnSucceeded( event -> {
boolean wasSuccessful = task.getValue();
// ...
});