Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to delegate SwingWorker's publish to other methods

My "problem" can be described by the following. Assume we have an intensive process that we want to have running in the background and have it update a Swing JProgress bar. The solution is easy:

import java.util.List;

import javax.swing.JOptionPane;
import javax.swing.JProgressBar;
import javax.swing.SwingWorker;


/**
 * @author Savvas Dalkitsis
 */
public class Test {

    public static void main(String[] args) {
        final JProgressBar progressBar = new JProgressBar(0,99);
        SwingWorker<Void, Integer> w = new SwingWorker<Void, Integer>(){

            @Override
            protected void process(List<Integer> chunks) {
                progressBar.setValue(chunks.get(chunks.size()-1));
            }

            @Override
            protected Void doInBackground() throws Exception {

                for (int i=0;i<100;i++) {
                    publish(i);
                    Thread.sleep(300);
                }

                return null;
            }

        };
        w.execute();
        JOptionPane.showOptionDialog(null,
                new Object[] { "Process", progressBar }, "Process",
                JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE,
                null, null, null);
    }

}

Now assume that i have various methods that take a long time. For instance we have a method that downloads a file from a server. Or another that uploads to a server. Or anything really. What is the proper way of delegating the publish method to those methods so that they can update the GUI appropriately?

What i have found so far is this (assume that the method "aMethod" resides in some other package for instance):

import java.awt.event.ActionEvent;
import java.util.List;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JOptionPane;
import javax.swing.JProgressBar;
import javax.swing.SwingWorker;


/**
 * @author Savvas Dalkitsis
 */
public class Test {

    public static void main(String[] args) {
        final JProgressBar progressBar = new JProgressBar(0,99);
        SwingWorker<Void, Integer> w = new SwingWorker<Void, Integer>(){

            @Override
            protected void process(List<Integer> chunks) {
                progressBar.setValue(chunks.get(chunks.size()-1));
            }

            @SuppressWarnings("serial")
            @Override
            protected Void doInBackground() throws Exception {

                aMethod(new AbstractAction() {

                    @Override
                    public void actionPerformed(ActionEvent e) {
                        publish((Integer)getValue("progress"));
                    }
                });

                return null;
            }

        };
        w.execute();
        JOptionPane.showOptionDialog(null,
                new Object[] { "Process", progressBar }, "Process",
                JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE,
                null, null, null);
    }

    public static void aMethod (Action action) {
        for (int i=0;i<100;i++) {
            action.putValue("progress", i);
            action.actionPerformed(null);
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

It works but i know it lacks something. Any thoughts?

like image 313
Savvas Dalkitsis Avatar asked May 25 '10 20:05

Savvas Dalkitsis


1 Answers

(I'm updating my answer to make it more clear and generalized)

Although you have successfully decoupled your logic and presentation, it's not done in a way that lends itself to code reuse. Java's PropertyChangeSupport makes it easy to decouple the logic from the presentation by implementing bound properties, and get some substantial reuse. The idea is to use event handlers instead of action objects.

First, conceptualize the abstraction. The background work needs to "shout out" (publish) to the GUI intermittently, and the GUI needs to listen for it. Two generic classes will codify this idea:

/**
 * Wrapper for the background logic.
 *
 * <T> return type
 * <S> intermediary type (the "shout out")
 */
public static abstract class LoudCall<T, S> implements Callable<T> {

    private PropertyChangeSupport pcs;
    private S shout;

    public LoudCall() {
        pcs = new PropertyChangeSupport(this);
    }

    public void shoutOut(S s) {
        pcs.firePropertyChange("shoutOut", this.shout, 
                this.shout = s);
    }

    public void addListener(PropertyChangeListener listener) {
        pcs.addPropertyChangeListener(listener);
    }

    public void removeListener(PropertyChangeListener listener) {
        pcs.removePropertyChangeListener(listener);
    }

    @Override
    public abstract T call() throws Exception;
}

/**
 * Wrapper for the GUI listener.
 *
 * <T> return type
 * <S> intermediary type (the "shout out" to listen for)
 */
public static abstract class ListenerTask<T, S> extends SwingWorker<T, S> 
        implements PropertyChangeListener {

    private LoudCall<T, S> aMethod;

    public ListenerTask(LoudCall<T, S> aMethod) {
        this.aMethod = aMethod;
    }

    @Override
    protected T doInBackground() throws Exception {
        aMethod.addListener(this);
        return aMethod.call();
    }

    @Override
    public void propertyChange(PropertyChangeEvent evt) {
        if ("shoutOut".equals(evt.getPropertyName())) {
            publish((S)evt.getNewValue());
        }
    }

    @Override
    protected abstract void process(List<S> chunks);
}

These classes can be used for all of your Swing widgets. For a ProgressBar, the "shout out" will be an Integer, and the return type is Void:

public class ProgressExample {  
    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
    @Override
    public void run() {

        // 1. setup the progress bar
        final JProgressBar progressBar = new JProgressBar(0, 99);

        // 2. Wrap the logic in a "Loud Call"
        LoudCall<Void, Integer> aMethod = new LoudCall<Void, Integer>() {
            @Override
            public Void call() throws Exception {
                for (int i = 0; i < 100; i++) {
                    // "i have an update for the GUI!"
                    shoutOut(i);
                    Thread.sleep(100);
                }
                return null;
            }
        };

        // 3. Run it with a "Listener Task"
        (new ListenerTask<Void, Integer>(aMethod) {
            @Override
            protected void process(List<Integer> chunks) {
                progressBar.setValue(chunks.get(chunks.size() - 1));
            }
        }).execute();

        // 4. show it off!
        JOptionPane.showOptionDialog(null,
            new Object[] { "Process", progressBar }, "Process",
            JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE,
            null, null, null
        );
    }
        });
    }
}

Only the listener needs to know anything about the GUI details, and the background logic still has control over publishing (indirectly, by "shouting"). This code is more terse, readable, and reusable.

I realize this question is pretty old now, but hopefully it helps somebody!

like image 73
Joe Coder Avatar answered Nov 14 '22 22:11

Joe Coder