【问题标题】:Call async method on UI thread在 UI 线程上调用异步方法
【发布时间】:2019-04-29 16:40:24
【问题描述】:

我正在尝试使用 IdentityServer 身份验证创建 WPF 客户端。我正在使用他们的OidcClient 登录。这是整个异步的,而我的应用程序是同步的,如果不付出巨大的努力就无法重构。调用

var result = await _oidcClient.LoginAsync();

不等待结果。调用Wait().Result 会导致死锁。将其包装到其他 Task.Run 抱怨该方法未在 UI 线程上运行(它打开带有登录对话框的浏览器)。

你有什么想法,如何解决这个问题?我需要写自定义同步OidcClient吗?

【问题讨论】:

  • 如果您的设计需要长时间同步阻塞 UI 线程,那么您需要修复您的设计。这是您获得工作代码的唯一方法,而且您拖延的时间越长,实际修复的难度就越大。
  • 您需要使用控件上的 Invoke 方法将 Task.Run 中的 UI 调用编组回 UI 线程。 docs.microsoft.com/en-us/dotnet/api/…
  • @pm100 如果您要使用 TPL,则没有理由明确使用 Invoke(如果您使用的是旧版本的 .NET,则在极少数情况下应该使用它),它也没有做任何事情来解决这个问题。它们已经在 UI 线程上,并且需要在 UI 线程上调用该方法,因此它们既不能在另一个线程上运行它,也不需要做任何事情来将其编组到 UI 线程。
  • 这可能是XY problem。准确地展示你想要做什么。您应该能够使用异步事件处理程序,但由于缺乏细节我不确定..
  • @pm100 他们调用的方法是与 UI 交互的异步方法,因此需要在 UI 线程上运行。在非 UI 线程中运行它是不正确的。如果你这样做,它永远不会起作用。它需要在 UI 线程中运行。它不会长时间阻塞 UI 线程,因为它是异步的(至少名称说明了很多,到目前为止没有任何关于它的说明表明它没有正确异步运行,相反,该方法之前返回操作完成,正如问题中所说,我们如何知道它异步的)。

标签: c# async-await .net-4.7


【解决方案1】:

与其他类似情况一样,您需要在不进行大量重构的情况下将异步引入旧版应用程序,我建议使用简单的“请稍候...”模式对话框。该对话框启动异步操作并在操作完成后自行关闭。

Window.ShowDialog 是一个同步 API,它阻塞主 UI 并且仅在模式对话框关闭时返回给调用者。但是,它仍然运行嵌套的消息循环并泵送消息。因此,与使用容易死锁的Task.Wait() 相比,异步任务继续回调仍然会被抽出并执行。

这是一个基本但完整的 WPF 示例,用 Task.Delay() 模拟 _oidcClient.LoginAsync() 并在 UI 线程上执行它,请参阅 WpfTaskExt.Execute 了解详细信息。

取消支持是可选的;如果无法取消实际的LoginAsync,则防止对话框过早关闭。

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

【讨论】:

  • 谢谢!这种方法允许我进行本地更改,而不是系统范围!
猜你喜欢
  • 1970-01-01
  • 2010-12-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-08-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多