我正在使用 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();
}
这是正确的方法还是有更好的方法?
感谢您的帮助。
使用
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());
}
}
}
这是一个使用 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());
}
ShowDialog()
是阻塞调用;在用户关闭对话框之前,执行不会前进到 await
语句。请改用 Show()
。不幸的是,您的对话框不会是模态的,但它会正确跟踪异步操作的进度。
按照 @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 以及完整的示例。
始终致电
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());
}
}
您可以尝试以下方法:
protected async void LoadData()
{
ProgressForm _progress = new ProgressForm();
var loadDataTask = GetData();
loadDataTask.ContinueWith(a =>
this.Invoke((MethodInvoker)delegate
{
_progress.Close();
}));
_progress.ShowDialog();
}