[UPDATE] As the OP has updated the question and specified he's using out-of-proc COM objects, the custom STA thread plumbing described below doesn't make sense. Now, a simple await Task.Run(() => { /* call the out-of-proc COM */})
is enough to keep the UI responsive. Kudos to @acelent for clarifying this point.
Recently I answered a related question: StaTaskScheduler and STA thread message pumping.
The solution was to create and use STA COM objects on a dedicated background STA thread which provides both message pumping and thread affinity for those COM objects.
I'd like to show how ThreadWithAffinityContext
can be used in your case, with async/await
:
dynamic _comObject = null;
ThreadWithAffinityContext _staThread = null;
// Start the long-running task
Task NewCommandHandlerAsync()
{
// create the ThreadWithAffinityContext if haven't done this yet
if (_staThread == null)
_staThread = new ThreadWithAffinityContext(
staThread: true,
pumpMessages: true);
// create the COM Object if haven't done this yet
if (_comObject == null)
{
await _staThread.Run(() =>
{
// _comObject will live on a dedicated STA thread,
// run by ThreadWithAffinityContext
_comObject = new ComObject();
}, CancellationToken.None);
}
// use the COM object
await _staThread.Run(() =>
{
// run a lengthy process
_comObject.DoWork();
}, CancellationToken.None);
}
// keep track of pending NewCommandHandlerAsync
Task _newCommandHandler = null;
// handle a WPF command
private async void NewCommand_Executed(object sender, ExecutedRoutedEventArgs e)
{
try
{
// avoid re-entrancy (i.e., running two NewCommandHandlerAsync in parallel)
if (_newCommandHandler != null)
throw new InvalidOperationException("One NewCommandHandlerAsync at a time!");
try
{
await _newCommandHandler = NewCommandHandlerAsync();
}
finally
{
_newCommandHandler = null;
}
}
catch (Exception ex)
{
// handle all exceptions possibly thrown inside "async void" method
MessageBox.Show(ex.Message);
}
}
The fact that we have offloaded the lengthy process _comObject.DoWork()
to a separate thread doesn't automatically solve the other common UI-related problem:
How to handle the UI when the lengthy background operation is pending?
There is a number of options. E.g., you can disable the UI elements which fire NewCommand_Executed
event, to avoid re-entrancy, and enable another UI element to allow the user to cancel the pending work (a Stop
button). You should also provide some progress feedback, if your COM object supports that.
Alternatively, you can display a modal dialog before staring the long-running task and hide it when the task has completed. As far as the UI usability goes, modality is less desirable, but it's very easy to implement (example).