0

I have a button that is taking my username and password from the fields and is sending a request to the backend for authentication. I am using a thread so my button animation and scene don't freeze like so:

Service<Void> service = new Service<Void>() {
            @Override
            protected Task<Void> createTask() {
                return new Task<Void>() {
                    @Override
                    protected Void call() throws Exception {
                        //do authentication
                        if(responseStatus != 200){
                            authenticated = false 
                        }else{
                            authenticate = true
                        }
                        Platform.runLater(() -> {
                        try{
                            if(authenticated) {
                                changeScene();
                            }
                        }finally{
                            latch.countDown();
                        }
                    });
                    latch.await();
                    return null;
                }
            };
        }
    };
    service.start();

private void changeScene(){
    try {
        Stage window = (Stage)loginPane.getScene().getWindow();
        LoggedFirstStyle.displayLoggedScene();
        window.close();

    } catch (IOException e) {
        e.printStackTrace();
    }
}

but the things is that platform run later is executing several times if I click the button multiple times and so is changeScene and several scenes are opened. Is the way I am doing this alright and if so how can I prevent from opening several threads in the same method?

  • 1
    Please show how you're using `Service` in the greater context of your application. It looks like you're creating a new instance every time which is not the correct usage. That said, a solution is to disable the `Button` while the `Service` is running (or just after the first click), as that prevents the user from firing the button again. – Slaw Feb 22 '19 at 18:10
  • @Slaw What is the correct usage? Do you mean that I should make a new class that extends service and initialize it as a global variable? I mean I can do it with anonyms class in the global variables but it kinda looks bad. Also when I do so it throws IllegalStateException: "Can only start a Service in the READY state. Was in state RUNNING" should I just catch it? –  Feb 22 '19 at 18:24
  • @Slaw I can also synchronize the body method of the login event listener and make an if statement with the authenticated and say to get in only if its not already authenticated, but I feel that it's not right way to do it like this. So is extending the abstract *Service* and use the same instance the best way? –  Feb 22 '19 at 18:25

1 Answers1

1

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>
Slaw
  • 37,820
  • 8
  • 53
  • 80