Search code examples
javaswingevent-dispatch-threadjprogressbarpropertychangelistener

Notify PropertyChangeListener faster


So I'm creating a JProgressBar that displays the progress of a CSV manipulation, where every line is read and checked if there are no null values in obligatory (NOT NULL) columns. For that, I've created a SwingWorker Task that handles converting the number of lines in the file to 100% on the maximum progress value, and adding up on the progress on the correct rate.

That's the SwingWorker:

public static class Task extends SwingWorker<String, Object> {

    private int counter;
    private double rate;

    public Task(int max) {
        // Adds the PropertyChangeListener to the ProgressBar
        addPropertyChangeListener(
             ViewHandler.getExportDialog().getProgressBar());
        rate = (float)100/max;
        setProgress(0);
        counter = 0;
    }

    /** Increments the progress in 1 times the rate based on maximum */
    public void step() {
        counter++;
        setProgress((int)Math.round(counter*rate));
    }

    @Override
    public String doInBackground() throws IOException {
        return null;
    }
    @Override
    public void done() {
      Toolkit.getDefaultToolkit().beep();
      System.out.println("Progress done.");
    }
}

My PropertyChangeListener, which is implemented by the JProgressBar wrapper:

@Override
    public void propertyChange(PropertyChangeEvent evt) {
        if ("progress".equals(evt.getPropertyName())) {
            progressBar.setIndeterminate(false);
            progressBar.setValue((Integer) evt.getNewValue());
        }
    }

Then, where I actually use it, I override the doInBackground() method with the processing I need, calling step() on every iteration.

    Task read = new Task(lines) {
        @Override
            public String doInBackground() throws IOException {
                while(content.hasNextValue()) {
                step();
                // Processing
            }
            return output.toString();
        }
   };
   read.execute();
   return read.get();

So what is happening: the processing works and succeeds, then done() is called, and just after that the propertyChange() registers two 'state' events and one 'progress' event, setting the ProgressBar's progress from 0% to 100%.

What is happening What I thought was happening (check Hovercraft's answer for clarification) is described in the JavaDocs:

Because PropertyChangeListeners are notified asynchronously on the Event Dispatch Thread multiple invocations to the setProgress method might occur before any PropertyChangeListeners are invoked. For performance purposes all these invocations are coalesced into one invocation with the last invocation argument only.

So, after all that, my question is: am I doing something wrong? If not, is there a way for me to make the Event Dispatch Thread notify the PropertyChangeListeners as the onProgress() happens, or at least from time to time?

Obs.: the processing I'm testing takes from 3~5s.


Solution

  • Your problem is here:

    read.execute();
    return read.get();
    

    get() is a blocking call, and so calling it from the event thread immediately after executing your worker will block the event thread and your GUI.

    Instead, it should be called from a call-back method such as the done() method or from the property change listener after the worker has changed its state property to SwingWorker.StateValue.DONE.


    For example

    import java.awt.*;
    import java.awt.event.*;
    import java.beans.PropertyChangeEvent;
    import java.beans.PropertyChangeListener;
    import java.util.concurrent.ExecutionException;
    import java.util.concurrent.TimeUnit;
    
    import javax.swing.*;
    
    @SuppressWarnings("serial")
    public class TestSwingWorkerGui extends JPanel {
        private JProgressBar progressBar = new JProgressBar(0, 100);
        private Action myAction = new MyAction("Do It!");
    
        public TestSwingWorkerGui() {
            progressBar.setStringPainted(true); 
            add(progressBar);
            add(new JButton(myAction));
        }
    
        private class MyAction extends AbstractAction {
            public MyAction(String name) {
                super(name);
            }
    
            @Override
            public void actionPerformed(ActionEvent e) {
                myAction.setEnabled(false);
                Task read = new Task(30) {
                    @Override
                    public String doInBackground() throws Exception {
                        int counter = getCounter();
                        int max = getMax();
                        while (counter < max) {
                            counter = getCounter();
                            step();
                            TimeUnit.MILLISECONDS.sleep(200);
                        }
                        return "Worker is Done";
                    }
                };
                read.addPropertyChangeListener(new MyPropListener());
                read.execute();
            }
        }
    
        private class MyPropListener implements PropertyChangeListener {
            @Override
            public void propertyChange(PropertyChangeEvent evt) {
                String name = evt.getPropertyName();
                if ("progress".equals(name)) {
                    progressBar.setIndeterminate(false);
                    progressBar.setValue((Integer) evt.getNewValue());
                } else if ("state".equals(name)) {
                    if (evt.getNewValue() == SwingWorker.StateValue.DONE) {
                        myAction.setEnabled(true);
                        @SuppressWarnings("unchecked")
                        SwingWorker<String, Void> worker = (SwingWorker<String, Void>) evt.getSource();
                        try {
                            String text = worker.get();
                            System.out.println("worker returns: " + text);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        } catch (ExecutionException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    
        private static void createAndShowGui() {
            TestSwingWorkerGui mainPanel = new TestSwingWorkerGui();
    
            JFrame frame = new JFrame("GUI");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.getContentPane().add(mainPanel);
            frame.pack();
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);
        }
    
        public static void main(String[] args) {
            SwingUtilities.invokeLater(() -> createAndShowGui());
        }
    }
    

    class Task extends SwingWorker<String, Void> {
    
        private int counter;
        // private double rate;
        private int max;
    
        public Task(int max) {
            // Adds the PropertyChangeListener to the ProgressBar
            // addPropertyChangeListener(gui);
            // !!rate = (float)100/max;
            this.max = max;
            setProgress(0);
            counter = 0;
        }
    
        /** Increments the progress in 1 times the rate based on maximum */
        public void step() {
            counter++;
            int progress = (100 * counter) / max;
            progress = Math.min(100, progress);
            setProgress(progress);
            // setProgress((int)Math.round(counter*rate));
        }
    
        public int getCounter() {
            return counter;
        }
    
        public int getMax() {
            return max;
        }
    
        @Override
        public String doInBackground() throws Exception {
            return null;
        }
    
        @Override
        public void done() {
          Toolkit.getDefaultToolkit().beep();
          System.out.println("Progress done.");
        }
    }