异步显示对话框

问题描述 投票:0回答:6

我正在使用 async/await 从数据库异步加载数据,在加载过程中,我想弹出一个加载表单,它只是一个简单的表单,带有运行进度条以指示有一个正在运行的进程。加载数据后,该对话框将自动关闭。我怎样才能做到这一点?以下是我当前的代码:

 protected async void LoadData() 
    {
       ProgressForm _progress = new ProgressForm();  
       _progress.ShowDialog()  // not working
       var data = await GetData();          
       _progress.Close();
    }

更新:

我设法通过更改代码让它工作:

 protected async void LoadData() 
        {
           ProgressForm _progress = new ProgressForm();  
           _progress.BeginInvoke(new System.Action(()=>_progress.ShowDialog()));
           var data = await GetData();          
           _progress.Close();
        }

这是正确的方法还是有更好的方法?

感谢您的帮助。

c# asynchronous async-await
6个回答
41
投票

使用

Task.Yield
很容易实现,如下所示(WinForms,为了简单起见,没有异常处理)。重要的是要了解执行流程如何跳转到此处的新嵌套消息循环(模式对话框的消息循环),然后返回到原始消息循环(这就是
await progressFormTask
的用途):

namespace WinFormsApp
{
  internal static class DialogExt
  {
    public static async Task<DialogResult> ShowDialogAsync(this Form @this)
    {
      await Task.Yield();
      if (@this.IsDisposed)
        return DialogResult.Cancel;
      return @this.ShowDialog();
    }
  }

  public partial class MainForm : Form
  {
    public MainForm()
    {
      InitializeComponent();
    }

    async Task<int> LoadDataAsync()
    {
      await Task.Delay(2000);
      return 42;
    }

    private async void button1_Click(object sender, EventArgs e)
    {
      var progressForm = new Form() { 
        Width = 300, Height = 100, Text = "Please wait... " };

      object data;
      var progressFormTask = progressForm.ShowDialogAsync();
      try 
      {
        data = await LoadDataAsync();
      }
      finally 
      {
        progressForm.Close();
        await progressFormTask;
      }

      // we got the data and the progress dialog is closed here
      MessageBox.Show(data.ToString());
    }
  }
}

4
投票

这是一个使用 Task.ContinueWith 的模式,并且应该避免使用模态 ProgressForm 时出现任何竞争条件:

protected async void LoadDataAsync()
{
    var progressForm = new ProgressForm();

    // 'await' long-running method by wrapping inside Task.Run
    await Task.Run(new Action(() =>
    {
        // Display dialog modally
        // Use BeginInvoke here to avoid blocking
        //   and illegal cross threading exception
        this.BeginInvoke(new Action(() =>
        {   
            progressForm.ShowDialog();
        }));
        
        // Begin long-running method here
        LoadData();
    })).ContinueWith(new Action<Task>(task => 
    {
        // Close modal dialog
        // No need to use BeginInvoke here
        //   because ContinueWith was called with TaskScheduler.FromCurrentSynchronizationContext()
        progressForm.Close();
    }), TaskScheduler.FromCurrentSynchronizationContext());
}

2
投票

ShowDialog()
是阻塞调用;在用户关闭对话框之前,执行不会前进到
await
语句。请改用
Show()
。不幸的是,您的对话框不会是模态的,但它会正确跟踪异步操作的进度。


0
投票

按照 @noseratio 的答案实现

ShowDialogAsync()
对于许多场景来说都很好。

仍然,耗时的计算后台工作的一个缺点是取消对话框(即

form.Close()
)只会在后台工作完成或取消之前立即关闭对话框。 在我的场景中,这导致主应用程序在几秒钟内没有响应,因为取消后台工作需要一段时间。

为了解决这个问题,必须延迟/拒绝对话取消,直到工作真正完成/取消为止(即达到

finally
)。另一方面,关闭尝试应该通知观察者所需的取消,这可以通过
CancellationToken
来实现。然后,该令牌可用于通过
ThrowIfCancellationRequested()
取消工作,抛出一个
OperationCanceledException
,这可以在专用的
catch
语句中处理。

有几种方法可以实现这一点,但我更喜欢

using(Disposable)
而不是大致相当的
try
/
finally

public static class AsyncFormExtensions
{
    /// <summary>
    /// Asynchronously shows the form as non-blocking dialog box
    /// </summary>
    /// <param name="form">Form</param>
    /// <returns>One of the DialogResult values</returns>
    public static async Task<DialogResult> ShowDialogAsync(this Form form)
    {
        // ensure being asynchronous (important!)
        await Task.Yield();

        if (form.IsDisposed)
        {
            return DialogResult.Cancel;
        }

        return form.ShowDialog();
    }

