【问题标题】:How to close modal window after a Task completes?任务完成后如何关闭模式窗口?
【发布时间】:2018-06-29 13:18:14
【问题描述】:

我有两个默认窗口。 我想要一个窗口开始一项工作,以模态(对话框)形式显示另一个窗口(进度指示,但现在不重要),然后在这项工作完成后将其关闭。我有我的实现中存在以下问题:

1) 工作完成后(出现“Completed!”消息框,但也不重要,只是提示),ProgressWindow 不会自动关闭;

2) 如果我通过手动单击红叉将其关闭,则会出现 System.InvalidOperationException 并显示消息“调用线程无法访问此对象,因为不同的线程拥有它。”发生在行

await task;

3) ContinueWith 中的代码实际上是在 Go 方法完成之前执行的 - 为什么?

我怎样才能实现这样的行为?

我的实现:

namespace WpfApp1
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        async void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            Window w = new ProgressWindow();

            var task = 
                Task
                .Run(() => Go())
                .ContinueWith(completedTask => w.Close());
            w.ShowDialog();
            await task; // InvalidOperationException throws
        }

        async protected void Go()
        {
            await Task.Delay(500); // imitate some work    
            MessageBox.Show("Completed!"); // indicate that work has been completed
        }
    }
}

【问题讨论】:

  • ShowDialog 不会阻止吗?如果用户不关闭对话框,您怎么会知道任务已完成?我认为您需要将任务作为参数传递给对话框,并将await 传递给某个地方。任务完成后,窗口可以自行关闭。
  • 我不明白“将任务传递给对话框”是什么意思。在对话框的代码后面的某处执行任务?.. 但是如果我向后做 - 将对话框传递给任务 - 我可以实现所需的行为。
  • 实际上,我认为您的方法(尽管您认为它不起作用)很好。我可能应该对 Peter Bons 的帖子发表评论。我在你的代码和他的代码之间来回切换。
  • 我还提出了我自己的解决方案,以实现所需的行为。
  • @KennethK。我的方法也不好,因为 ContinueWith 中的代码实际上是在 Go 完成之前执行的。这很奇怪,我不知道为什么。

标签: c# wpf modal-dialog async-await task


【解决方案1】:

这是一个扩展方法ShowDialogUntilTaskCompletion,可以用来代替内置的ShowDialog。当提供的Task 完成时,它会自动关闭窗口。如果任务完成异常,窗口仍然关闭,方法返回而不抛出。

如果窗口通过其他方式关闭,例如用户单击窗口的关闭按钮或按 Alt+F4,此方法也会返回。所以不能保证方法返回时任务会完成。

public static class WindowExtensions
{
    public static bool? ShowDialogUntilTaskCompletion(this Window window,
        Task task, int minDurationMsec = 500)
    {
        if (window == null) throw new ArgumentNullException(nameof(window));
        if (task == null) throw new ArgumentNullException(nameof(task));
        if (minDurationMsec < 0)
            throw new ArgumentOutOfRangeException(nameof(minDurationMsec));

        var closeDelay = Task.Delay(minDurationMsec);
        HandleTaskCompletion();
        return window.ShowDialog();

        async void HandleTaskCompletion()
        {
            try
            {
                await Task.Yield(); // Ensure that the completion is asynchronous
                await task;
            }
            catch { } // Ignore exception
            finally
            {
                try
                {
                    await closeDelay;
                    window.Close();
                }
                catch { } // Ignore exception
            }
        }
    }

}

作为奖励,它还接受minDurationMsec 参数,以在指定的最短持续时间内保持窗口可见(这样窗口不会在眨眼间关闭)。

使用示例:

async void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    Window w = new ProgressWindow();
    var task = Task.Delay(2000); // Simulate some asynchronous work
    w.ShowDialogUntilTaskCompletion(task);
    try
    {
        // Most likely the task will be completed at this point
        await task;
    }
    catch (Exception ex)
    {
        // Handle the case of a faulted task
    }
}

