July 2007 Archives

UI in your Eclipse 3.3-based RCP App

July 13, 2007 11:10 AM

So you're developing Eclipse RCP applications, and you're excited about moving your RCP app to the Eclipse 3.3 framework...

Wait, you're not? Then this post probably doesn't interest you too much.

But if you are, there are some UI changes in the way Eclipse 3.3 starts up that you should be aware of.

Likely, your application has a subclass of WorkbenchAdvisor, probably called something like YourWorkbenchAdvisor. And this is doing various useful things like creating views and setting preferences, all that good stuff. And maybe you're overriding WorkbenchAdvisor.postStartup() to do some useful things after the Workbench has started.

Maybe one of those useful things happens to be raising a Dialog. In versions past, this would have worked fine for you. In Eclipse 3.3, you may be in for a rude awakening. Despite the name, the Workbench has not started and the UI is not fully functional when YourWorkbenchAdvisor.postStartup() is called.

In particular: other threads cannot call back into the UI thread with Display.asyncExec() or Display.syncExec(). The UISynchronizer will queue up any runnable you post on to the UI thread to run once it actually has started. This can present a problem if the dialog you raise fires off a background thread to do some work.

For example, in Teamprise Explorer our login dialog has an option to test your connection to the TFS server. This involves firing off a background thread to look at your network settings, try to connect, etc. The results of these test are then posted back to the dialog using Display.asyncExec().

What happens in Eclipse 3.3? Since the login dialog was started from our postStartup() method, the UISynchronizer hasn't started. So when we call Display.asyncExec() to display our results, they're put in a queue to display once the UI has fully started. This, of course, only occurs after you've closed the login dialog, at which point there's no dialog to display these settings to.

Ouch.

To combat this, you should move any UI code out of YourWorkbenchAdvisor.postStartup(). You can either do this by putting your code in the UI queue to execute as soon as the Workbench starts fully, or you can move it out of postStartup() entirely and use a different hook to raise your UI once the Workbench is started.

These options are both sort of a hack, so you're welcome to use whichever you find less obscene:

  1. Queue your UI code in the UISynchronizer, where it will be run as soon as the Workbench is started
    If a non-UI thread calls Display.asyncExec() before the Workbench startup has completed, your Runnable will be queued to run after the Workbench has started, but this is only true if it's called from a thread other than the UI thread. So you need to get off the UI thread to post this into the queue. Firing off a new thread from your postStartup() method should do the trick:
    new Thread(new Runnable() {
        public void run()
        {
            Display.asyncExec(new Runnable() {
                public void run()
                {
                    // do something on the UI thread
                    // after Workbench startup
                }
            });
        }
    }).start();

    This will put your inner Runnable on the UI queue to execute as soon as Workbench startup is complete. (Had you called Display.asyncExec() from the current thread, it would execute immediately.)

    If you don't like the idea of stuffing a Runnable inside another Runnable, there's a second option:

  2. Override WorkbenchAdvisor.eventLoopIdle() to call your code the first time the UI is idle

    If you look closely at Workbench.runUI(), you'll see that the UISynchronizer isn't started until right before a call to Workbench.runEventLoop(). So hooking in to WorkbenchAdvisor.eventLoopIdle() is your first opportunity to execute code once the UI thread is properly started.

    Adding this to YourWorkbenchAdvisor should be sufficient:

    private static ThreadLocal eventLoopStarted = new ThreadLocal() {
        protected Object initialValue()
        {
            return Boolean.FALSE;
        }
    };
    private void start()
    {
        // do something on the UI thread after Workbench startup
    }
    public void eventLoopIdle(Display display)
    {
        // the ui is now started, we can do our work
        if(eventLoopStarted.get().equals(Boolean.FALSE))
        {
            // call any post-startup UI
            start();
            eventLoopStarted.set(Boolean.TRUE);
        }
        else
        {
            // default: yield cpu until new events enter the queue
            super.eventLoopIdle(display);
        }
    }

    I'm synchronizing on the eventLoopStarted flag, which is probably not necessary since only the UI thread should be calling eventLoopIdle(). But proof of thread-safety is left as an exercise for the reader.

    Also, if you override eventLoopIdle(), be sure you call back to the base class or call Display.sleep(). Unless, of course, you like burning up your processor whilst doing nothing.

There you go - with just a bit of hackery, you should be able to move your RCP application over to Eclipse 3.3 and still get all the same UI at application startup time.

Edward Thomson is a Software Engineer at Teamprise, where he develops cross-platform client solutions for Microsoft Team Foundation Server, with an emphasis on Macintosh compatibility and IDE integration.