Automated Planning from Templates III: Multi-threading
- mschmidt33
- Sep 7
- 7 min read
Automated Planning from Templates Recap
In Part I, we introduced the concept of clinical template–driven automation using the open-source Clinical Template Reader. This post demonstrated how to load and parse a Clinical Protocol XML file and use its contents to programmatically generate a valid VMAT plan inside Eclipse. At first, the post deep dives into the structure of a Clinical Protocol, and then a few minor modifications required to the Clinical Protocol-- setting the correct isocenter position and field fitting -- were reviewed. The application included steps like assigning the target structure and treatment machine, defining the isocenter, and setting up arc beams—all from a standardized protocol format.
In Part II, we extended the workflow by applying RapidPlan knowledge-based optimization using the OptimizeFromRapidPlanModel method. This allowed us to invoke a RapidPlan model and perform automated dose optimization, structure matching, and dose calculation—all with just a few lines of C# code. We highlighted how easily RapidPlan can be integrated into clinical scripting tools when using protocol-driven automation. This post also details the RapidPlan structure within Eclipse and makes some sense of the Dictionary (key-value pair) structure within the RapidPlan system. Finally, the post reviews how the Clinical Template Reader API can read into the Clinical Goals within a Clinical Protocol and send the user some review metrics quantifying the quality of the generated treatment plan.

Overview
In this final installment, our goal is to make the automated planning application more responsive and production-ready by implementing a multi-threaded architecture. Specifically, we'll decouple the ESAPI-heavy operations—such as structure matching, optimization, and dose calculation—from the UI thread using a background worker pattern. Inspired by Carlos Anderson’s excellent blog post, we’ll integrate a dedicated ESAPI thread to maintain application responsiveness, and enhance the user experience by adding a progress bar to visualize long-running operations. This approach ensures smoother interaction for clinical users and lays the foundation for more scalable automation tools. This project is available open-source on github in the multi-thread branch of the project repository.
ESAPI Worker Overview
Carlos Anderson tackles a common challenge when building ESAPI-based applications: long-running operations freezing the UI. Because ESAPI requires all interactions to occur on the same thread as the Execute method, heavy tasks block the UI thread unless handled thoughtfully. His solution? Shift the user interface to a dedicated thread and dispatch ESAPI work back to the original thread:
EsapiWorker Class: This utility wraps ESAPI execution logic, utilizing the Dispatcher from the main ESAPI thread. The Run(Action<ScriptContext>) method queues ESAPI operations via Dispatcher.BeginInvoke, ensuring UI-thread responsiveness.
Separate UI Thread: The application spawns a new STA (single-threaded apartment) thread for the WPF UI using a simple helper RunOnNewStaThread(Action). This isolates the UI, allowing it to remain responsive even when ESAPI tasks are running.
Script Lifetime Management: By employing a DispatcherFrame, the main thread stays alive and the script doesn’t terminate prematurely—maintaining synchronization between UI and ESAPI operations.
This pattern elegantly balances ESAPI’s threading constraints with smooth user experience—making it a perfect fit for refactoring the ProtocolPlanner into a multi-threaded, responsive tool.
Each time ESAPI data is accessed, whether by property or method invocation, the ESAPI Worker must be used.
There is a bit of nuance to remember when using an ESAPI Worker. Each time ESAPI data is accessed, whether by property or method invocation, the ESAPI Worker must be used. This may make refactoring challenging if ESAPI calls are not extracted to their own methods. During this post, special attention will be paid to carefully integrating the ESAPI Worker into an existing application.
Adding the ESAPI Worker
WPF requires that all UI elements (e.g., Window, UserControl) be created and accessed on the main UI thread.
First, add the ESAPIWorker to the application. In the existing Services folder, create a new class called ESAPI Worker and paste the class definition as seen in Carlos's blog.
public class ESAPIWorker
{
private readonly ScriptContext _scriptContext;
private readonly Dispatcher _dispatcher;
public ESAPIWorker(ScriptContext scriptContext)
{
_scriptContext = scriptContext;
_dispatcher = Dispatcher.CurrentDispatcher;
}
public void Run(Action<ScriptContext> a)
{
dispatcher.BeginInvoke(a, scriptContext);
}
}Next in the ProtocolPlanner.cs file - the launch point for the script- add the few methods required to set up the dispatcher frame. The two methods we will add to this class are the RunOnNewStaThread and InitializeAndStartMainWindow methods. Their definitions (slightly modified for this application) are below.
private void RunOnNewStaThread(Action a)
{
Thread thread = new Thread(() => a());
thread.SetApartmentState(ApartmentState.STA);
thread.IsBackground = true;
thread.Start();
}
private void InitializeAndStartMainWindow(ESAPIWorker esapiWorker, ClinicalTemplate template)
{
var viewModel = new MainViewModel(esapiWorker, template);
var view = new MainView();
view.DataContext = viewModel;
Window window = new Window()
{
Height = 650,
Width = 850};
window.Content = view;
window.ShowDialog();
}
} The Execute method is modified to remove the Window object from the method definition. This Window object comes from Eclipse, and therefore brings unnecessary complication when trying to separate to a individual thread. The window is invoked by the Eclipse application, and the thread that calls Execute is not the main UI thread of your WPF application. WPF requires that all UI elements (e.g., Window, UserControl) be created and accessed on the main UI thread. If you attempt to create or interact with WPF UI elements on the thread provided by ESAPI, you'll encounter threading issues, such as the error: "The calling thread cannot access this object because a different thread owns it."

