Create ESAPI Scripts That Don’t Freeze the UI

ESAPI is Single-Threaded

In a binary plug-in script, any use of the Eclipse Scripting API (ESAPI) must be made on the same thread as the Execute method in the Script class. But if your user interface (UI) is also on this thread, any time-consuming operations using ESAPI will freeze the UI until the operation ends. (For an excellent tutorial on threading, see Threading in C#.)

One solution is to run the UI on a different thread and execute any ESAPI operations on the original thread. In this blog post, I’ll show you one way to do that. I’ll use a simple example: when the user presses a button, we’ll calculate the mean dose of the current plan while showing the user our progress.

The EsapiWorker Class

In WPF applications, the UI uses a Dispatcher object to handle user events (e.g., a button click). The Dispatcher maintains a queue of such tasks to execute on the thread it is associated with. The Dispatcher can actually execute any code sent to it, not just user events. We’re going to use the main thread’s Dispatcher to execute ESAPI operations. We’ll wrap this logic in a class:

The constructor takes the ScriptContext object, which will be used later. It also obtains the CurrentDispatcher, which is the Dispatcher associated with the current thread. Therefore, we must instantiate the EsapiWorker on the main thread.

The Run method takes an Action with the ScriptContext as its parameter (we’ll see why this parameter is helpful in a moment). In the Run method, we call BeginInvoke on the Dispatcher to execute the Action on the main thread. The Run method may be called from any thread, but the Action will always be executed on the main thread.

The UI Thread

Next, we need to create the UI thread and use it to show the main Window. Let’s start by writing a method that will set up the thread and execute any Action we pass to it:

For reasons I don’t want to get into, the UI thread must be set to apartment state STA. We then set the thread as a background thread, so that it’s automatically terminated if the main application ends. The Action that we will pass to the method (shown later) includes the creation of the Window.

The RunOnNewStaThread method will be called in the Execute method of the Script class (shown later). Once we start the new UI thread, the main thread will continue to execute until it reaches the end of the Execute method. However, this also ends the script!

Using the DispatcherFrame Class

In order to prevent the script from ending prematurely, we need to tell the main thread to wait until the Window in the UI thread is closed. One way to do this is by using a DispatcherFrame. This class allows us to create a new queue of tasks (called a “frame”) within the main Dispatcher, and wait until the frame is no longer needed. Here’s the final code in the Execute method of the Script class:

We create the frame before starting the new UI thread. As soon as the new thread is created, we “push” that frame on the main Dispatcher. Note that we tell the frame to stop (by setting frame.Continue to false) after the main Window is finished.

Showing the Window

The method to initialize and show the main Window is defined as follows:

First, we create the view model (see the Model-View-ViewModel pattern), which will take the EsapiWorker in order to use it (I’ll show you how in a moment). Then, we create the Window and show it as a dialog. It’s important to show it as a dialog so that this method doesn’t return until the Window is closed.

Using the EsapiWorker and Updating the UI

Now let’s actually use the EsapiWorker object in our view model. If you remember the example, we want to calculate the mean dose of a plan while showing its progress. Here’s the method in the view model that does this:

When we call the Run method, we pass it an Action that takes a ScriptContext as a parameter. This allows us to use the script context within our Action. Remember, this Action will be executed on the main thread because the EsapiWorker will send it to the main thread’s Dispatcher.

At each dose plane, we update the Progress property of the view model. This property is data-bound to a progress bar in the Window, so it’s automatically updated as the Progress value is changed. After the mean dose is calculated, the MeanDose property in the view model is set to the result. The MeanDose property is data-bound to a TextBlock.

The final step is to call the view model’s CalculateMeanDose method when the calculate button is clicked. One way to do this is to call it from the button’s Click handler.

When we run this script and start the calculation, the progress bar is updated as the calculation executes in the main thread. The UI is completely responsive during the calculation because the UI is on a separate thread. When the calculation finishes, the result is shown in a TextBlock.

Final Thoughts

The solution I’ve presented here is just one way to tackle the problem of multithreading with ESAPI. And it’s not a perfect solution. I haven’t had a chance to test all possible edge cases where it may fail. If you find another solution to this problem, please let me know!

4 thoughts on “Create ESAPI Scripts That Don’t Freeze the UI

  1. Rex Cardan

    Nice! I didn’t know about the DispatcherFrame. It seems kind of like a ManualResetEvent object. It just sits and waits for some action on another thread. I’m playing around with it. Good post. Thanks.

  2. Peter Kimstrand

    Thanks for an instructive example!

    I’ve implemented this and it works fine, but I can’t work out how to also implement a progress callback, ie updates to the UI. Any suggestions?
    I’ve tried using a BackgroundWorker, but I access the ESAPI too many times in the underlying code, the time consuming process, too make it work without completely refactoring the whole code base.

    1. Carlos Anderson Post author

      In my example, the Progress property of the view model acts as the callback. It’s data-bound to the view, so when it’s updated, the view (e.g., a progress bar) is also updated. Is this part not working for you?


Leave a Reply

Your email address will not be published. Required fields are marked *