JavaFX ChangeListener not always working
Asked Answered
M

1

6

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 ?

Mannos answered 14/7, 2015 at 13:40 Comment(2)
"Why?": explained fully in the documentation. What do you actually want to do here? (I assume it's not just to display the values 1-300 in a text area, as you would not need a task for that.)Molasses
the task renames files in a selected folder over network and i try to list all processed files in the textarea. like a setup that prints the copied files.Mannos
M
15

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();
        }
    }
}
Molasses answered 14/7, 2015 at 18:42 Comment(4)
answers like this are the reason why i ask on this platform ! wow and great, thank you very much. For people like me that have just some sciolism. that holds so much information and knowledge... when you have to dig that stuff out of the api descriptions... hell man... i tried runlater but got exactly what you predicted when renaming files on the local machine and not over the network. the order is also important, because i count the files and print the number to the screen. so in the end i have to choose number 3. :)Mannos
This is a great answer! Helped me with the situation I have exactly! option 3 is the way to go!Fibrous
Yes, this is a great answer. Option 3 was the best way to go for me. However, using messages.forEach(msg -> textArea.appendText("\"+msg)); was a bit slow for me. Instead, I concatenated all lines before appending them to the text area, i.e. textArea.appendText(String.join("", messages)). I do not need to add a newline character because my messages already have newlines in them.Coverlet
@Coverlet For some UI tasks, a ListView can be more appropriate than a TextArea, as ListView is a virtualized control. See Most efficient way to log messages to JavaFX ... via threads for some more info on a ListView based approach. The reason that ListView can help in situations like this is that it only needs to render the visible lines, not all the text in the text area.Contumely

© 2022 - 2024 — McMap. All rights reserved.