C++/WinRT envy: Bringing thread switching tasks to C# (WPF and WinForms edition)

Raymond Chen

Raymond

Last time, we brought Thread­Switcher.Resume­Foreground­Async and Thread­Switcher.Resume­Background­Async to C# for UWP. Today, we’ll do the same for WPF and Windows Forms.

It’ll be easier the second and third times through because we already learned how to structure the implementation. It’s just the minor details that need to be tweaked.

using System;
using System.Runtime.CompilerServices;
using System.Threading;          // For ThreadPool
using System.Windows.Forms;      // For Windows Forms
using System.Windows.Threading;  // For WPF

// For WPF
struct DispatcherThreadSwitcher : INotifyCompletion
{
    internal DispatcherThreadSwitcher(Dispatcher dispatcher) =>
        this.dispatcher = dispatcher;
    public DispatcherThreadSwitcher GetAwaiter() => this;
    public bool IsCompleted => dispatcher.CheckAccess();
    public void GetResult() { }
    public void OnCompleted(Action continuation) =>
        dispatcher.BeginInvoke(continuation);
    Dispatcher dispatcher;
}

// For Windows Forms
struct ControlThreadSwitcher : INotifyCompletion
{
    internal ControlThreadSwitcher(Control control) =>
        this.control = control;
    public ControlThreadSwitcher GetAwaiter() => this;
    public bool IsCompleted => !control.InvokeRequired;
    public void GetResult() { }
    public void OnCompleted(Action continuation) =>
        control.BeginInvoke(continuation);
    Control control;
}

// For both WPF and Windows Forms
struct ThreadPoolThreadSwitcher : INotifyCompletion
{
    public ThreadPoolThreadSwitcher GetAwaiter() => this;
    public bool IsCompleted =>
        SynchronizationContext.Current == null;
    public void GetResult() { }
    public void OnCompleted(Action continuation) =>
        ThreadPool.QueueUserWorkItem(_ => continuation());
}

class ThreadSwitcher
{
    // For WPF
    static public DispatcherThreadSwitcher ResumeForegroundAsync(
        Dispatcher dispatcher) =>
        new DispatcherThreadSwitcher(dispatcher);

    // For Windows Forms
    static public ControlThreadSwitcher ResumeForegroundAsync(
        Control control) =>
        new ControlThreadSwitcher(control);

    // For both WPF and Windows Forms
    static public ThreadPoolThreadSwitcher ResumeBackgroundAsync() =>
         new ThreadPoolThreadSwitcher();
}

The principles for these helper classes are the same as for their UWP counterparts. They are merely adapting to a different control pattern.

WPF uses the System.­Threading.­Dispatcher class to control access to the UI thread. The way to check if you are on the dispatcher’s thread is to call Check­Access() and see if it grants you access. If so, then you are already on the dispatcher thread. Otherwise, you are on the wrong thread, and the way to get to the dispatcher thread is to use the Begin­Invoke method.

In Windows Forms, controls incorporate their own dispatcher. To determine if you’re on the control’s thread, you check the Invoke­Required property. If it tells you that you need to invoke, then you call Begin­Invoke to get to the correct thread.

Both WPF and Windows Forms use the CLR thread pool. As before, we check the Synchronization­Context to determine whether we are on a background thread already. If not, then we use Queue­User­Work­Item to get onto the thread pool.

So there we have it, C++/WinRT-style thread switching for three major C# user interface frameworks. If you feel inspired, you can do the same for Silverlight, Xamarin, or any other C# UI framework I may have forgotten.

Raymond Chen
Raymond Chen

Follow Raymond   

3 Comments
Avatar
Damien Knapman 2019-03-29 07:59:22
Something is screaming at me that there must be a neat way to get the appropriate SynchronizationContext injected into this setup and to use that instead of having three different implementations (and having to pass a relevant control every time when trying to "get back" to the UI). Haven't worked it out yet though. I suppose we could capture it during the first call to ResumeBackgroundAsync but that still leaves a gap if that's not the first call to TaskSwitcher.
Avatar
毅 吕 2019-03-29 18:40:13
Thanks for your better edition of thread switching. I've added all document comments and post your code and this page link at the end of my article: https://blog.walterlv.com/post/bring-thread-switching-tasks-to-csharp-for-wpf.html
Avatar
Joshua Schaeffer 2019-03-31 04:23:36
The original Async CTP (C#) had thread switching as a concept but then it was taken out. It wasn't complete though. WinRT lumbered along late in the game. I promise you cannot scale as a human programmer without this paradigm. A few non-obvious key principles for WPF: - ref-count UI threads before exiting them (using statements are best), and on desired exit run the DispatcherFrame to flush events and check the count at the lowest DispatcherPriority until the count goes to zero. - You want to enforce viewmodel thread affinity or this will get nasty. Put a Dispatcher property in every one of your viewmodels and make sure your viewmodel calls VerifyAccess(). Cross-thread dependency property assignments are sloppy and show that you don't understand your own threading model, plus they're impossible to ref-count. Switch to the UI thread before making the assignment, don't just fire-and-forget. - Support a CancellationToken in the switch call so that if the target or source Dispatcher is shut down or shutting down you can abort the switch. Add a DispatcherPriority parameter to the switch (this matters with screen updates). - You must deterministically destroy all your viewmodels or the GC tree will get out of control. Give every viewmodel a cancellation token and implement IDisposable. This token will be used to interrupt thread switches. - Make the switch call an extension method on Dispatcher to make the code look unintimidating. public static void SwitchToAsync(this Dispatcher dispatcher,DispatcherPriority priority = DispatcherPriority.Send,CancellationToken cancellationToken = default); - Don't use the SynchronizationContext to detect the thread, use (Dispatcher.Thread == Thread.CurrentThread) or save the thread ID separately so you don't risk creating Dispatchers on the thread pool. The reason is that WPF lets you create custom DispatcherSynchronizationContext to set the DispatcherPriority that Post() uses. You will be using SwitchTo methods to compose enclosing Task methods that handle the SynchronizationContext implicitly. - Always trap the switch call. UI threads end when the user wants them to. - Example code: public async Task StuffAsync(){try{using (RefCountCurrentThread()){await SwitchToThreadPoolAsync();// ....await Dispatcher.SwitchToAsync(DispatcherPriority.Render, _disposalCancellationToken);}}catch{// etc...}} This subject really deserves a Microsoft Press book but nobody writes those anymore.
Avatar
Aybe One 2019-04-01 11:25:26
It was well worth the wait, you solved my async level loading problem in Unity so elegantly that I only have to add those helper methods between critical parts :) Thanks a million Raymond !!!