Home > Software engineering >  WinUI3: Show ContentDialog from background thread. CoreDispatcher.RunAsync not available?
WinUI3: Show ContentDialog from background thread. CoreDispatcher.RunAsync not available?

Time:04-28

I am trying to show a WinUI dialog - i.e. a class that derives from 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?

CodePudding user response:

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...
});

CodePudding user response:

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;
}
  • Related