【问题标题】:How can I implement a 1s delay before my progress/cancel dialog is shown?如何在显示进度/取消对话框之前实现 1 秒延迟?
【发布时间】:2012-09-12 18:25:38
【问题描述】:

我正在尝试构建一个进度/取消表单以在我的 WinForms 应用程序中使用,该应用程序运行任何可等待的“操作”,同时为用户提供一些进度信息和取消操作的机会。

因为表单是使用ShowDialog() 显示的,所以它是一个模态表单,可以很好地禁用下面的表单 - 所以我不需要乱用禁用其他表单上的所有控件。

他们实现它的方式,我完全希望你撕成碎片:-),是在Form.Load 事件处理程序期间等待操作的结果,然后在操作完成后关闭表单完成(无论是因为它运行完成、被取消还是引发异常)。

public partial class ProgressForm<T> : Form
{
    private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
    private Progress<string> _progress = new Progress<string>();
    private Func<IProgress<string>, CancellationToken, Task<T>> _operation = null;
    private Exception _exception = null;
    private T _result = default(T);


    public static T Execute(Func<IProgress<string>, CancellationToken, Task<T>> operation)
    {
        using (var progressForm = new ProgressForm<T>())
        {
            progressForm._operation = operation;

            progressForm.ShowDialog();

            if (progressForm._exception != null)
                throw progressForm._exception;
            else
                return progressForm._result;
        }
    }


    public ProgressForm()
    {
        InitializeComponent();

        this._progress.ProgressChanged += ((o, i) => this.ProgressLabel.Text = i.ToString());
    }

    private async void ProgressForm_Load(object sender, EventArgs e)
    {
        try
        {
            this._result = await this._operation(this._progress, this._cancellationTokenSource.Token);
        }
        catch (Exception ex) // Includes OperationCancelledException
        {
            this._exception = ex;
        }

        this.Close();
    }

    private void CancelXButton_Click(object sender, EventArgs e)
    {
        if (this._cancellationTokenSource != null)
            this._cancellationTokenSource.Cancel();
    }
}

这是这样称呼的:

int numberOfWidgets = ProgressForm<int>.Execute(CountWidgets);

...CountWidgets() 是一个可等待的东西(在这种情况下,一个函数返回 Task&lt;int&gt;,带有适当的 IProgress 和 CancellationToken 参数)。

到目前为止,它运行良好,但我想添加一个“功能”。理想情况下,我希望表单在(比如说)一秒钟内保持不可见,这样如果操作完成得非常快,就不会在表单显示时出现“闪烁”,然后立即再次隐藏。

所以,我的问题是如何在表格显示之前引入 1s 延迟。显然,我仍然想立即开始操作,但是一旦我“等待”操作的结果,我就不再控制(可以这么说),因为控制权将返回给@987654328 的调用者@事件处理程序 - 它将继续显示表单的工作。

我怀疑本质上我真的需要第二个线程,并且我需要在阻塞主 UI 线程时在该线程上执行操作。 (我知道阻塞 UI 线程是不受欢迎的,但在这种情况下,我认为这实际上是我需要的)。

创建线程等的方法有很多,我不知道在新的“异步/等待”世界中如何做到这一点......

【问题讨论】:

  • 目前还不清楚它是如何工作的 - 我们没有足够的上下文来说明您显示的代码在哪里适合您的其余代码。
  • @JonSkeet:已添加表单的完整代码。
  • 那么,你想要 1 秒的延迟,如果整个过程需要 1.5 秒呢?同样的问题仍然存在。你需要考虑其他事情。
  • 如果执行时间太短,我会在进程完成后关闭对话框之前添加此延迟。
  • 我们通过使用单例来做到这一点。因此,呼叫显示进度表集是一个计时器。停止显示进度会杀死表单或计时器。

标签: c# winforms multithreading async-await c#-5.0


【解决方案1】:

我认为您必须将“任务运行器”与“对话框”分开才能做到这一点。首先,一个响应进度并可以发出取消的对话框:

public partial class ProgressForm : Form
{
  private readonly CancellationTokenSource _cancellationTokenSource;

  public ProgressForm(CancellationTokenSource cancellationTokenSource, IProgress<string> progress)
  {
    InitializeComponent();

    _cancellationTokenSource = cancellationTokenSource;
    progress.ProgressChanged += ((o, i) => this.ProgressLabel.Text = i.ToString());
  }

