Call async method on UI thread
Asked Answered
M

1

15

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?

Millymilman answered 27/11, 2018 at 21:8 Comment(8)
If your design requires synchronously blocking the UI thread for extended periods of time then you need to fix your design. That's the only way you're going to get working code, and the longer you put it off, the harder it'll be to actually fix.Could
you need to marshal the UI call in Task.Run back to the UI thread using the Invoke method on the control. learn.microsoft.com/en-us/dotnet/api/…Waterresistant
@Waterresistant There's no reason to ever explicitly use Invoke if you're going to use the TPL (and very few situations where you should be using it if you're on older versions of .NET), nor does it do anything to resolve this problem. They're already on the UI thread, and that method needs to be called on the UI thread, so they can neither run it on another thread, nor do they need to do anything to marshal it to the UI thread.Could
This may be an XY problem. Show exactly what you are trying to do. You should be able to use async event handlers but I am uncertain due to lack of details..Fancy
@Could Towards the end OP says "Wrapping it to other Task.Run is complaining that the method is not running on UI thread (it opens browser with login dialog)." - Task.Run will run the code in non UI thread (correctly) but will fail to paint the UI because its not on the UI threadWaterresistant
@Waterresistant 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. It won't block the UI thread for an extended period of time because its asynchronous (at least the name says as much, and nothing said about it thus far indicates it's not properly running asynchronously, to the contrary, that the method is returning before the operation is done, as is said in the question, is how we know it is asynchronous).Could
@Could I know the design is poor, but it dates to 2006, since than only subtle changes were made. During that time the app became huge. I am now trying to separate some pieces and first of them is authentication. The call above should open browser with login dialog. It can't be called from async event as it would require rewriting all code for all layers.Lorenz
@JanZahradník Either your big application is constantly blocking the UI thread all over the place, and the application is functionally unusable due to how often its freezing, or the design isn't actually expecting everything to be entirely synchronous. If it's the former, not much to do besides fix it, if it's the latter, then you're simply not using the application properly. Perhaps the design does account for some means of performing asynchronous operations, and you're just not using it.Could
C
9

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();
        }
    }
}
Cage answered 28/11, 2018 at 0:33 Comment(3)
Thank you! This method allows me to make a local change, not a system wide!Lorenz
Wouldn't it be easier to just rewrite the original code of the question to: var task = _oidcClient.LoginAsync(); task.GetAwaiter().GetResult(); The dialog maybe nice but maybe I don't need it.Gretta
@TheincredibleJan that will most likely result in a deadlock, which was the point of the original question. Just a few days ago: twitter.com/wuutguy/status/1575076329287864320Cage

© 2022 - 2024 — McMap. All rights reserved.