Search code examples
javaswingworker

Dialog with swingworker is a chicken/egg


I am trying to follow the Java best practices by not doing long tasks on the main thread (EDT). So I am planning to use a swingWorker with Modal Dialog. This way the modal dialog blocks the user for doing anything until that task is done and I can update status on the dialog while the process is taking place.

Now the problem is that with the modal dialog, it not only blocks the user but also nothing after setVisible gets called

So if I do

dialog.setVisible(true);
new SwingWorkerTask().execute(); //This does not get called

and if I do

new SwingWorkerTask().execute();
dialog.setVisible(true); // Well what the point of setting visible after the fact.

So How do I block user action and show a dialog while task is taking place?

Thank you


Solution

  • It is only a chicken/egg if you make it such. You can construct all Swing objects on EDT and then let your SwingWorker (or any other thread) govern all updates by instructing EDT to execute them via SwingUtilities.invokeLater(Runnable).

    import java.awt.BorderLayout;
    import java.awt.event.ActionEvent;
    import java.awt.event.ActionListener;
    import java.beans.PropertyChangeEvent;
    import java.beans.PropertyChangeListener;
    import javax.swing.JButton;
    import javax.swing.JDialog;
    import javax.swing.JFrame;
    import javax.swing.JProgressBar;
    import javax.swing.SwingUtilities;
    import javax.swing.SwingWorker;
    
    public class RudeProgressBar extends JFrame {
    
        private JButton button;
    
        public RudeProgressBar() {
            setTitle("Rude Progress Bar");
            setDefaultCloseOperation(EXIT_ON_CLOSE);
            setLayout(new BorderLayout());
    
            button = new JButton("Do teh work");
            add(button, BorderLayout.SOUTH);
    
            button.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    JDialog dialog = new JDialog(RudeProgressBar.this, true);
                    dialog.setTitle("Doing teh work");
                    dialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
                    final JProgressBar progressBar = new JProgressBar(0, 100);
                    dialog.setLayout(new BorderLayout());
                    dialog.add(progressBar);
                    dialog.setSize(100, 100);
                    dialog.setLocationRelativeTo(RudeProgressBar.this);
                    MyTask task = new MyTask(dialog);
                    task.addPropertyChangeListener(new PropertyChangeListener() {
                        @Override
                        public void propertyChange(PropertyChangeEvent evt) {
                            if ("progress".equals(evt.getPropertyName())) {
                                progressBar.setValue((Integer)evt.getNewValue());
                            }
                        }
                    });
                    task.execute();
                }
            });
    
            setSize(200, 200);
            setLocationRelativeTo(null);
        }
    
        public static void main(String[] args) {
            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    new RudeProgressBar().setVisible(true);
                }
            });
        }
    
        private class MyTask extends SwingWorker<Void, Void> {
    
            private final JDialog dialog;
    
            public MyTask(JDialog dialog) {
                this.dialog = dialog;
            }
    
            @Override
            protected Void doInBackground() throws Exception {
                SwingUtilities.invokeLater(new Runnable() {
                    @Override
                    public void run() {
                        dialog.setVisible(true);
                    }
                });
    
                int progress = 0;
                for (int i = 0; i < 5; i++) {
                    Thread.sleep(1000);
                    setProgress(progress += 20);
                }            
    
                return null;
            }
    
            @Override
            protected void done() {
                dialog.setVisible(false);
                dialog.dispose();
            }
        }
    }
    

    If you are worried that the invokeLater implementation (inside SwingWorker.doInBackground) might get executed after SwingWorker.done, simply put the code in done into another invokeLater. By doing this, you queue your Runnable implementations for EDT to execute them in certain order. The queuing will happen even if this method is called from EDT itself.

    Note that if you take a look at SwingWorker implementation, you'll see that it relies on javax.swing.Timer to execute done() and the Timer itself calls invokeLater, so calling it inside done again amounts to doing nothing. Nothing will be wrong if you do it, however.