Search code examples
c#wpfasync-awaitdispatcher

Serializing async tasks on Dispatcher thread


We have a WPF dialog library, which exposes an async Task ShowAsync(...) method, library has to be used. Around it, we've built a MVVM-based singleton service, DialogService with our own async ShowAsync method, and view models call it when needed. The problem is, the library does not support showing more than one dialog at the time and we have to keep the dispatcher thread working, so if another operation requests a dialog before the user closes the first one, the library throws an exception, which then cascades into another dialog call, so on and so on.

So we need to implement some sort of queueing, in the sense that second task cannot even begin (be cold?) until the first task is completed. It all has to happen in dispatcher thread, but on a plus not the ShowAsync is always called from the dispatcher thread and we use ConfigureAwait(true) on calling library method. Some of the calls to dialog service have their own ContinueWith constructs, if that is important.

I've seen some solutions like SerialQueue et al, but they all deal with serializing tasks in general, without caring on what context and thread they run, we need a more WPFy solution where everything runs on dispatcher thread without making it unresponsive.

Any ideas would be welcome.


Solution

  • I solved it with a semaphore with a single choke point, the dialogs will queue up waiting for the semaphore to release. This solution seems more in line with await/async philosophy:

    internal class DialogService : IDialogService
    {
        private readonly DispatcherSynchronizationContextAwaiter uiContextAwaiter;
        private readonly SemaphoreSlim dialogSemaphore = new(1);
    
        public DialogService(DispatcherSynchronizationContextAwaiter uiContextAwaiter)
        {
            this.uiContextAwaiter = uiContextAwaiter;
        }
    
        public async Task<DialogResult> ShowDialogAsync(string title, string message, DialogType dialogType = DialogType.Information, DialogButtons dialogButtons = DialogButtons.OK)
        {
            await dialogSemaphore.WaitAsync();
            try
            {
                await uiContextAwaiter;
                var result = await DialogHost.Show(new DialogViewModel {Title = title, Message = message, DialogType = dialogType, DialogButtons = dialogButtons});
                return (DialogResult?) result ?? DialogResult.OK;
            }
            finally
            {
                dialogSemaphore.Release();
            }
        }
    }
    

    DispatcherSynchronizationContextAwaiter is not fundamentally important for my problem, but it does allow the ShowDialogAsync to be called from any thread. It simply posts a continuation on the dispatcher thread. I took this code from Thomas Levesque's blog from 2015, and adjusted to my needs. This is the source if you need it:

    internal class DispatcherSynchronizationContextAwaiter: INotifyCompletion
    {
        private static readonly SendOrPostCallback postCallback = state => ((Action)state)?.Invoke();
    
        private readonly SynchronizationContext context;
        public DispatcherSynchronizationContextAwaiter(SynchronizationContext context)
        {
            this.context = context;
        }
    
        public bool IsCompleted => context == SynchronizationContext.Current;
    
        public void OnCompleted(Action continuation) => context.Post(postCallback, continuation);
        
        public void GetResult() { }
    
        // clone yourself on GetAwait
        public  DispatcherSynchronizationContextAwaiter GetAwaiter() => new(context);
    }