Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Call async method on UI thread

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?

like image 490
Jan Zahradník Avatar asked Nov 27 '18 21:11

Jan Zahradník


People also ask

Can async method run on the UI thread?

@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.

Can an async method run on the UI thread of a Windows Forms app?

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.

Does async run on main thread?

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.

What happens when you call async method?

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.


1 Answers

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();
        }
    }
}
like image 52
noseratio Avatar answered Sep 29 '22 17:09

noseratio