Search code examples
javajavafxmodel-view-controllerobserver-pattern

Javafx from a thread notify events to controller class for setting Label,Textfield based on model


In thread I run some code and I need update javafx ui elements that are in JsonOverviewController. I don't want to pass ui elements in model or class controller. I don't want this

        jfWatch = new WatchController();
        jfWatch.setPathDirectory(absPathSelDir);
        jfWatch.setMyJsonFilesTable(myJsonFilesTable);
        jfWatch.setMyIdTableColumn(myIdTableColumn);
        ...
        
        jfWatch.setMyStateLabel(myStateLabel);
        
        jfWatch.start();

The code below show that Label.TextField are pass to thread and in run method update them.

package it.einaudi.storejson;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.sql.SQLException;

import com.fasterxml.jackson.core.JsonParseException;

import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.stage.DirectoryChooser;
import javafx.stage.Stage;
import it.einaudi.storejson.model.ConnectionConfig;
import it.einaudi.storejson.model.JsonFile;
import it.einaudi.storejson.model.JsonFileManager;

public class JsonOverviewController {
    
    @FXML
    private Button myButton;
    
    @FXML
    private Label myDirWatch;
    
    @FXML
    private Label myCurrentJsonFileId;
    
    @FXML
    private Label myCurrentJsonFileName;
    
    @FXML
    private TextArea myCurrentJsonFileContent;
    
    @FXML
    private Button myImportButton;
    
    @FXML
    private TableView<JsonFile> myJsonFilesTable;
    
    @FXML
    private TableColumn<JsonFile, String> myIdTableColumn;
    
    @FXML
    private TableColumn<JsonFile, String> myNameFileTableColumn;
    
    @FXML
    private TableColumn<JsonFile, String> myStateTableColumn;
    
    @FXML
    private TextField myConnFullurlField;
    
    @FXML
    private TextField myConnUsernameField;
    
    @FXML
    private TextField myConnPasswordField;
    
    @FXML
    private TextField myConnNamedbField;
    
    @FXML
    private Label myStateLabel;
    
    //private JsonWatchService threadJsonWatch;
    private WatchController jfWatch;
    
    private JsonFileManager jfManager;
    
    /**
     * The constructor (is called before the initialize()-method).
     */
    public JsonOverviewController() {       
        //threadJsonWatch = null;
        jfManager = null;
        jfWatch = null;
    }
    
    
    @FXML
    private void handleButtonAction(ActionEvent event) {
        // Button was clicked, do something...
        System.out.println("myButton premuto");
        DirectoryChooser directoryChooser = new DirectoryChooser();
        Node node = (Node) event.getSource();
        File selectedDirectory;
        Stage thisStage = (Stage) node.getScene().getWindow();
        String absPathSelDir;
        
        selectedDirectory = directoryChooser.showDialog(thisStage);
        if(selectedDirectory == null ) return;
        
        absPathSelDir = selectedDirectory.getAbsolutePath();
        
        myDirWatch.setText(absPathSelDir);
            
        if(jfWatch != null) jfWatch.interrupt();
        
        jfWatch = new WatchController();
        jfWatch.setPathDirectory(absPathSelDir);
        jfWatch.setMyJsonFilesTable(myJsonFilesTable);
        jfWatch.setMyIdTableColumn(myIdTableColumn);
        jfWatch.setMyNameFileTableColumn(myNameFileTableColumn);
        jfWatch.setMyStateTableColumn(myStateTableColumn);
        
        jfWatch.setMyCurrentJsonFileId(myCurrentJsonFileId);
        jfWatch.setMyCurrentJsonFileName(myCurrentJsonFileName);
        jfWatch.setMyCurrentJsonFileContent(myCurrentJsonFileContent);
        
        jfWatch.setMyStateLabel(myStateLabel);
        
        jfWatch.start();
        
        myStateLabel.setText("La cartella " + 
                             absPathSelDir + 
                             " è monitorata.");    
    }
    