To remedy this, a new Window is being created in the InitializeAndStartMainWindow method above. Alternatively, the MainView could be modified from a UserControl object to a Window object directly, removing the requirement for a intermediary parent window. Finally update the Execute method to call these new methods directly as follows:
public void Execute(ScriptContext context)
{
// TODO : Add here the code that is called when the script is launched from Eclipse.
// The ESAPI worker needs to be created in the main thread
var esapiWorker = new ESAPIWorker(context);
// This new queue of tasks will prevent the script
// for exiting until the new window is closed
DispatcherFrame frame = new DispatcherFrame();
ClinicalTemplate clinicalTemplate = new ClinicalTemplate("Master-AE.vic.com");
RunOnNewStaThread(() =>
{
// This method won't return until the window is closed
InitializeAndStartMainWindow(esapiWorker, clinicalTemplate);
// End the queue so that the script can exit
frame.Continue = false;
});
// Start the new queue, waiting until the window is closed
Dispatcher.PushFrame(frame);
}Refactoring the ViewModel
The MainViewModel may need some refactoring in order to implement this ESAPI Worker. First, the MainViewModel constructor had a previous dependency on the ScriptContext. Currently, we want the constructor to input the ESAPIWorker object instead. Please note that I have removed the private ScriptContext _context; field from this class entirely.
//constructor
private ESAPIWorker _esapiWorker;
public MainViewModel(ESAPIWorker esapiWorker, ClinicalTemplate clinicalTemplate)
{
_esapiWorker = esapiWorker;
_clinicalProtocols = clinicalTemplate;
ClinicalProtocols = new List<Protocol>();
...
}Find all instance of the ScriptContext object being invoked (or other ESAPI objects) and instead change them to utilizing the Run method of the ESAPI Worker. Here is an example with the method that extracts the RapidPlan models. Here, the scrptx object passed into the Run method will invoke the ScriptContext object to perform the extraction of RapidPlan models.
private void GetRapidPlanModels()
{
_esapiWorker.Run(scrptx =>
{
foreach (var model in scrptx.Calculation.GetDvhEstimationModelSummaries())
{
RapidPlanModels.Add(model);
}
});
}Similarly, the entirety of OnOptimize and GeneratePlan methods have been wrapped entirely in the _esapiWorker.Run method.
If the code is compiled and tested at the moment, the following error will occur when attempting to load the application. The purpose of this error is the methods adding items to the UI from within the ESAPI Worker thread. In order to add items to a collection from within the ESAPI thread, the collection must be explicitly marked with the EnableCollectionSynchronization method. The issue of the matter here is that the DoseMetrics collection is being modified from the ESAPI thread while its bound to the UI thread.

Binding Operations
When you use BindingOperations.EnableCollectionSynchronization, you’re telling WPF that multiple threads might try to access or update a collection (like ObservableCollection or List) at the same time. Normally, WPF expects collections to only be modified from the UI thread, and it will throw an error if another thread touches them. By enabling synchronization, you give WPF a way to coordinate safe access.

