1

I am trying to show a WinUI dialog - i.e. a class that derives from ContentDialog - from code that executes from a background thread and I need to wait for the result of the dialog.

I know that showing a dialog from a background thread is only possible with some kind of dispatcher that dispatches this code back to the UI thread.

This is the method that shows my dialog in its current form:

public async Task<MyDialogResult> ShowMyDialogAsync(MyViewModel viewModel, string primaryButtonText, string closeButtonText)
{
    // EXCEPTION OCCURS HERE WHEN CALLING FROM NON UI-THREAD
    MyDialog dialog = new MyDialog(); // This class derives from ContentDialog

    // Set dialog properties
    dialog.PrimaryButtonText = primaryButtonText;
    dialog.CloseButtonText = closeButtonText;
    dialog.ViewModel = viewModel;
    dialog.XamlRoot = _xamlRoot;

    await dialog.ShowAsync();
    return dialog.ViewModel.MyDialogResult;
}

Please note that I need some result from this dialog which is entered by the user. That is why the method has a return value of Task<MyDialogResult>.

While doing my research I came across this answer, which refers to the article Await a UI task sent from a background thread from Microsoft.

There they use a method RunAsync which gets transformed into a method RunTaskAsync<T>. The RunAsync method can be found on the CoreDispatcher class.

My problem is: I am unable to try this approach, because the CoreDispatcher property seems always to be null at runtime when accessing it (from my MainWindow class for example). Here is a screenshot of my MainWindow instance during runtime:

enter image description here

According to this article, "Dispatcher" being null seems to be a design choice.

The only similar property available is a DispatcherQueue. But this only has the method TryEnqueue. But I do not know how to use it in they way I need, which is not only to execute the code on the UI thread but also wait for its completion, so code can run after it in a controlled manner.

Does anyone know how an approach how to show in WinUI ContentDialog using some kind of dispatcher and wait for the result of the dialog?

Martin
  • 5,165
  • 1
  • 37
  • 50

2 Answers2

2

I got it working.

Part 1

The first part of the solution for me was to find out that there is a Nuget package "CommunityToolkit.WinUI" that does contain a class DispatcherQueueExtensions which has extension methods for the DispatcherQueue class, among those some "EnqueueAsync" methods.

This DispatcherQueueExtensions documentation refers to UWP, but the usage for WinUI is identical.

With the "CommunityToolkit.WinUI" Nuget package in use, you can write code like this:

// Execute some asynchronous code
await dispatcherQueue.EnqueueAsync(async () =>
{
    await Task.Delay(100);
});

// Execute some asynchronous code that also returns a value
int someOtherValue = await dispatcherQueue.EnqueueAsync(async () =>
{
    await Task.Delay(100);

    return 42;
});

Part 2

The second part focuses on the problem, that the code running on the background thread must no only invoke code on the UI thread, but also wait for its completion (because user data from a dialog is required in my case).

I found a question "RunAsync - How do I await the completion of work on the UI thread?" here on stackoveflow.

In this answer the user "Mike" introduces a class DispatcherTaskExtensions which solves this problem. I rewrote the code to work with instances of type DispatcherQueue instead of CoreDispatcher (which is not supported in WinUI, as I discovered).

/// <summary>
/// Extension methods for class <see cref="DispatcherQueue"/>.
///
/// See: https://stackoverflow.com/questions/19133660/runasync-how-do-i-await-the-completion-of-work-on-the-ui-thread?noredirect=1&lq=1
/// </summary>
public static class DispatcherQueueExtensions
{
    /// <summary>
    /// Runs the task within <paramref name="func"/> to completion using the given <paramref name="dispatcherQueue"/>.
    /// The task within <paramref name="func"/> has return type <typeparamref name="T"/>.
    /// </summary>
    public static async Task<T> RunTaskToCompletionAsync<T>(this DispatcherQueue dispatcherQueue,
        Func<Task<T>> func, DispatcherQueuePriority priority = DispatcherQueuePriority.Normal)
    {
        var taskCompletionSource = new TaskCompletionSource<T>();
        await dispatcherQueue.EnqueueAsync(( async () =>
        {
            try
            {
                taskCompletionSource.SetResult(await func());
            }
            catch (Exception ex)
            {
                taskCompletionSource.SetException(ex);
            }
        }), priority);
        return await taskCompletionSource.Task;
    }

    /// <summary>
    /// Runs the task within <paramref name="func"/> to completion using the given <paramref name="dispatcherQueue"/>.
    /// The task within <paramref name="func"/> has return type void.
    /// 
    /// There is no TaskCompletionSource "void" so we use a bool that we throw away.
    /// </summary>
    public static async Task RunTaskToCompletionAsync(this DispatcherQueue dispatcherQueue,
        Func<Task> func, DispatcherQueuePriority priority = DispatcherQueuePriority.Normal) =>
        await RunTaskToCompletionAsync(dispatcherQueue, async () => { await func(); return false; }, priority);
}