    /**
     * Initializes the controller class. This method is automatically called
     * after the fxml file has been loaded.
     */
    
    /**
     * Initializes the controller class. This method is automatically called
     * after the fxml file has been loaded.
     */
    @FXML
    private void initialize() {
        // Handle Button event.
        myButton.setOnAction(this::handleButtonAction);
        
        myIdTableColumn.setCellValueFactory(new PropertyValueFactory<JsonFile,String>("idFile"));
        myNameFileTableColumn.setCellValueFactory(new PropertyValueFactory<JsonFile,String>("nameFile"));
        myStateTableColumn.setCellValueFactory(new PropertyValueFactory<JsonFile,String>("stateFile"));
        
    }
    
}

public class WatchController extends Thread {
    
    private String pathDirectory;
    
    private Label myCurrentJsonFileId;
    
    private Label myCurrentJsonFileName;
    
    private TextArea myCurrentJsonFileContent; 
    
    private TableView<JsonFile> myJsonFilesTable;
    
    private TableColumn<JsonFile, String> myIdTableColumn;
    
    private TableColumn<JsonFile, String> myNameFileTableColumn;
    
    private TableColumn<JsonFile, String> myStateTableColumn;
    
    private Label myStateLabel;

    public void run() {
        if(!found) {
            Platform.runLater(() -> myCurrentJsonFileId.setText(""));
            Platform.runLater(() -> myCurrentJsonFileName.setText(""));
            Platform.runLater(() -> myCurrentJsonFileContent.setText("Anteprima file json non disponibile."));  
        }else {
            final JsonFile firstJCopy = firstJ;
            Platform.runLater(() -> myCurrentJsonFileId.setText(firstJCopy.getIdFile()));
            Platform.runLater(() -> myCurrentJsonFileName.setText(firstJCopy.getNameFile()));
            
            fullPathFile = pathDirectory + "\\" + firstJ.getNameFile();
            System.out.println(fullPathFile);
            strJ = readFileAsList(fullPathFile);
            String strJCopy = strJ;
            Platform.runLater(() -> myCurrentJsonFileContent.setText(strJCopy));
        }
    }
}

On the contrary I want that my thread notify to controller that has occurred and send to controller some data of model. Thus in controller class based on data model a set Label,Textfield correctly.