When WPF or another thread wants to access or modify the collection, it locks this object first, ensuring that only one thread at a time is inside the collection code. In order to allow for these collection objects to be modified from both threads, the RapidPlanModels object has been changed from a List to an ObservableCollection and the BindingOperations.EnableCollectionSynchronization method called on each collection.
private readonly object _rapidPlanModelsLockObject = new object();
private readonly object _doseMetricsLockObject = new object();
public MainViewModel(ESAPIWorker esapiWorker, ClinicalTemplate clinicalTemplate)
{
_esapiWorker = esapiWorker;
_clinicalProtocols = clinicalTemplate;
ClinicalProtocols = new List<Protocol>();
RapidPlanModels = new ObservableCollection<DVHEstimationModelSummary>();
BindingOperations.EnableCollectionSynchronization(RapidPlanModels, _rapidPlanModelsLockObject);
DoseMetrics = new ObservableCollection<MetricModel>();
BindingOperations.EnableCollectionSynchronization(DoseMetrics, _doseMetricsLockObject);
...
}Adding the Progress Bar
Progress bars help users understand that the application is working in the background. Without a progress bar, users may think the application has frozen or crashed during long operations like optimization or dose calculation. A progress bar reassures them that the system is working as intended. By exposing what’s happening “behind the scenes,” progress bars build user trust. This is especially important in clinical software where users need confidence that a script or application is functioning properly.
In order to add a new progress bar, the MainView.xaml has a new row added with Auto as the height. The StackPanel that holds the datagrid has been moved to Row="2", and a new row is placed in between this and the Stackpanel holding the RapidPlanModels.
<!-- Progress bar-->
<ProgressBar Grid.Row="1" Grid.ColumnSpan="2" Height="30" Margin="10"
Minimum="0" Maximum="100" Value="{Binding ProgressValue}"
/>Add a new full property called ProgressValue and utilize it during the OnOptimize method.
_esapiWorker.Run(scrptx =>
{
CalculationLog = "Starting Optimization...";
ProgressValue = 10;//10
var dvhe_response = _clinicalProtocols.OptimizeFromRapidPlanModel(scrptx.Calculation.GetDvhEstimationModelStructures(SelectedRapidPlanModel.ModelUID),
AutoPlan,
SelectedRapidPlanModel,
null,
null,
false);//false for testing.
ProgressValue += 50;//60
CalculationLog += dvhe_response;
CalculationLog += "\nOptimization Complete.";
CalculationLog += "\nCalculating Dose";
AutoPlan.CalculateDose();
ProgressValue += 30;//90
CalculationLog += "\nDose Calculation Complete.";
CalculationLog += "\nComparing Dose Metrics to Clinical Protocol.";
var doseMetrics = _clinicalProtocols.CompareProtocolDoseMetrics(AutoPlan, SelectedClinicalProtocol);
foreach (var metric in doseMetrics)
{
DoseMetrics.Add(new MetricModel(metric));
}
ProgressValue += 10;//100
});Now, your users will enjoy having a progress bar to help guide their way through the application!

Summary
In this third installment of the Automated Planning from Templates series, we addressed a common challenge when building ESAPI-based applications: the UI freezing while optimization or dose calculations run. This happens because long-running ESAPI operations execute on the same thread as the application window, blocking user interaction. To solve this, we implemented multi-threading by separating the ESAPI worker thread from the UI thread.
However, once work moved off the UI thread, the application was unable to use the ESAPI provided window, as that window is generated in the ESAPI thread as opposed to the required UI thread. The application also encountered synchronization errors when updating shared collections like ObservableCollection. The solution was to use BindingOperations.EnableCollectionSynchronization, which ensures thread-safe access to collections by requiring a lock object to control when and how each thread modifies the data. This allows both the ESAPI worker and the UI to safely communicate without conflict.
Finally, we introduced a progress bar to give users clear feedback during optimization and dose calculation. The progress bar reassures users that the application is working, improves perceived performance, and prevents frustration from an unresponsive interface. Together, these refinements create a smoother, safer, and more user-friendly automated planning application.







Comments