    /// <summary>
    /// Show a non-blocking dialog box with cancellation support while other work is done.
    /// </summary>
    /// <param name="form">Form</param>
    /// <returns>Non-blocking disposable dialog</returns>
    public static DisposableDialog DisposableDialog(this Form form)
    {
        return new DisposableDialog(form);
    }
}

/// <summary>
/// Non-blocking disposable dialog box with cancellation support
/// </summary>
public class DisposableDialog : IAsyncDisposable
{
    private Form _form;
    private FormClosingEventHandler _closingHandler;
    private CancellationTokenSource _cancellationTokenSource;

    /// <summary>
    /// Propagates notification that dialog cancelling was requested
    /// </summary>
    public CancellationToken CancellationToken => _cancellationTokenSource.Token;

    /// <summary>
    /// Awaitable result of ShowDialogAsync
    /// </summary>
    protected Task<DialogResult> ResultAsync { get; }

    /// <summary>
    /// Indicates the return value of the dialog box
    /// </summary>
    public DialogResult Result { get; set; } = DialogResult.None;

    /// <summary>
    /// Show a non-blocking dialog box with cancellation support while other work is done.
    /// 
    /// Form.ShowDialogAsync() is used to yield a non-blocking async task for the dialog.
    /// Closing the form directly with Form.Close() is prevented (by cancelling the event).
    /// Instead, a closing attempt will notify the CancellationToken about the desired cancellation.
    /// By utilizing the token to throw an OperationCanceledException, the work can be terminated.
    /// This then causes the desired (delayed) closing of the dialog through disposing.
    /// </summary>
    public DisposableDialog(Form form)
    {
        _form = form;
        _cancellationTokenSource = new CancellationTokenSource();

        _closingHandler = new FormClosingEventHandler((object sender, FormClosingEventArgs e) => {
            // prevent closing the form
            e.Cancel = true;

            // Store the desired result as the form withdraws it because of "e.Cancel=true"
            Result = form.DialogResult;

            // notify about the cancel request
            _cancellationTokenSource.Cancel();
        });

        form.FormClosing += _closingHandler;

        ResultAsync = _form.ShowDialogAsync();
    }

    /// <summary>
    /// Disposes/closes the dialog box
    /// </summary>
    /// <returns>Awaitable task</returns>
    public async ValueTask DisposeAsync()
    {
        if (Result == DialogResult.None)
        {
            // default result on sucessful completion (would become DialogResult.Cancel otherwise)
            Result = DialogResult.OK;
        }

        // Restore the dialog result as set in the closing attempt
        _form.DialogResult = Result;

        _form.FormClosing -= _closingHandler;
        _form.Close();

        await ResultAsync;
    }
}

使用示例:

private async Task<int> LoadDataAsync(CancellationToken cancellationToken)
{
    for (int i = 0; i < 10; i++)
    {
        // do some work
        await Task.Delay(500);
        // if required, this will raise OperationCanceledException to quit the dialog after each work step
        cancellationToken.ThrowIfCancellationRequested();
    }
    return 42;
}

private async void ExampleEventHandler(object sender, EventArgs e)
{
    var progressForm = new Form();

    var dialog = progressForm.DisposableDialog();
    // show the dialog asynchronously while another task is performed
    await using (dialog)
    {
        try
        {
            // do some work, the token must be used to cancel the dialog by throwing OperationCanceledException
            var data = await LoadDataAsync(dialog.CancellationToken);
        }
        catch (OperationCanceledException ex)
        {
            // Cancelled
        }
    }
}

我已经为代码创建了一个 Github Gist 以及完整的示例。


-1
投票

始终致电

ShowDialog()
LoadDataAsync
Close()
,以避免
IsDisposed
,如 @noseratio 的回答。所以使用
Task.Yield()
来延迟
LoadDataAsync()
而不是
ShowDialog()

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }
    async Task<int> LoadDataAsync()
    {
        Console.WriteLine("Load");
        await Task.Delay(2000);
        return 42;
    }

    private async void button1_Click(object sender, EventArgs e)
    {
        var progressForm = new Form()
        {
            Width = 300,
            Height = 100,
            Text = "Please wait... "
        };

        async Task<int> work()
        {
            try
            {
                await Task.Yield();
                return await LoadDataAsync();
            }
            finally
            {
                Console.WriteLine("Close");
                progressForm.Close();
            }
        }
        var task = work();
        Console.WriteLine("ShowDialog");
        progressForm.ShowDialog();
        object data = await task;

        // we got the data and the progress dialog is closed here
        Console.WriteLine("MessageBox");
        MessageBox.Show(data.ToString());
    }
}

-2
投票

您可以尝试以下方法:

protected async void LoadData()
{
    ProgressForm _progress = new ProgressForm();
    var loadDataTask = GetData();
    loadDataTask.ContinueWith(a =>
        this.Invoke((MethodInvoker)delegate
        {
            _progress.Close();
        }));
    _progress.ShowDialog();
}
© www.soinside.com 2019 - 2024. All rights reserved.