How do I implement InvokeRequired UI pattern inside an async method?

2.7k Views Asked by At

Let's say I have a Form that tries to deal with a multi-threaded environment; it therefore checks if it's running on the UI thread before any UI modification is done:

partial class SomeForm : Form
{
    public void DoSomethingToUserInterface()
    {
        if (InvokeRequired)
        {
            BeginInvoke(delegate { DoSomethingToUserInterface() });
        }
        else
        {
            … // do the actual work (e.g. manipulate the form or its elements)
        }
    }
}

Now let's say I am performing some lengthy operation inside the part of that method; I'd therefore like to make it asynchronous using async/await.

Given that I should change the method signature to return a Task instead of void (so that exceptions can be caught), how would I implement the part that performs the BeginInvoke? What should it return?

public async Task DoSomethingToUserInterfaceAsync()
{
    if (InvokeRequired)
    {
        // what do I put here?
    }
    {
        … // like before (but can now use `await` expressions)
    }
}
2

There are 2 best solutions below

8
On BEST ANSWER

When using async-await with a custom awaiter such as the common TaskAwaiter, the SynchronizationContext is being implicitly captured for you. Later, when the asynchronous method completes, the continuation (any code after the await) is being marshaled back to that same sync context using SynchronizationContext.Post.

This altogether eliminates the need to use InvokeRequired and other techniques used to mainpulate work on the UI thread. In order to do that, you'll have to trace your method calls all the way to the top level ones and refactor them to use async-await probably.

But, to address the specific problem as is, what you can do is capture the WinFormSynchronizationContext when your Form initializes:

partial class SomeForm : Form
{
    private TaskScheduler _uiTaskScheduler;
    public SomeForm()
    {
        _uiTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
    }
}

And later use it when you want to await:

if (InvokeRequired)
{
    Task uiTask = new Task(() => DoSomethingToUserInterface());
    uiTask.RunSynchronously(_uiTaskScheduler);
}
else
{
    // Do async work
}
8
On

You can use TaskScheduler.FromCurrentSynchronizationContext to get the task scheduler for current synchronization context(For UI thread), and store it in a field for later use.

Then when you're interested to start any task in UI thread, you have to pass the uiScheduler to the StartNew method, so that TPL will schedule the task in the provided scheduler(UI thread in this case).

Anyway you decided to run the stuff in UI thread, so just schedule it to UIScheduler, you don't need to check for InvokeRequired whatsoever.

public async Task DoSomethingToUserInterfaceAsync()
{
    await Task.Factory.StartNew(() => DoSomethingToUserInterface(), CancellationToken.None, TaskCreationOptions.None, uiScheduler);
    ...
}

To retrieve the UI scheduler, you can use the following code

private TaskScheduler uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();

Note: It is extremely important that TaskScheduler.FromCurrentSynchronizationContext should be called only from UI thread, otherwise, it will throw exception, or you'll get a TaskScheduler for some other SynchronizationContext which won't do what you need.

Also note that if you have started the async operation from UI thread itself, you don't need any of the above magic. await will resume in the context where it has been started.