I'm trying to create WPF client with IdentityServer authentication. I'm using their OidcClient
to get logged in. It's whole async while my app is sync and can't be refactored without huge effort. Calling
var result = await _oidcClient.LoginAsync();
doesn't wait for the result. Calling Wait()
or .Result
causes deadlock. Wrapping it to other Task.Run
is complaining that the method is not running on UI thread (it opens browser with login dialog).
Do you have any idea, how to solve this? Do I need to write custom sync OidcClient
?
@pm100 The method they're calling is an asyncrhonous method that interacts with the UI, and as such needs to be run on the UI thread. It's incorrect to run it in a non-UI thread. It will never work if you do that. It needs to be run in the UI thread.
You can start an async operation from the UI thread, await it without blocking the UI thread, and naturally resume on the UI thread when it's done.
You create your new async Task from your UI code on the main thread — and now this happens! Remember, you learned that every use of await is a suspension point, and your code might resume on a different thread. The first piece of your code runs on the main thread because the task initially runs on the main actor.
The call to the async method starts an asynchronous task. However, because no Await operator is applied, the program continues without waiting for the task to complete. In most cases, that behavior isn't expected.
As with other similar cases where you need to introduce asynchrony to a legacy app without much refactoring, I'd recommend using a simple "Please wait..." modal dialog. The dialog initiates an async operation and closes itself when the operation has finished.
Window.ShowDialog
is a synchronous API in the way it blocks the main UI and only returns to the caller when the modal dialog has been closed. However, it still runs a nested message loop and pumps messages. Thus, the asynchronous task continuation callbacks still get pumped and executed, as opposed to using a deadlock-prone Task.Wait()
.
Here is a basic but complete WPF example, mocking up _oidcClient.LoginAsync()
with Task.Delay()
and executing it on the UI thread, refer to WpfTaskExt.Execute
for the details.
Cancellation support is optional; if the actual LoginAsync
can't be cancelled, the dialog is prevented from being closed prematurely.
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
namespace WpfApp1
{
public partial class MainWindow : Window
{
public MainWindow()
{
var button = new Button() { Content = "Login", Width = 100, Height = 20 };
button.Click += HandleLogin;
this.Content = button;
}
// simulate _oidcClient.LoginAsync
static async Task<bool> LoginAsync(CancellationToken token)
{
await Task.Delay(5000, token);
return true;
}
void HandleLogin(object sender, RoutedEventArgs e)
{
try
{
var result = WpfTaskExt.Execute(
taskFunc: token => LoginAsync(token),
createDialog: () =>
new Window
{
Owner = this,
Width = 320,
Height = 200,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
Content = new TextBox
{
Text = "Loggin in, please wait... ",
HorizontalContentAlignment = HorizontalAlignment.Center,
VerticalContentAlignment = VerticalAlignment.Center
},
WindowStyle = WindowStyle.ToolWindow
},
token: CancellationToken.None);
MessageBox.Show($"Success: {result}");
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
}
public static class WpfTaskExt
{
/// <summary>
/// Execute an async func synchronously on a UI thread,
/// on a modal dialog's nested message loop
/// </summary>
public static TResult Execute<TResult>(
Func<CancellationToken, Task<TResult>> taskFunc,
Func<Window> createDialog,
CancellationToken token = default(CancellationToken))
{
var cts = CancellationTokenSource.CreateLinkedTokenSource(token);
var dialog = createDialog();
var canClose = false;
Task<TResult> task = null;
async Task<TResult> taskRunner()
{
try
{
return await taskFunc(cts.Token);
}
finally
{
canClose = true;
if (dialog.IsLoaded)
{
dialog.Close();
}
}
}
dialog.Closing += (_, args) =>
{
if (!canClose)
{
args.Cancel = true; // must stay open for now
cts.Cancel();
}
};
dialog.Loaded += (_, __) =>
{
task = taskRunner();
};
dialog.ShowDialog();
return task.GetAwaiter().GetResult();
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With