Solution

  • In the standard MVC architecture, your model should contain a list of listeners, and should notify those listeners of events as needed. Your controller (which in JavaFX with FXML is more like a MVP presenter) should be a listener and should register itself with the model.

    So:

    public interface WatchListener {
        public void startedProcessing();
        public void fileProcessed(String data);
        public void notFound();
    }
    
    public class WatchController extends Thread {
        
        private String pathDirectory;
        
        private final List<WatchListener> listeners = new ArrayList<>();
    
        public void addListener(WatchListener listener) {
            listeners.add(listener);
        }
    
        public void removeListener(WatchListener listener) {
            listeners.remove(listener);
        }
    
        public void run() {
            if(!found) {
                Platform.runLater(() -> {
                    listeners.forEach(WatchListener::notFound);
                }); 
            } else {
                final JsonFile firstJCopy = firstJ;
                Platform.runLater(() -> {
                    listeners.forEach(WatchListener::startedProcessing);
                });
                
                fullPathFile = pathDirectory + "\\" + firstJ.getNameFile();
                System.out.println(fullPathFile);
                strJ = readFileAsList(fullPathFile);
                String strJCopy = strJ;
                Platform.runLater(() -> {
                    listeners.forEach(listener -> listener.fileProcessed(strJCopy));
                });
            }
        }
    }
    

    Then in your controller:

    public class JsonOverviewController implements WatchListener {
        
        // fields and initialize method as before
        
        
        @FXML
        private void handleButtonAction(ActionEvent event) {
            // Button was clicked, do something...
            System.out.println("myButton premuto");
            DirectoryChooser directoryChooser = new DirectoryChooser();
            Node node = (Node) event.getSource();
            File selectedDirectory;
            Stage thisStage = (Stage) node.getScene().getWindow();
            String absPathSelDir;
            
            selectedDirectory = directoryChooser.showDialog(thisStage);
            if(selectedDirectory == null ) return;
            
            absPathSelDir = selectedDirectory.getAbsolutePath();
            
            myDirWatch.setText(absPathSelDir);
                
            if(jfWatch != null) jfWatch.interrupt();
            
            jfWatch = new WatchController();
            watchController.addListener(this);
            jfWatch.setPathDirectory(absPathSelDir);
            
            jfWatch.start();
            
            myStateLabel.setText("La cartella " + 
                                 absPathSelDir + 
                                 " è monitorata.");    
        }
        
        @Override
        public void startedProcessing() {
            myCurrentJsonFileId.setText("");
            myCurrentJsonFileName.setText("");
            myCurrentJsonFileContent.setText("Anteprima file json non disponibile.");
        }
    
        @Override
        public void fileProcessed(String data) {
            myCurrentJsonFileContent.setText(data);
        }
    
        @Override
        public void notFound() {
            myCurrentJsonFileId.setText("");
            myCurrentJsonFileName.setText("");
            myCurrentJsonFileContent.setText("Anteprima file json non disponibile.");  
        }
    }
    

    A better approach than using a plain thread here may be to use the JavaFX concurrent API and implement WatchController as a Task. This has support for executing code for when the task is started, completes, etc., on the FX Application Thread (it basically handles all the Platform.runLater() calls for you as the state changes). This would look something like:

    public class WatchController extends Task<String> {
    
        private final String pathDirectory;
    
        public WatchController(String pathDirectory) {
            this.pathDirectory = pathDirectory ;
        }
    
        @Override
        public String call() throws Exception {
            if(!found) {
                throw new FileNotFoundException(pathDirectory + " not found");
            } else {
                final JsonFile firstJCopy = firstJ;
                fullPathFile = pathDirectory + "\\" + firstJ.getNameFile();
                System.out.println(fullPathFile);
                return readFileAsList(fullPathFile);
            }
        }
    }
    

    and then in the controller:

    public class JsonOverviewController implements WatchListener {
    
        // fields and initialize method as before
    
        private final Executor exec = Executors.newCachedThreadPool();
    
    
        @FXML
        private void handleButtonAction(ActionEvent event) {
            // Button was clicked, do something...
            System.out.println("myButton premuto");
            DirectoryChooser directoryChooser = new DirectoryChooser();
    
            File selectedDirectory = directoryChooser.showDialog(myButton.getScene().getWindow());
            if(selectedDirectory == null ) return;
    
            String absPathSelDir = selectedDirectory.getAbsolutePath();
    
            myDirWatch.setText(absPathSelDir);
    
            if(jfWatch != null) jfWatch.cancel();
    
            jfWatch = new WatchController(absPathSelDir);
            watchController.setOnRunning(evt -> startedProcessing());
            watchController.setOnFailed(evt -> notFound());
            watchController.setOnSucceeded(evt -> fileProcessed(jfWatch.getValue());
    
            exec.execute(jfWatch);
    
            myStateLabel.setText("La cartella " + 
                                 absPathSelDir + 
                                 " è monitorata.");    
        }
    
        @Override
        public void startedProcessing() {
            myCurrentJsonFileId.setText("");
            myCurrentJsonFileName.setText("");
            myCurrentJsonFileContent.setText("Anteprima file json non disponibile.");
        }
    
        @Override
        public void fileProcessed(String data) {
            myCurrentJsonFileContent.setText(data);
        }
    
        @Override
        public void notFound() {
            myCurrentJsonFileId.setText("");
            myCurrentJsonFileName.setText("");
            myCurrentJsonFileContent.setText("Anteprima file json non disponibile.");  
        }
    }
    

    You probably need to do a little more work in the Task implementation to fully support cancelling the task in an appropriate way. See the Task documentation for more details.