【讨论】:

    【解决方案2】:

    async (p) => 
          { 
          await Task.Run(() => { 
          
          if (p == null) return; 
          //code
          }).ConfigureAwait(true); 
          p.Close(); 
          });

    【讨论】:

    • 请简要说明此代码如何帮助解决问题中的问题。
    【解决方案3】:

    我阅读了 Peter Bons 的答案并重新考虑它 - 他的方法使用异步 Go 方法(当然是在我的问题中),然后在没有 Task.Run 的情况下等待其结果。我得出的另一个变体是基于 Peter Bons 和 Andrew Shkolik 的回答。我通过Task.Run异步调用一个同步方法Go,并使用Dispatcher来操作窗口。

    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
    
        async void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            Window w = new ProgressWindow();
            Task work = Task.Run(() => Go(w));
            w.ShowDialog();
            await work;
        }
    
        void Go(Window w)
        {
            Thread.Sleep(2000); // imitate some work
            Dispatcher.BeginInvoke(
                new Action(() =>
                {
                    w.Close();
                }));
        }
    }
    

    【讨论】:

      【解决方案4】:

      这里不需要使用延续,只要坚持await。不仅如此,还因为你使用了async void,程序在关闭窗口之前没有等待半秒。

      此外,在这种情况下使用 Task.Run 确实没有任何好处,因为 Go 已经可以等待。

      改进和简化的代码是:

      public partial class MainWindow : Window
      {
          public MainWindow()
          {
              InitializeComponent();
          }
      
          async void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
          {
              Window w = new ProgressWindow();
              Task work = Go(w);
              w.ShowDialog();
              await work; // exceptions in unawaited task are difficult to handle, so let us await it here.
          }
      
          async Task Go(Window w)
          {
              await Task.Delay(500);    
              w.Close();
          }
      }
      

      您收到错误的原因是Task.Run 创建的任务(的延续)在非 UI 线程上执行,并且不允许在非 UI 线程中访问 UI (w.Close();) .

      如果您的工作受益于 Task.Run,​​您可以像这样修改 Go() 方法:

      async Task Go(Window w)
      {
          await Task.Run(() => { 
              // ... heavy work here  
          });    
          w.Close();
      }
      

      【讨论】:

      • 在调用 w.ShowDialog() 之后的第一个示例中,在我关闭 ProgressWindow 窗口之前没有发生任何事情。只有在执行 Go 之后。
      • 您的第二个示例具有相同的行为。
      • 我明白了,我忽略了窗口作为对话框打开的事实,因此阻止了任务的开始。是否可以将其作为非对话框窗口打开?查看我的更新答案
      • 不,对话框表单是必需的,因为我想在操作完成之前阻止用户进行任何活动。不过还是谢谢你的帮助。
      • 没错,你看,如果你使用 Task.Run 你会创建一个后台线程。但是假设您正在执行 I/O 绑定操作,例如使用本机异步操作访问文件或网络,实际上不需要使用 Task.Run。见this blogpost serie。例如,使用 await Task.Delay(xxx) 不会阻塞 UI 线程,因此不需要 Task.Run。
      【解决方案5】:

      您试图从后台线程关闭窗口... 如果你仍然想使用 Task.Run().ContinueWith() 那么你应该使用 Dispatcher 来关闭窗口。但最好使用 async\await 语法。

          async void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
          {
              Window w = new ProgressWindow();
      
              var task = Task.Run(() => Go()).ContinueWith(completedTask =>
                  {
                      Application.Current.Dispatcher.BeginInvoke(
                      DispatcherPriority.Send, new Action(() =>
                      {
                          w.Close(); 
                      }));
      
                  });
              w.ShowDialog();
              await task;
          }
      

      【讨论】:

      • 根据您的代码,ProgressWindow 在单击主窗口后会立即关闭。通常,ContinueWith 中的代码在调用 Task.Run 后立即执行,无需等待 Go 完成。但是,如果我将 Task.Delay(2000) 更改为 Thread.Sleep(2000),那么我将实现所需的行为。为什么 ContinueWith 立即执行而不等待 Go?
      • 因为Go 不可等待(它不返回任务)
      猜你喜欢
      • 1970-01-01
      • 2016-10-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-11-05
      • 1970-01-01
      • 1970-01-01
      • 2022-06-30
      相关资源
      最近更新 更多