C# 显示IProgress并支持取消WinForms异步任务的模式进度表
我一直在尝试使用一个可重用的模式进度窗口(即progressForm.ShowDialog())来显示正在运行的异步任务的进度,包括启用取消功能。C# 显示IProgress并支持取消WinForms异步任务的模式进度表,c#,.net,winforms,task-parallel-library,async-await,C#,.net,Winforms,Task Parallel Library,Async Await,我一直在尝试使用一个可重用的模式进度窗口(即progressForm.ShowDialog())来显示正在运行的异步任务的进度,包括启用取消功能。 我看到一些实现通过在表单上挂接激活的事件处理程序来启动异步任务,但我需要先启动任务,然后显示显示进度的模式对话框,然后在完成或取消完成时关闭模式对话框(注意-我希望在取消完成时关闭表单-任务继续时发出关闭信号) 我目前有以下几点——尽管这项工作正在进行——这是否存在问题——或者这是否可以以更好的方式完成 我确实读到我需要在不进行调试的情况下运行这个C
我看到一些实现通过在表单上挂接激活的事件处理程序来启动异步任务,但我需要先启动任务,然后显示显示进度的模式对话框,然后在完成或取消完成时关闭模式对话框(注意-我希望在取消完成时关闭表单-任务继续时发出关闭信号) 我目前有以下几点——尽管这项工作正在进行——这是否存在问题——或者这是否可以以更好的方式完成 我确实读到我需要在不进行调试的情况下运行这个CTRL-F5(以避免AggregateException在继续中停止调试器,并让它像在生产代码中一样在try-catch中被捕获) ProgressForm.cs -带有ProgressBar(progressBar1)和按钮(btnCancel)的表单 服务中心 -包含WinForms应用程序(以及控制台应用程序)使用的逻辑的类文件
我认为我遇到的最大问题是使用wait(而不是 ContinueWith)表示无法使用ShowDialog,因为两者都被阻塞 调用。如果我先调用ShowDialog,代码在该点被阻止, 并且进度表单需要实际启动异步方法(该方法 这是我想要避免的)。如果我打电话,请等待 MyService.DoSomethingWithResultAsync首先,然后这个块和我 无法显示我的进度表
ShowDialog
实际上是一个阻塞API,在关闭对话框之前它不会返回。但是它是非阻塞API,在这个意义上它会继续泵送消息,尽管是在一个新的嵌套消息循环上。我们可以将此行为与async/wait
和TaskCompletionSource
结合使用:
private async void btnDo_Click(object sender, EventArgs e)
{
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken token = cts.Token;
// Create the ProgressForm, and hook up the cancellation to it.
ProgressForm progressForm = new ProgressForm();
progressForm.Cancelled += () => cts.Cancel();
var dialogReadyTcs = new TaskCompletionSource<object>();
progressForm.Load += (sX, eX) => dialogReadyTcs.TrySetResult(true);
// show the dialog asynchronousy
var dialogTask = Task.Factory.StartNew(
() => progressForm.ShowDialog(),
token,
TaskCreationOptions.None,
TaskScheduler.FromCurrentSynchronizationContext());
// await to make sure the dialog is ready
await dialogReadyTcs.Task;
// continue on a new nested message loop,
// which has been started by progressForm.ShowDialog()
// Create the progress reporter - and have it update
// the form directly (if form is valid (not disposed))
Action<int> progressHandlerAction = (progressInfo) =>
{
if (!progressForm.IsDisposed) // don't attempt to use disposed form
progressForm.UpdateProgress(progressInfo);
};
Progress<int> progress = new Progress<int>(progressHandlerAction);
try
{
// await the worker task
var taskResult = await MyService.DoSomethingWithResultAsync(100, token, progress);
}
catch (Exception ex)
{
while (ex is AggregateException)
ex = ex.InnerException;
if (!(ex is OperationCanceledException))
MessageBox.Show(ex.Message); // report the error
}
if (!progressForm.IsDisposed && progressForm.Visible)
progressForm.Close();
// this make sure showDialog returns and the nested message loop is over
await dialogTask;
}
private async void btn\u单击(对象发送方,事件参数e)
{
CancellationTokenSource cts=新的CancellationTokenSource();
CancellationToken=cts.token;
//创建ProgressForm,并将取消连接到它。
ProgressForm ProgressForm=新的ProgressForm();
progressForm.Cancelled+=()=>cts.Cancel();
var dialogReadyTcs=new TaskCompletionSource();
progressForm.Load+=(sX,eX)=>dialogReadyTcs.TrySetResult(true);
//异步显示对话框
var dialogTask=Task.Factory.StartNew(
()=>progressForm.ShowDialog(),
代币
任务创建选项。无,
TaskScheduler.FromCurrentSynchronizationContext());
//等待以确保对话框已准备就绪
等待对话框readytcs.Task;
//在新的嵌套消息循环上继续,
//它已由progressForm.ShowDialog()启动
//创建进度报告器-并对其进行更新
//直接提交表格(如果表格有效(未处理))
动作进度公差=(进度信息)=>
{
如果(!progressForm.IsDisposed)//不要尝试使用已释放的表单
progressForm.UpdateProgress(progressInfo);
};
进步=新的进步(进步和宽容);
尝试
{
//等待工人的任务
var taskResult=wait MyService.DoSomethingWithResultAsync(100,令牌,进度);
}
捕获(例外情况除外)
{
while(ex是aggregateeexception)
ex=ex.InnerException;
如果(!(ex是OperationCanceledException))
MessageBox.Show(例如Message);//报告错误
}
如果(!progressForm.IsDisposed&&progressForm.Visible)
progressForm.Close();
//这将确保showDialog返回并且嵌套的消息循环结束
等待对话任务;
}
我在这里看到的一个潜在问题是,在初始化和显示进度对话框(使用progressForm.ShowDialog()时)之前,工作任务开始执行
。此外,几乎从来没有必要将async/awaiy
与ContinueWith
混合使用。在这种情况下也可以避免这种情况。@nosratio-谢谢-我真的希望在表单以模式显示之前开始运行任务。它可以按原样工作-但是在调用“ShowDialog()之前启动任务有什么问题“?@Noseratio-我尝试了很多,没有使用ContinueWith
,而只是使用wait
-但我无法在我试图让它工作的上下文中理解它。有什么想法吗?看起来这种情况可能不会发生在这个特定的代码中,但在理论上,如果您在模态进度对话框完全打开之前启动任务初始化后,IProgress
通知可能会在对话框准备就绪之前到达UI线程。我认为我遇到的最大问题是,使用wait
(而不是ContinueWith
)这意味着我不能使用ShowDialog
,因为两者都是阻塞调用。如果我先调用ShowDialog
,代码在该点被阻塞,并且进度表单需要实际启动异步方法(这是我想要避免的)。如果我先调用wait MyService.DoSomethingWithResultAsync
,则此代码会阻塞,然后我无法显示我的进度表。我一定是缺少了一些简单的内容。我是async
/任务的新手-以前曾在此场景中使用过AsyncCallback
。谢谢-我以前从未见过TaskCompletionSource-seems是我缺少的主要东西。我不太确定你在用while做什么(例如AggregateException)ex=ex.InnerException;
虽然是部分。但除此之外-我已经根据您的输入重新编写了。@lcpldev,ex=ex.InnerException
循环只是为了在工作任务引发AggregateException
时到达实际异常。AggregateException
可以嵌套另一个AggregateException
等.问题:而不是使用Task.Factory.StartNewpublic class MyService
{
public async Task<bool> DoSomethingWithResult(
int arg, CancellationToken token, IProgress<int> progress)
{
// Note: arg value would normally be an
// object with meaningful input args (Request)
// un-quote this to test exception occuring.
//throw new Exception("Something bad happened.");
// Procressing would normally be several Async calls, such as ...
// reading a file (e.g. await ReadAsync)
// Then processing it (CPU instensive, await Task.Run),
// and then updating a database (await UpdateAsync)
// Just using Delay here to provide sample,
// using arg as delay, doing that 100 times.
for (int i = 0; i < 100; i++)
{
token.ThrowIfCancellationRequested();
await Task.Delay(arg);
progress.Report(i + 1);
}
// return value would be an object with meaningful results (Response)
return true;
}
}
public partial class MainForm : Form
{
public MainForm()
{
InitializeComponent();
}
private async void btnDo_Click(object sender, EventArgs e)
{
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken token = cts.Token;
// Create the ProgressForm, and hook up the cancellation to it.
ProgressForm progressForm = new ProgressForm();
progressForm.Cancelled += () => cts.Cancel();
// Create the progress reporter - and have it update
// the form directly (if form is valid (not disposed))
Action<int> progressHandlerAction = (progressInfo) =>
{
if (!progressForm.IsDisposed) // don't attempt to use disposed form
progressForm.UpdateProgress(progressInfo);
};
Progress<int> progress = new Progress<int>(progressHandlerAction);
// start the task, and continue back on UI thread to close ProgressForm
Task<bool> responseTask
= MyService.DoSomethingWithResultAsync(100, token, progress)
.ContinueWith(p =>
{
if (!progressForm.IsDisposed) // don't attempt to close disposed form
progressForm.Close();
return p.Result;
}, TaskScheduler.FromCurrentSynchronizationContext());
Debug.WriteLine("Before ShowDialog");
// only show progressForm if
if (!progressForm.IsDisposed) // don't attempt to use disposed form
progressForm.ShowDialog();
Debug.WriteLine("After ShowDialog");
bool response = false;
// await for the task to complete, get the response,
// and check for cancellation and exceptions
try
{
response = await responseTask;
MessageBox.Show("Result = " + response.ToString());
}
catch (AggregateException ae)
{
if (ae.InnerException is OperationCanceledException)
Debug.WriteLine("Cancelled");
else
{
StringBuilder sb = new StringBuilder();
foreach (var ie in ae.InnerExceptions)
{
sb.AppendLine(ie.Message);
}
MessageBox.Show(sb.ToString());
}
}
finally
{
// Do I need to double check the form is closed?
if (!progressForm.IsDisposed)
progressForm.Close();
}
}
}
private async void btnDo_Click(object sender, EventArgs e)
{
bool? response = null;
string errorMessage = null;
using (CancellationTokenSource cts = new CancellationTokenSource())
{
using (ProgressForm2 progressForm = new ProgressForm2())
{
progressForm.Cancelled +=
() => cts.Cancel();
var dialogReadyTcs = new TaskCompletionSource<object>();
progressForm.Shown +=
(sX, eX) => dialogReadyTcs.TrySetResult(null);
var dialogTask = Task.Factory.StartNew(
() =>progressForm.ShowDialog(this),
cts.Token,
TaskCreationOptions.None,
TaskScheduler.FromCurrentSynchronizationContext());
await dialogReadyTcs.Task;
Progress<int> progress = new Progress<int>(
(progressInfo) => progressForm.UpdateProgress(progressInfo));
try
{
response = await MyService.DoSomethingWithResultAsync(50, cts.Token, progress);
}
catch (OperationCanceledException) { } // Cancelled
catch (Exception ex)
{
errorMessage = ex.Message;
}
finally
{
progressForm.Close();
}
await dialogTask;
}
}
if (response != null) // Success - have valid response
MessageBox.Show("MainForm: Result = " + response.ToString());
else // Faulted
if (errorMessage != null) MessageBox.Show(errorMessage);
}
private async void btnDo_Click(object sender, EventArgs e)
{
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken token = cts.Token;
// Create the ProgressForm, and hook up the cancellation to it.
ProgressForm progressForm = new ProgressForm();
progressForm.Cancelled += () => cts.Cancel();
var dialogReadyTcs = new TaskCompletionSource<object>();
progressForm.Load += (sX, eX) => dialogReadyTcs.TrySetResult(true);
// show the dialog asynchronousy
var dialogTask = Task.Factory.StartNew(
() => progressForm.ShowDialog(),
token,
TaskCreationOptions.None,
TaskScheduler.FromCurrentSynchronizationContext());
// await to make sure the dialog is ready
await dialogReadyTcs.Task;
// continue on a new nested message loop,
// which has been started by progressForm.ShowDialog()
// Create the progress reporter - and have it update
// the form directly (if form is valid (not disposed))
Action<int> progressHandlerAction = (progressInfo) =>
{
if (!progressForm.IsDisposed) // don't attempt to use disposed form
progressForm.UpdateProgress(progressInfo);
};
Progress<int> progress = new Progress<int>(progressHandlerAction);
try
{
// await the worker task
var taskResult = await MyService.DoSomethingWithResultAsync(100, token, progress);
}
catch (Exception ex)
{
while (ex is AggregateException)
ex = ex.InnerException;
if (!(ex is OperationCanceledException))
MessageBox.Show(ex.Message); // report the error
}
if (!progressForm.IsDisposed && progressForm.Visible)
progressForm.Close();
// this make sure showDialog returns and the nested message loop is over
await dialogTask;
}