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