Async/await with COM plugins

Add-in Express™ Support Service
That's what is more important than anything else

Async/await with COM plugins
 
James Telfer




Posts: 9
Joined: 2020-03-13
As has been noted in other places on the web and in this forum, using async/await with COM can be problematic, as the code does not resume execution on the UI thread. This causes cross-thread exception in WPF and other errors in Windows Forms.

When looking into this issue with the add-in I was developing, it appears to stem from the fact that the SynchronizationContext is not set on the thread when the fired event originates from a COM context.

To fix this, it is possible to set the value of SynchronizationContext.Current to a reasonable value directly; e.g.:
SynchronizationContext.Current = new WindowsFormsSynchronizationContext()


However the simplest strategy appears to be to wrap the event handler in a lambda that causes the fired event to be invoked on the UI thread. The side effect of this is that the SynchronizationContext is then set correctly, and await'ed calls will resume on the UI thread.


_loginWebBrowser.Navigating += (_, args) => Dispatcher.Invoke(() => LoginWebBrowser_Navigating());


The above registers an event handler that simply delegates the calling of the 'real' event handler method via the WPF dispatcher. A similar strategy works for Windows Forms.
Posted 25 Mar, 2020 06:51:14 Top
Andrei Smolin


Add-in Express team


Posts: 18821
Joined: 2006-05-11
Hello James,

You are free to use background threads. You must not call into any Office object model when on such a thread, though. For the code to not run on a background thread, you must not create background threads explicitly nor via ConfigureAwait(false) and Task.Run().


Andrei Smolin
Add-in Express Team Leader
Posted 25 Mar, 2020 07:47:49 Top
James Telfer




Posts: 9
Joined: 2020-03-13
Apologies for not being clear enough, let me explain better.

If you have a WPF app, and you register a Click handler, the handler code will fire on the UI thread. If you then await an async method, the code after the await will also execute on the UI thread:


public void HandleClick(object sender, EventArgs e)
{
    // we start on the UI thread
    textBox.Text = "UI change is OK here";

    // may take a while, and may execute on a different thread
    var text = await GetRemoteText();

    // when it resumes after await, it's back on the UI thread
    textBox.Text = $"Remote text: ${text}"
}


The above works fine in an add in. The way it works though uses a bit of magic: the SychronizationContext. Without a WPF-aware SynchronizationContext, the code after the await wouldn't necessarily execute on the UI thread.

But as I said, even in the context of a COM add-in, we're OK so far.

However, if the event comes not from a WPF or Windows Forms object, but comes instead from a COM object: that is a problem.


public void adxOutlookAppEvents_ExplorerSelectionChange(object sender, object explorer)
{
    // we start on the UI thread
    textBox.Text = "UI change is OK here";

    // may take a while, and may execute on a different thread
    var text = await GetRemoteText();

    // when it resumes after await, UI code will most likely fail
    textBox.Text = $"Remote text: ${text}"
}


The COM event is fired on the UI thread, but without a SychronizationContext.Current value. This means that the code after the await will not necessarily be executed on the UI thread and will then fail.

The way to fix this is to ensure that there's a context: either set an appropriate (WPF/Windows Forms) sync context or use Dispatch (as shown above).

It is possible to wrap every change to the UI in a COM event handler after the await with a Dispatch call, but this leads to quite ugly code. Setting the SynchronizationContext at the boundary (when the event is fired) allows for natural async/await code that acts as you would normally expect of a WinForms/WPF app.

This behaviour can be observed easily by watching the SynchronizationContext.Current value in the debugger when a WPF/WinForms event is fired vs a COM event. When it's a COM event, the value is null.

It would be quite neat if Add-in Express was able to set the context when the event fired.
Posted 25 Mar, 2020 16:07:06 Top
James Telfer




Posts: 9
Joined: 2020-03-13
For context, here are three threads where this problem is noted:


One of these threads references a relevant StackOverflow post: https://stackoverflow.com/questions/19535147/await-and-synchronizationcontext-in-a-managed-component-hosted-by-an-unmanaged-a

Instead of advising the use of the SendMessage approach, if they were to wrap calls in a with a SynchronizationContext they would be fine to write more idiomatic UI code.
Posted 25 Mar, 2020 16:16:55 Top
Andrei Smolin


Add-in Express team


Posts: 18821
Joined: 2006-05-11
Hello James,

Thank you for the suggestion. We will consider implementing it: we would also need to support .NET 2.0 and previous async programming models.

You can use this class to work around this:

class MySynchronizationContext : IDisposable
{
    private SynchronizationContext oldContext = null;
    private SynchronizationContext newContext = null;
    bool restoreContext = false;
    public MySynchronizationContext(bool restoreContext= false)
    {
        this.restoreContext = restoreContext;
        oldContext = SynchronizationContext.Current;
        if (!(oldContext is WindowsFormsSynchronizationContext))
        {
            newContext = new WindowsFormsSynchronizationContext();
            SynchronizationContext.SetSynchronizationContext(newContext);
        }
    }
    void IDisposable.Dispose()
    {
        if (restoreContext)
        {
            SynchronizationContext.SetSynchronizationContext(oldContext);
        }
    }
}


Here's how you use it:


private async void DoBreakfast(object sender, EventArgs e)
{
    using (MySynchronizationContext synchronization = new MySynchronizationContext())
    {
        Log.Print(string.Format("DoBreakfast start"));
        Coffee cup = PourCoffee();
        Log.Print("coffee is ready");
        Task<Egg> eggsTask = FryEggs(2);
        Egg eggs = await eggsTask;
        Log.Print("eggs are ready");

        Task<Bacon> baconTask = FryBacon(3);
        Bacon bacon = await baconTask;
        Log.Print("bacon is ready");
        Task<Toast> toastTask = ToastBread(2);
        Toast toast = await toastTask;
        ApplyButter(toast);
        ApplyJam(toast);
        Log.Print("toast is ready");
        Juice oj = PourOJ();
        Log.Print("oj is ready");
        Log.Print("Breakfast is ready!");
    }
}



Andrei Smolin
Add-in Express Team Leader
Posted 26 Mar, 2020 05:01:31 Top