javaconcurrencyjavafxchangelistener

JavaFX ChangeListener not always working


i have a JavaFX Application and in there a concurrency Task. While the Task is running, i want to append the message from updateMessage() to a TextArea

because the binding doesn't append new text to the TextArea, i used a ChangeListener

worker.messageProperty().addListener((observable, oldValue, newValue) -> {
    ta_Statusbereich.appendText("\n" + newValue);
});

That is working but not on every change. I checked it with a System.out.println() and counted in the task from 1 to 300

for (Integer i = 1; i <= 300; i++) {
    updateMessage(i.toString());
    System.out.println(i.toString());
}

this println() in the Task gives me what i want 1,2,3,4,5,6,7,8 and so on, but my TextArea shows 1,4,5,8,9 i then added a println in the ChangeListener and get the same result, 1,4,5,8,9 (the result is random not always 1,4,5...)

why ? are there other ways to append the message text to the TextAres, maybe with bind ?


Solution

  • The message property is designed as a property which holds a "current message" for the task: i.e. the target use case is something akin to a status message. In this use case, it doesn't matter if a message that is stored in the property for only a very brief time is never intercepted. Indeed, the documentation for updateMessage() states:

    Calls to updateMessage are coalesced and run later on the FX application thread, so calls to updateMessage, even from the FX Application thread, may not necessarily result in immediate updates to this property, and intermediate message values may be coalesced to save on event notifications.

    (my emphasis). So, in short, some values passed to updateMessage(...) may never actually be set as the value of messageProperty if they are superceded quickly by another value. In general, you can expect only one value to be observed every time a frame is rendered to the screen (60 times per second, or fewer). If you have a use case where it is important you want to observe every value, then you need to use another mechanism.

    A very naïve implementation would just use Platform.runLater(...) and directly update the text area. I do not recommend this implementation, as you risk flooding the FX Application Thread with too many calls (the exact reason why updateMessage(...) coalesces calls), making the UI unresponsive. However, this implementation would look like:

    for (int i = 1 ; i <= 300; i++) {
        String value = "\n" + i ;
        Platform.runLater(() -> ta_Statusbereich.appendText(value));
    }
    

    Another option is to make each operation a separate task, and execute them all in parallel in some executor. Append to the text area in each task's onSucceeded handler. In this implementation, the order of the results is not predetermined, so if order is important, this is not an appropriate mechanism:

    final int numThreads = 8 ;
    Executor exec = Executors.newFixedThreadPool(numThreads, runnable -> {
        Thread t = Executors.defaultThreadFactory().newThread(runnable);
        t.setDaemon(true);
        return t ;
    });
    
    // ...
    
    for (int i = 1; i <= 300; i++) {
        int value = i ;
        Task<String> task = new Task<String>() {
            @Override
            public String call() {
                // in real life, do real work here...
                return "\n" + value ; // value to be processed in onSucceeded
            }
        };
        task.setOnSucceeded(e -> ta_Statusbereich.appendText(task.getValue()));
        exec.execute(task);
    }
    

    If you want to do all this from a single task, and control the order, then you can put all the messages into a BlockingQueue, taking messages from the blocking queue and placing them in the text area on the FX Application thread. To ensure you don't flood the FX Application thread with too many calls, you should consume the messages from the queue no more than once per frame rendering to the screen. You can use an AnimationTimer for this purpose: it's handle method is guaranteed to be invoked once per frame rendering. This looks like:

    BlockingQueue<String> messageQueue = new LinkedBlockingQueue<>();
    
    Task<Void> task = new Task<Void>() {
        @Override
        public Void call() throws Exception {
            final int numMessages = 300 ;
            Platform.runLater(() -> new MessageConsumer(messageQueue, ta_Statusbereich, numMessages).start());
            for (int i = 1; i <= numMessages; i++) {
                // do real work...
                messageQueue.put(Integer.toString(i));
            }
            return null ;
        }
    };
    new Thread(task).start(); // or submit to an executor...
    
    // ...
    
    public class MessageConsumer extends AnimationTimer {
        private final BlockingQueue<String> messageQueue ;
        private final TextArea textArea ;
        private final numMessages ;
        private int messagesReceived = 0 ;
        public MessageConsumer(BlockingQueue<String> messageQueue, TextArea textArea, int numMessages) {
            this.messageQueue = messageQueue ;
            this.textArea = textArea ;
            this.numMessages = numMessages ;
        }
        @Override
        public void handle(long now) {
            List<String> messages = new ArrayList<>();
            messagesReceived += messageQueue.drainTo(messages);
            messages.forEach(msg -> textArea.appendText("\n"+msg));
            if (messagesReceived >= numMessages) {
                stop();
            }
        }
    }