4

I have a modal dialog containing a JLabel. This dialog calls a method in another class which does a long-running lookup activity. I'd like this lookup class to update the JLabel, which is passed from the modal dialog.

I've tried updating the JLabel from the code loop in the lookup process, and from within a Timer. (I found that the actionPerformed method of a Swing timer never gets called when a modal dialog is running.) Since implementing the Timer, I moved my lookup process to a separate Thread.

My lookup works, but the JLabel is still never repainted until the end of the process. I'll be grateful for any help.

Here's the relevant method. The DictionaryTuple is a POJO containing two fields: a List and a String. Just getters and setters, but dictionaryThread does have access to tupleList; note that we don't touch it here until dictionaryThread is finished.

public List<DictionaryTuple> findTuples() {
    tupleList = new ArrayList<DictionaryTuple>();
    Thread dictionaryThread = new Thread(this);  // This is the long-running lookup thread
    dictionaryThread.start();

    progressTimer = new Timer();
    progressTimer.scheduleAtFixedRate(new TimerTask() {
        @Override
        public void run() {
                       //  progressCaption is updated in dictionaryThread
            synchronized (progressCaption) {
                lblProgress.setText(progressCaption);
            }
            lblProgress.repaint();
            Thread.yield();
        }
    }, 250, 250);

     try {
        dictionaryThread.join();
    } catch (InterruptedException e) {
    } finally {
      progressTimer.cancel();
      lblProgress.setText("");
      progressCaption = "";
    }
    return tupleList;
}

I've also tried this variation; in that case, the inner run() invocations are held until the processing is completed:

    progressTimer.scheduleAtFixedRate(new TimerTask() {
        @Override
        public void run() {
            synchronized (progressCaption) {
            System.out.println("Fire!");
            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    System.out.println("Update");
                    lblProgress.setText(progressCaption);
                    lblProgress.repaint();
                }});
            }
        }
    }, 250, 250);
Andrew Thompson
  • 168,117
  • 40
  • 217
  • 433
Elly
  • 63
  • 8
  • Fair question. It's a POJO containing two fields: a List and a String. Just getters and setters. – Elly Jul 06 '12 at 12:06
  • Thank you. I noted you have not yet accepted an answer. Is the problem solved? If not, it might be a good idea to post an [SSCCE](http://sscce.org/) that demonstrates the problem behavior. – Andrew Thompson Jul 06 '12 at 12:25

3 Answers3

2

You need to show us exactly where you set the dialog to visible -- is it before or after you call the findTuples() method? This is critical.

Suggestions:

  • Always update the JLabel on the Swing event thread or EDT (event dispatch thread).
  • Don't use a java.util.Timer to update the components of a Swing app.
  • You can use a javax.swing.Timer to update Swing components, but that's not going to work here, unless you are using it to poll the background process.
  • Consider using a SwingWorker for your background threading and then update the JLabel via eitherthe publish/process methods of the worker or via a PropertyChangeListener added to the SwingWorker.
  • Call all your methods that you want running when the dialog is running before setting the dialog to visible.
Hovercraft Full Of Eels
  • 283,665
  • 25
  • 256
  • 373
  • Sorry I didn't mention it: the findTuples() method is called when a button is clicked in the dialog--so, of course, the dialog is made visible first. – Elly Jul 04 '12 at 02:08
  • 1
    @Elly: there's no "of course" about it. The dialog will block, so the only "of course" here is that you have to get this code above called ***before*** showing the dialog. There's no other possible solution with a modal dialog. – Hovercraft Full Of Eels Jul 04 '12 at 02:10
  • @Elly: I think that this is the main issue, the main thing preventing you from progressing with your project, and so it makes sense to me that this is what we should be working on here -- how to massage your code so that you can get this listening code and label updating code running *before* the modal dialog is displayed. Once we can figure this out, I think that you'll progress along quickly. So please tell us more about the problem, and about what is currently preventing you from making the calls in the right order. Again, let's see how we can right this situation. – Hovercraft Full Of Eels Jul 04 '12 at 04:37
  • 'Craft, my use of "of course" refers to the fact that the JDialog is made visible first because the JButton that starts the process is part of the JDialog. It's not a comment on whether it was the right thing to do. – Elly Jul 06 '12 at 12:54
  • @Elly: Ah so if the code is being called from the JDialog while it's running, then you're right, it's not the problem. I'm still not sure what is causing the problem just yet though. – Hovercraft Full Of Eels Jul 06 '12 at 14:14
2

This demo should be helpful. See the different if I run the long job by calling run vs running it in a separate thread


    public static void modalDialogTest() {
        JDialog dialog = new JDialog((JFrame)null);
        final JLabel jl = new JLabel("Need some space");
        JButton jb = new JButton ("Click me");
        Timer t = new Timer(1000, new AbstractAction() {
            int it = 0;
            @Override
            public void actionPerformed(ActionEvent arg0) {
                // TODO Auto-generated method stub
                jl.setText("" + it);
                jl.repaint();
                it++;
            }

        });
        t.start();

        AbstractAction wait = new AbstractAction() {

            @Override
            public void actionPerformed(ActionEvent e) {
                Runnable r = new Runnable() {
                    public void run() {
                        try {
                            Thread.sleep(10000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                };
                //r.run();
                new Thread(r).start();
            }
        };

        jb.addActionListener(wait);
        dialog.getContentPane().add(jl);
        dialog.getContentPane().add(jb);
        dialog.getContentPane().setLayout(new FlowLayout());
        dialog.pack();
        dialog.setVisible(true);
        dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
    }
ControlAltDel
  • 33,923
  • 10
  • 53
  • 80
1

Thank you all for your suggestions. The one I've implemented is Hovercraft's, to use a SwingWorker.

The Dictionary class, in which my method sits, now extends SwingWorker. The findTuples() method becomes doInBackground(). The Timer and my own Thread code are no longer required.

doInBackground() uses publish() with the property name "progress" to provide the text to appear in the updated JLabel.

Back in the dialog code, invocation involves creating a Dictionary instance, and using a PropertyChangeListener to process the change in the JLabel (named lblProgress) and to detect when processing is completed:

private void myCallingMethod() {
    final Dictionary dictionary = new Dictionary();

    dictionary.addPropertyChangeListener(new PropertyChangeListener() {
        @Override
        public void propertyChange(PropertyChangeEvent event) {
            List<DictionaryTuple> tupleList;

            // Is processing complete?
            if (event.getNewValue() instanceof StateValue
                    && event.getNewValue() == StateValue.DONE) {
                try {
                    tupleList = dictionary.get();
                    lblProgress.setText(tupleList.size() + " Tuples Found");
                } catch (Exception e) {
                    JOptionPane.showMessageDialog(
                            null,
                            "Error in processing dictionary: "
                                    + e.getMessage());
                    tupleList = new ArrayList<DictionaryTuple>();
                    lblProgress.setText("");
                }

                // Process the results.
                processTuples(tupleList);

            } else {
                // The process isn't done yet.
                // Is this a request to change lblProgress?
                if (event.getPropertyName().equals("progress")) {  
                    lblProgress.setText((String) event.getNewValue());
                }
            }
        }
    });

    // Start the dictionary lookup to run asynchronously.
    dictionary.execute();
}

I learned not to rely on SwingWorker.isDone() to determine whether the SwingWorker's processing is complete. In my case, the PropertyChangeListener was called twice in that state: once with the results of the last publish() call, and once when the task actually completed.

Elly
  • 63
  • 8