  public static void ShowDialog(CancellationTokenSource cancellationTokenSource, IProgress<string> progress)
  {
    using (var progressForm = new ProgressForm(cancellationTokenSource, progress))
    {
        progressForm.ShowDialog();
    }
  }

  private void CancelXButton_Click(object sender, EventArgs e)
  {
    if (this._cancellationTokenSource != null)
      this._cancellationTokenSource.Cancel();
  }
}

接下来,真正的“任务运行者”:

public static class FriendlyTaskRunner
{
  public static async Task<T> Execute<T>(Func<CancellationToken, IProgress<string>, Task<T>> operation)
  {
    var cancellationTokenSource = new CancellationTokenSource();
    var progress = new Progress<string>();
    var timeout = Task.Delay(1000);
    var operationTask = operation(cancellationTokenSource.Token, progress);

    // Synchronously block for either the operation to complete or a timeout;
    //  if the operation completes first, just return the result.
    var completedTask = Task.WhenAny(timeout, operationTask).Result;
    if (completedTask == operationTask)
      return await operationTask;

    // Kick off a progress form and have it close when the task completes.
    using (var progressForm = new ProgressForm(cancellationTokenSource, progress))
    {
      operationTask.ContinueWith(_ => { progressForm.Close(); });
      progressForm.ShowDialog();
    }

    return await operationTask;
  }
}

请注意synchronously blocking the UI thread may cause deadlocks - 在这种情况下,如果operation 尝试同步回 UI 线程,它将被阻塞直到超时之后 - 所以这不是真正的“死锁”,而是效率很低。

【讨论】:

  • 谢谢,太棒了 :-) 它确实遇到了与 Hans Passant 的回答(现已删除)相同的问题,因为 ShowDialog 没有被立即调用,所以我们没有得到附带好处在操作期间禁用基础表单。但是,还有其他方法可以解决这个问题。
  • 一个问题:如果在await Task.WhenAny() 调用过程中 operationTask 引发了异常,那么该Execute() 方法的调用者会返回什么样的异常呢?我需要开始担心 AggregateExceptions 吗?
  • @Gary:我修改了代码,使其(同步)阻塞一秒钟,然后调用 ShowDialog 而不让步。就个人而言,我更喜欢禁用调用表单,使用常规 Show 显示对话框,然后注册一个继续以关闭表单。但这更容易。
  • @Gary:代码干净利落地处理异常。仅使用一次Task.Result,并且用于Task.WhenAny 返回值,即always completes in a successful state。顺便说一句,如果operation 抛出异常,您的原始代码将丢失堆栈跟踪。
  • 对于任何尝试使用此代码的人,您可能会注意到一个错误:接近末尾的 ContinueWith 部分将在随机 ThreadPool 线程上执行 progressForm.Close() 调用,然后会引发一个InvalidOperationException 因为它只能在 UI 线程上执行。我可以通过使用TaskScheduler.FromCurrentSynchronizationContext() 作为ContinueWith 的第二个参数来解决这个问题。
【解决方案2】:

如果您想延迟显示表单,我不建议您保留部分内容。我将独立调用该操作,然后创建一个TimerTick 事件处理程序检查任务是否完成,如果完成,则不执行任何操作。否则它应该创建表单,将IProgress&lt;T&gt;CancellationTokenSource 和任务传递到表单中。您仍然可以等待已经开始的任务。对于要启动的任务,在创建表单之前需要进度对象和取消令牌——因此需要独立创建...

【讨论】:

  • 谢谢。您是第 N 个建议我在延迟期过后才出示表格的人。你(全部)可能是对的。我被吸引到立即显示(或者更确切地说,创建)表单的原因是它具有在操作期间禁用下面的表单的附带好处,这很有用。但是,正如我在其他地方所说,还有其他方法可以实现相同的目标。
  • 您可以轻松禁用“父”表单。我建议仅禁用一部分控件(例如用于生成此操作的控件)。但是,您可以简单地使用 Form.Enabled 禁用它,然后在 Tick 处理程序或子表单中重新启用它(如果显示)。
猜你喜欢
  • 1970-01-01
  • 2011-05-11
  • 2011-07-29
  • 2012-04-25
  • 1970-01-01
  • 1970-01-01
  • 2011-07-09
  • 2020-06-13
  • 1970-01-01
相关资源
最近更新 更多