A Service
provides the ability to "reuse" a Task
. I put "reuse" in quotes because what's really happening is the Service
creates a new Task
each time it's started. For more information about the differences between, and usages of, Service
and Task
you can:
As a Service
is meant to be reused you should only create one instance. It also maintains state so it can only be executed "once at a time". In other words, the same Service
can't execute multiple times in parallel. When the Service
completes it will either be in the SUCCEEDED
, CANCELLED
, or FAILED
states; to start the Service
again you must either call restart()
(will cancel a running Service
) or call reset()
before calling start()
again.
While the Service
is running you'd want to disable certain UI components so that the user cannot try to start it multiple times. You'd do this through listeners and/or bindings. If needed, you can also put checks in so that your code won't attempt to start the Service
if it is already running. Whether or not those checks are needed depends on what code can start the Service
and how it can be executed.
Here's a small example. It uses FXML to create the interface but the important parts are the LoginController
and LoginService
classes. Depending on your application, you may also want to add a way to cancel the login.
Main.java
package com.example;
import java.io.IOException;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class Main extends Application {
@Override
public void start(Stage primaryStage) throws IOException {
// Login.fxml is in the same package as this class
Parent root = FXMLLoader.load(getClass().getResource("Login.fxml"));
primaryStage.setScene(new Scene(root));
primaryStage.setTitle("Service Example");
primaryStage.show();
}
}
LoginService.java
package com.example;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
public class LoginService extends Service<Boolean> {
private final StringProperty username = new SimpleStringProperty(this, "username");
public final void setUsername(String username) { this.username.set(username); }
public final String getUsername() { return username.get(); }
public final StringProperty usernameProperty() { return username; }
private final StringProperty password = new SimpleStringProperty(this, "password");
public final void setPassword(String password) { this.password.set(password); }
public final String getPassword() { return password.get(); }
public final StringProperty passwordProperty() { return password; }
@Override
protected Task<Boolean> createTask() {
return new LoginTask(getUsername(), getPassword());
}
private static class LoginTask extends Task<Boolean> {
private final String username;
private final String password;
public LoginTask(String username, String password) {
this.username = username;
this.password = password;
}
@Override
protected Boolean call() throws Exception {
Thread.sleep(3_000L); // simulate long running work...
return !isCancelled() && "root".equals(username) && "root".equals(password);
}
}
}
LoginController.java
package com.example;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.concurrent.Worker;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.Cursor;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.PasswordField;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
public class LoginController {
@FXML private GridPane root;
@FXML private TextField userField;
@FXML private PasswordField passField;
@FXML private Button loginBtn;
private LoginService service;
@FXML
private void initialize() {
service = new LoginService();
service.usernameProperty().bind(userField.textProperty());
service.passwordProperty().bind(passField.textProperty());
// Don't let user interact with UI while trying to login
BooleanBinding notReadyBinding = service.stateProperty().isNotEqualTo(Worker.State.READY);
userField.disableProperty().bind(notReadyBinding);
passField.disableProperty().bind(notReadyBinding);
loginBtn.disableProperty().bind(notReadyBinding);
root.cursorProperty().bind(
Bindings.when(service.runningProperty())
.then(Cursor.WAIT)
.otherwise(Cursor.DEFAULT)
);
service.setOnSucceeded(event -> serviceSucceeded());
service.setOnFailed(event -> serviceFailed());
}
private void serviceSucceeded() {
if (service.getValue()) {
/*
* Normally you'd change the UI here to show whatever the user needed to
* sign in to see. However, to allow experimentation with this example
* project we simply show an Alert and call reset() on the LoginService.
*/
showAlert(Alert.AlertType.INFORMATION, "Login Successful", "You've successfully logged in.");
service.reset();
} else {
showAlert(Alert.AlertType.ERROR, "Login Failed", "Your username or password is incorrect.");
service.reset();
}
}
private void serviceFailed() {
showAlert(Alert.AlertType.ERROR, "Login Failed", "Something when wrong while trying to log in.");
service.getException().printStackTrace();
service.reset();
}
private void showAlert(Alert.AlertType type, String header, String content) {
Alert alert = new Alert(type);
alert.initOwner(root.getScene().getWindow());
alert.setHeaderText(header);
alert.setContentText(content);
alert.showAndWait();
}
@FXML
private void handleLogin(ActionEvent event) {
event.consume();
// isBlank() is a String method added in Java 11
boolean blankUsername = userField.textProperty().getValueSafe().isBlank();
boolean blankPassword = passField.textProperty().getValueSafe().isBlank();
if (blankUsername || blankPassword) {
showAlert(Alert.AlertType.ERROR, null, "Both username and password must be specified.");
} else {
service.start();
}
}
}
Login.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.PasswordField?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.RowConstraints?>
<?import javafx.scene.text.Font?>
<GridPane fx:id="root" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" vgap="20.0"
xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/10.0.1"
fx:controller="com.example.LoginController">
<columnConstraints>
<ColumnConstraints hgrow="SOMETIMES" minWidth="-Infinity" percentWidth="50.0"/>
<ColumnConstraints halignment="RIGHT" hgrow="SOMETIMES" minWidth="-Infinity" percentWidth="50.0"/>
</columnConstraints>
<rowConstraints>
<RowConstraints minHeight="-Infinity" percentHeight="25.0" vgrow="SOMETIMES"/>
<RowConstraints minHeight="-Infinity" percentHeight="25.0" vgrow="SOMETIMES"/>
<RowConstraints minHeight="-Infinity" percentHeight="25.0" vgrow="SOMETIMES"/>
<RowConstraints minHeight="-Infinity" percentHeight="25.0" vgrow="SOMETIMES"/>
</rowConstraints>
<children>
<Button fx:id="loginBtn" defaultButton="true" mnemonicParsing="false" onAction="#handleLogin" text="Login"
GridPane.columnIndex="1" GridPane.rowIndex="3"/>
<Label minWidth="-Infinity" text="Welcome">
<font>
<Font name="Segoe UI" size="32.0"/>
</font>
</Label>
<TextField fx:id="userField" prefColumnCount="20" promptText="Username" GridPane.columnSpan="2"
GridPane.rowIndex="1"/>
<PasswordField fx:id="passField" prefColumnCount="20" promptText="Password" GridPane.columnSpan="2"
GridPane.rowIndex="2"/>
</children>
<padding>
<Insets bottom="50.0" left="50.0" right="50.0" top="50.0"/>
</padding>
</GridPane>