Search code examples
javafxjavafx-11

Updating a JavaFX status label


I have a JavaFX program using Model/View/Controller where I want a long running model to update a status label on the view. I found people suggesting using the Timeline class to do this. I implemented it expecting that every second, the status Label would update. However, only the final status displays. What am I doing wrong?

My controller looks like:

@FXML
private Button pullApplicantsButton;
@FXML
private Label statusLabel;
@FXML
private DatePicker orientationDate;
@FXML
private Spinner numberOfApplicants;

@FXML
private void pullApplicants() throws Exception {

    SelectApplicantsModel selectApplicantsModel = new SelectApplicantsModel(orientationDate.getValue() , ( int ) numberOfApplicants.getValue() , this.statusLabel);
    selectApplicantsModel.process();
}

my model looks like:

public SelectApplicantsModel(LocalDate nextOrientationDate, int numberOfApplicants , Label statusLabel ) throws FileNotFoundException {

    this.nextOrientationDate = nextOrientationDate;
    this.numberOfApplicants = numberOfApplicants;
    this.statusLabel = statusLabel;
}

public void process() throws Exception {

    Timeline timeline = new Timeline(
            new KeyFrame(Duration.seconds( 1 ) , event -> {
                statusLabel.setText( programStatus );
            })
    );
    timeline.setCycleCount( Animation.INDEFINITE );
    timeline.play();
    programStatus = "starting";
    changeSearchStringToIncludeOrientationDate(nextOrientationDate);
    MemberClicks memberClicks = new MemberClicks();
    programStatus = "retrieving profiles";
    JsonArray applicantProfilesJsonArray = memberClicks.getProfiles(searchJsonArray);
    programStatus = "converting profiles";

and the view looks like:

  <Label text="Picks the next 30 applicants for the upcoming orientation.  Applicants whose Memberclick's OrientationDate matches the next orientation date get priority, followed by those with the oldest normalized application date." wrapText="true" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.rowIndex="0" />
  <Label text="Date of next orientation:" GridPane.columnIndex="0" GridPane.rowIndex="1" />
  <DatePicker fx:id="orientationDate" editable="false" GridPane.columnIndex="1" GridPane.rowIndex="1" />
  <Label text="Number of applicants to pull:" GridPane.columnIndex="0" GridPane.rowIndex="2" />
  <Spinner fx:id="numberOfApplicants" editable="false" GridPane.columnIndex="1" GridPane.rowIndex="2" />
  <Button fx:id="pullApplicantsButton" mnemonicParsing="false" onAction="#pullApplicants" text="Pull Applicants" GridPane.columnIndex="0" GridPane.rowIndex="4" />
  <Button fx:id="closeWindowButton" mnemonicParsing="false" onAction="#closeWindow" text="Close Window" GridPane.columnIndex="1" GridPane.rowIndex="4" />
  <Label fx:id="statusLabel" text="" wrapText="true" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.rowIndex="5" />


Solution

  • You can use a Task for your purpose which does all the work in a background thread so that the GUI thread will not be blocked. Here is a minimal example:

    Controller Class:

    package sample;
    
    import javafx.fxml.FXML;
    import javafx.scene.control.Label;
    
    public class Controller {
    
        @FXML
        private Label statusLabel;
    
        @FXML
        public void handleStartBtnClick() {
            MyTask myTask = new MyTask();
            statusLabel.textProperty().bind(myTask.messageProperty());
            new Thread(myTask).start();
        }
    }
    

    MyTask Class:

    package sample;
    
    import javafx.concurrent.Task;
    
    public class MyTask extends Task<Void> {
    
        @Override
        protected Void call() throws Exception {
    
            updateMessage("starting");
    
            // while (...) {
            // do something:
            // changeSearchStringToIncludeOrientationDate(nextOrientationDate);
            // MemberClicks memberClicks = new MemberClicks();
    
            Thread.sleep(1000); // just for demonstration purpose
    
            // Update the status:
            updateMessage("retrieving profiles");
    
            Thread.sleep(1000);
    
            // Do next step:
            // ...
            updateMessage("converting profiles");
            Thread.sleep(1000);
    
            // } End of while loop
    
            return null;
        }
    
        @Override
        protected void succeeded() {
            updateMessage("succeeded");
        }
    }
    

    FXML File:

    <?xml version="1.0" encoding="UTF-8"?>
    
    <?import javafx.scene.control.Button?>
    <?import javafx.scene.control.Label?>
    <?import javafx.scene.layout.VBox?>
    
    
    <VBox xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sample.Controller">
        <children>
            <Button onAction="#handleStartBtnClick" text="Start background task"/>
            <Label fx:id="statusLabel" text="Status"/>
        </children>
    </VBox>