At your Service!
The Service class (javafx.concurrency.Service) may not only be used to keep the UI reactive on long duration tasks but can also be used to change the state of controls or keep UI in a reasonable state on short term actions.
Recall that a Service
is like a reusable Task
. A Task
can be executed once and has to be re-instantiated to be started again. The Service
class creates a new Task
each time it is (re-)started (createTask()
is called then).
This is the start()
-method from javafx.concurrency.Service
class:
public void start() { checkThread(); if (getState() != State.READY) { throw new IllegalStateException( "Can only start a Service in the READY state. Was in state " + getState()); } // Create the task task = createTask(); // Wire up all the properties so they use this task state.bind(task.stateProperty()); value.bind(task.valueProperty()); exception.bind(task.exceptionProperty()); workDone.bind(task.workDoneProperty()); totalWorkToBeDone.bind(task.totalWorkProperty()); progress.bind(task.progressProperty()); running.bind(task.runningProperty()); message.bind(task.messageProperty()); title.bind(task.titleProperty()); // Advance the task to the "SCHEDULED" state task.setState(State.SCHEDULED); // Start the task executeTask(task); }
Note that the properties binding of the Task
(reset()
is cleaning up all stuff).
In this post want to show how I’m using Services together with a JavaFX UI, like in MQTT.fx to connect or disconnect to a MQTT broker.
A simple connection pane with 5 controls:
Wanted appearance / behavior:
connectButton
: only enabled if: not connected AND no service is running
disconnectButton
: only enabled if: connected AND no service is running
cancelButton & progressIndicator
: only visible if: any service is running
serviceMessageLabel
: user feedback about the states of the services and the application
The controller has two properties and contains two services (inner classes):
StringProperty statusMessagesProperty
BooleanProperty connectedProperty
ConnectService
private class ConnectService extends Service<Void> { @Override protected void succeeded() { statusMessagesProperty().set("Connected."); connectedProperty().set(true); } @Override protected void failed() { statusMessagesProperty().set("Connecting failed."); LOGGER.severe(getException().getMessage()); connectedProperty().set(false); } @Override protected void cancelled() { statusMessagesProperty().set("Connecting cancelled."); connectedProperty().set(false); } @Override protected Task<Void> createTask() { return new Task<Void>() { @Override protected Void call() throws Exception { updateMessage("Connecting...."); Thread.sleep(1000); // DEMO: un-comment to provoke "Not on FX application thread"-Exception: // connectButton.setVisible(false); updateMessage("Waiting for server feedback."); Thread.sleep(2000); return null; } }; } }
DisconnectService
private class DisconnectService extends Service<Void> { @Override protected void succeeded() { statusMessagesProperty().set(""); connectedProperty().set(false); } @Override protected void cancelled() { statusMessagesProperty().set("Disconnecting cancelled."); connectedProperty().set(false); } @Override protected Task<Void> createTask() { return new Task<Void>() { @Override protected Void call() throws Exception { updateMessage("Disconnecting...."); Thread.sleep(1000); updateMessage("Waiting for server feedback."); Thread.sleep(2000); return null; } }; } }
The wanted UI-behavior can be accomplished just by some Bindings
BooleanBinding anyServiceRunning = connectService.runningProperty().or(disconnectService.runningProperty());
serviceRunningIndicator.visibleProperty().bind(anyServiceRunning);
cancelButton.visibleProperty().bind(anyServiceRunning);
connectButton.disableProperty().bind(connectedProperty().or(anyServiceRunning));
disconnectButton.disableProperty().bind(connectedProperty().not().or(anyServiceRunning));
messagesLabel.textProperty().bind(statusMessagesProperty());
ChangeListeners are passing the messages from the services to the statusMessagesProperty:
connectService.messageProperty().addListener((ObservableValue extends String> observableValue, String oldValue, String newValue) -> {
statusMessagesProperty().set(newValue);
});
disconnectService.messageProperty().addListener((ObservableValue extends String> observableValue, String oldValue, String newValue) -> {
statusMessagesProperty().set(newValue);
});
Three methods to control the services:
@FXML public void cancel() { LOGGER.info("cancel"); connectService.cancel(); disconnectService.cancel(); } @FXML public void connect() { LOGGER.info("connect"); disconnectService.cancel(); connectService.restart(); } @FXML public void disconnect() { LOGGER.info("disconnect"); connectService.cancel(); disconnectService.restart(); }
Nice & convenient:
Instead of verifying the state of the Service
you can just call restart()
.
restart()
executes:
1. cancel()
(if running): cancel the executed service-task
2. reset()
: clear the Service
(task=null and unbind all stuff)
and then
3. start()
: create new Task
and bind its properties
You can get the complete code of this tutorial in my BitBucket repo:
ServiceDemoApp
https://bitbucket.org/Jerady/servicedemoapp/src
This is a great JavaFX example, Jens. I’m learning quite a lot from your explanations. I have a unique challenge that I have have not yet been able to resolve. I am using a Service to print a receipt, and it works quite well for only one page. However, I have an array of Nodes (VBox) that I need to print on separate pages. I have not had any luck resetting the Service so I can iterate through the remaining receipts, printing them one at a time. I’ve tried using a While Loop but it freezes the application. Using the setOnSucceeded(…), setOnScheduled(…) methods don’t work either. How would you approach this scenario?
Solved it! In reading back through your blog entry, Jens, I noticed the methods overriding the Service states, specifically suceeded() and cancelled(). I implemented the succeeded() method in my Service class and was able to split the shopping cart items to separate pages on the receipt printer. The key was having a current index as a class member, incremented after each successful print. The endJob() method is called when the Task finishes printing and the Service enters the SUCCEEDED state. The Service lifecycle calls the succeeded() method automatically. I test the index against the total count and call the Service restart() method if the index < total count. Thank you, Jens, your blog entry was an important part of realizing this solution.