Search code examples
c#.net-5winui-3winui

Flowing AsyncLocal to UI thread in WinUI 3


I am dispatching work to the UI thread using the MainWindow's DispatcherQueue. But when I do that, I lose the value stored in AsyncLocal. It seems to not flow the execution context automatically, but I am also worried what will happen if I flow it manually, since that would then also flow the sync context, locale info, etc.

So my question is, what would be the safe way to make sure that AsyncLocal variables are flowed to the UI thread?

Example of what I would like to work:

var asyncLocal = new AsyncLocal<int>();
asyncLocal.Value = 10;

var dispatcherQueue = _mainWindow.DispatcherQueue;
var taskCompletionSource = new TaskCompletionSource();

dispatcherQueue.TryEnqueue(new DispatcherQueueHandler(() =>
{
    Debug.Assert(asyncLocal.Value == 10);
    asyncLocal.Value = 20;
    Debug.Assert(asyncLocal.Value == 20);

    taskCompletionSource.SetResult();
}));

await taskCompletionSource.Task;
Debug.Assert(asyncLocal.Value == 10);

I have also noticed that the stack trace gets lost because the execution context isn't flowed, it would be nice to be able to fix this as well.


Solution

  • The short answer is you cannot. You cannot, because enqueuing an action to DispatcherQueue is not part of your asynchronous flow. It basically sends a message to the UI thread to execute a method.

    You need to share your data between threads - the async execution thread(s) and the UI thread. If you have only one async flow, i.e. your async code is not re-entrant and not started from multiple threads, then any global storage will do.

    If you code is re-entrant or can be started from multiple threads / tasks, then you need a global storage which can identify its calling context. For example, you can use ConcurrentDictionary<Guid, MyStorageClass>.

    Then you can do:

    var localStorage = new MyStorageClass(); //Init accordingly
    localStorage.Value = 10; //Assuming your class has a property called Value
    
    var sessionGuid = Guid.NewGuid();
    concurrentStorage.TryAdd (sessionGuid, localStorage);
    
    var dispatcherQueue = _mainWindow.DispatcherQueue;
    var taskCompletionSource = new TaskCompletionSource();
    
    dispatcherQueue.TryEnqueue(new DispatcherQueueHandler(() =>
    {
        Debug.Assert(concurrentStorage[sessionGuid].Value.Value == 10);
        concurrentStorage[sessionGuid].Value.Value = 20;
        Debug.Assert(concurrentStorage[sessionGuid].Value.Value == 20);
    
        taskCompletionSource.SetResult();
    }));
    
    await taskCompletionSource.Task;
    Debug.Assert(concurrentStorage[sessionGuid].Value.Value == 10);