8

I have the following use-case:

I have code executing in Thread A (not EDT). Then I want to ask the user a question, but this must be done on the EDT as it involves Swing code (opening a dialog etc). Finally, I want to pass the user's answer back to Thread A, so it can continue.

I'm struggling to find a good way to pass the user's answer back to Thread A. How do you do this?

Zoyd
  • 3,449
  • 1
  • 18
  • 27
Eric Lindauer
  • 1,813
  • 13
  • 19

5 Answers5

18
FutureTask<Integer> dialogTask = new FutureTask<Integer>(new Callable<Integer>() {
  @Override public Integer call() {
    return JOptionPane.showConfirmDialog(...);
  }
});
SwingUtilities.invokeLater(dialogTask);
int result = dialogTask.get();
jtahlborn
  • 52,909
  • 5
  • 76
  • 118
  • This is very interesting... I wonder how this would handle an Exception being thrown in the call() method? I'd like the exception (if any) to be thrown on the original (not EDT) thread... – Eric Lindauer Nov 20 '12 at 03:57
  • 2
    @EricLindauer - all that is handled exactly as you would expect. the exception will be thrown in the original thread wrapped in an ExecutionException (see `Future.get()` for more details). – jtahlborn Nov 20 '12 at 03:59
  • 1
    NOTE: you must make sure you are NOT on the EDT when you call this code, as the get() will never return and the EDT will never be released to go execute the FutureTask... – Eric Lindauer Nov 20 '12 at 06:08
2

From within thread A, you can use SwingUtilities.invokeAndWait(Runnable) to execute your user prompt on the EDT. This will block thread A until your runnable completes (i.e., until the user has submitted a result and you have stored it somewhere). Your runnable can be written to store the result somewhere that thread A can access it, once thread A regains control.

JimN
  • 3,120
  • 22
  • 35
  • Right... so the "store the result someplace" part is really what I'm asking about... is there an elegant way to accomplish this? – Eric Lindauer Nov 20 '12 at 03:56
  • It seems that FutureTask / SwingUtilities.invokeLater() and task.get() work quite nicely to accomplish the goal, with no need to write any extra code to store / retrieve the result between the threads. – Eric Lindauer Nov 20 '12 at 04:12
2

Basically, you need to use EventQueue#invokeAndWait (AKA SwingUtilities#invokeAndWait). This will block the current thread until the run method returns.

The real trick is trying to get it setup so that you can get the return value ;)

public class TestOptionPane03 {

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                String choice = ask("Chocolate", "Strewberry", "Vanilla");
                System.out.println("You choose " + choice);
            }
        }).start();
    }

    public static String ask(final String... values) {

        String result = null;

        if (EventQueue.isDispatchThread()) {

            JPanel panel = new JPanel();
            panel.add(new JLabel("Please make a selection:"));
            DefaultComboBoxModel model = new DefaultComboBoxModel();
            for (String value : values) {
                model.addElement(value);
            }
            JComboBox comboBox = new JComboBox(model);
            panel.add(comboBox);

            int iResult = JOptionPane.showConfirmDialog(null, panel, "Flavor", JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE);
            switch (iResult) {
                case JOptionPane.OK_OPTION:
                    result = (String) comboBox.getSelectedItem();
                    break;
            }

        } else {

            Response response = new Response(values);
            try {
                SwingUtilities.invokeAndWait(response);
                result = response.getResponse();
            } catch (InterruptedException | InvocationTargetException ex) {
                ex.printStackTrace();
            }

        }

        return result;

    }

    public static class Response implements Runnable {

        private String[] values;
        private String response;

        public Response(String... values) {
            this.values = values;
        }

        @Override
        public void run() {
            response = ask(values);
        }

        public String getResponse() {
            return response;
        }
    }
}

In this example, I basically create my own query object the implements Runnable and can store the response from the user

MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • ok, this is basically exactly what I have come up with. I created a generic Response with getters and setters quite similar to your idea. I also had to add Exceptions to it to pass errors back to thread A... It seemed pretty heavy-handed, but maybe it's the only way to go... Thanks for your time! – Eric Lindauer Nov 20 '12 at 03:55
  • 1
    This was my best attempt as well, but using FutureTask as shown in the accepted answer is much better. Thanks anyways! :) – Eric Lindauer Nov 20 '12 at 04:05
