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:

connectionPane

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

start

connecting

connected

disconnecting

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

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

3 thoughts on “At your Service!”

  1. 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?

    1. 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.