Usage

This example uses a class that has a private field _dispatcherQueue of type DispatcherQueue and a private field _xamlRoot of type UIElement that need to be set in advance.

Now we can use this like so:

public async Task<MyDialogResult> ShowMyDialogAsync(MyViewModel viewModel, string primaryButtonText, string closeButtonText)
{
        // This code shows an dialog that the user must answer, the call must be dispatched to the UI thread
        // Do the UI work, and await for it to complete before continuing execution:
        var result = await _dispatcherQueue.RunTaskToCompletionAsync( async () =>
            {
               MyDialog dialog = new MyDialog(); // This class derives from ContentDialog

               // Set dialog properties
               dialog.PrimaryButtonText = primaryButtonText;
               dialog.CloseButtonText = closeButtonText;
               dialog.ViewModel = viewModel;
               dialog.XamlRoot = _xamlRoot;   
               await dialog.ShowAsync();

               // CODE WILL CONTINUE HERE AS SOON AS DIALOG IS CLOSED
               return dialog.ViewModel.MyDialogResult; // returns type MyDialogResult
            }
        );

        return result;
}
Martin
  • 5,165
  • 1
  • 37
  • 50
  • What's the benefit of using a `TaskCompletionSource` here instead of simply using the async `EnqueueAsync` in the `DispatcherQueueExtensions` class? Did you try my suggestion? – mm8 Apr 28 '22 at 20:40
  • My code is a little bit different from the question I posted. I had the problem that background code just continued to execute and did not wait the dialog result was there / the dialog was closed. I will try again with your suggestion. This felt like the same problem posted in this question: https://stackoverflow.com/questions/19133660/runasync-how-do-i-await-the-completion-of-work-on-the-ui-thread/38135118#38135118 That it is why I used the approach from user "Mike" too, in addition to the `DispatcherQueueExtensions` class. – Martin Apr 29 '22 at 07:16
  • If you use the overload that accepts a `Func`, you should be able to await the `ShowAsync()` method of the `ContentDialog` as per my example. Then the task returned by `EnqueueAsync` won't complete until the dialog has been dismissed. You can then continue with executing code on the background thread. – mm8 Apr 29 '22 at 07:48
  • You need to make sure the property XamlRoot of ContentDialog is assigned: contentDialog.XamlRoot = App.MainWindow.Content.XamlRoot; or contentDialog.XamlRoot = (App.Current as App)?.m_window.Content.XamlRoot – Will Young Aug 14 '23 at 05:58
1

As you have already discovered yourself, you should be able to use the DispatcherQueueExtensions in the CommunityToolkit.WinUI NuGet package.

Just make sure that you call the DispatcherQueue.GetForCurrentThread() method to get a reference to the DispatcherQueue on the UI thread and that you specify a XamlRoot for the ContentDialog one way or another.

Below is a working example.

using CommunityToolkit.WinUI;
...

DispatcherQueue dispatcherQueue = DispatcherQueue.GetForCurrentThread();

await Task.Run(async () =>
{
    Thread.Sleep(3000); //do something on the background thread...

    int someValue = await dispatcherQueue.EnqueueAsync(async () =>
    {
        ContentDialog contentDialog = new ContentDialog()
        {
            Title ="title...",
            Content = "content...",
            CloseButtonText = "Close"
        };
        contentDialog.XamlRoot = (App.Current as App)?.m_window.Content.XamlRoot;
        await contentDialog.ShowAsync();

        return 1;
    });

    // continue doing doing something on the background thread...
});
mm8
  • 163,881
  • 10
  • 57
  • 88
  • I am currently using this "DispatcherQueueExtensions" https://learn.microsoft.com/en-us/windows/communitytoolkit/extensions/dispatcherqueueextensions in combination with this approach: https://stackoverflow.com/a/38135118/4424024 If it works, it will post it as an answer here – Martin Apr 27 '22 at 14:04
  • 1
    Ok, the link was not targeted for WinUI. However the Nuget package "CommunityToolkit.WinUI" does contain a class "DispatcherQueueExtensions" which as extension methods for the "DispatcherQueue" class, among those some "EnqueueAsync" methods. – Martin Apr 27 '22 at 14:25
  • 1
    Using the `DispatcherQueueExtensions` seems to work just fine. Please see my edited answer. – mm8 Apr 27 '22 at 14:45
  • You should be able to simply replace the call to `contentDialog.ShowAsync()` with your custom method assuming that your `ContentDialog` has a valid `XamlRoot` – mm8 Apr 27 '22 at 14:47
  • Does your code stop between lines "await contentDialog.ShowAsync()" and "return 1" to show the dialog? I ran into this problem during my first attempts with my code until i used the "DispatcherQueueExtensions" class I used in my answer. – Martin Apr 27 '22 at 15:47
  • @Martin: Yes, it does. `1` isn't return until the dialog has been closed. Why are you using a `TaskCompletionSource`? – mm8 Apr 28 '22 at 19:24