2

I wrote the following convenience method to add to jtahlborn's answer. It adds a check to avoid blocking the EDT, and provides a nice stream-lined exception handling:

/**
 * executes the given callable on the EDT, blocking and returning the result of the callable.call() method.
 * 
 * If call() throws an exception, it is rethrown on the the current thread if the exception is either a RuntimeException, or the
 * class that is assignable to exceptionClass. Otherwise, it is wrapped in a RuntimeException and thrown on the current thread.
 * 
 * @param exceptionClass The class of any exception that may be thrown by the callable.call() method, which will now be thrown
 *            directly by this method (ie, not wrapped in an ExecutionException)
 */
public static <T, E extends Exception> T invokeAndWaitAndReturn(Callable<T> callable, Class<E> exceptionClass)
        throws InterruptedException, E {
    if (SwingUtilities.isEventDispatchThread()) {
        try {
            return callable.call();
        }
        catch (Exception e) {
            throw throwException(exceptionClass, e);
        }
    }
    else {
        FutureTask<T> task = new FutureTask<T>(callable);
        SwingUtilities.invokeLater(task);

        try {
            return task.get();
        }
        catch (ExecutionException ee) {
            throw throwException(exceptionClass, ee.getCause());
        }
    }
}

@SuppressWarnings("unchecked")
private static <E extends Exception> E throwException(Class<E> exceptionClass, Throwable t) {
    if (exceptionClass.isAssignableFrom(t.getClass())) {
        return (E) t;
    }
    else if (t instanceof RuntimeException) {
        throw (RuntimeException) t;
    }
    else {
        throw new RuntimeException(t);
    }
}

You call it like this, and don't need to worry whether you are currently executing on the EDT or not:

try {
    Integer result = invokeAndWaitAndReturn(new Callable<Integer>() {
        public Integer call() throws MyException {
            // do EDT stuff here to produce the result
        }
    }, MyException.class);
} catch(InterruptedException ie) {
    Thread.currentThread().interrupt();
} catch(MyException me) {
    // handle the "expected" Exception here
}
Eric Lindauer
  • 1,813
  • 13
  • 19
  • of course, writing a version of "invokeAndWait" that took no Exception argument, or 2 or 3, is easy enough! – Eric Lindauer Nov 20 '12 at 06:45
  • The "no exception" convenince method is: public static T invokeAndWaitAndReturn(Callable callable) throws InterruptedException { return invokeAndWaitAndReturn(callable, RuntimeException.class); } – Eric Lindauer Nov 20 '12 at 07:29
1

This util method does what is in the supplier in a separate swing thread, and it wait till the response. It also throws an exception if there is:

public class InvokeAndGet {

public static <T> T execute(Supplier<T> supplier, long timeout) throws InterruptedException, SyncException {
    AtomicReference<T> atomicRef = new AtomicReference<>();
    AtomicReference<Exception> atomicException = new AtomicReference<>();
    CountDownLatch latch = new CountDownLatch(1);
    SwingUtilities.invokeLater(() -> {
        try {
            atomicRef.set(supplier.get());
        }catch(Exception e){
            atomicException.set(e);
        }finally {
            latch.countDown();
        }
    });
    latch.await(timeout, TimeUnit.MILLISECONDS);
    if(atomicException.get() != null) {
        throw new SyncException(atomicException.get());
    }else {
        return atomicRef.get();
    }
}

@SuppressWarnings("serial")
public static class SyncException extends Exception {
    public SyncException(Throwable arg0) {
        super(arg0);
    }
}

}

And here two tests so you can see how to use it:

@Test
public void execute() throws InterruptedException, SyncException {
    Integer result = InvokeAndGet.execute(() -> 1+1, 5000);
    assertEquals(2, result.intValue());
}

@Test(expected = SyncException.class)
public void executeException() throws InterruptedException, SyncException {
    InvokeAndGet.execute(() -> 1/0, 5000);
}

There is still room to improve it to make it more general, because this implementation relays in SwingUtilities and sometimes you want to use ThreadExecutor.

ravenskater
  • 702
  • 1
  • 7
  • 20