我为我的应用程序创建了一个“进度/取消”机制,我可以在执行长时间运行的操作时显示模态对话框,并让对话框显示进度。该对话框还有一个取消按钮,允许用户取消操作。 (感谢SO社区帮助我完成这一部分。)
以下是运行虚拟长时间运行操作的代码:
public static async Task ExecuteDummyLongOperation()
{
await ExecuteWithProgressAsync(async (ct, ip) =>
{
ip.Report("Hello world!");
await TaskEx.Delay(3000);
ip.Report("Goodbye cruel world!");
await TaskEx.Delay(1000);
});
}
lamba的参数是CancellationToken
和IProgress
。我在这个示例中没有使用CancellationToken
,但IProgress.Report
方法是在进度/取消表单上设置标签控件的文本。
如果我从表单上的按钮单击处理程序启动这个长时间运行的操作,它可以正常工作。但是,我现在发现,如果我从VSTO PowerPoint加载项中的功能区按钮的click事件处理程序启动操作,则在第二次调用ip.Report
时失败(在它尝试设置标签控件的文本时)。在这种情况下,我得到了可怕的InvalidOperationException
,说有一个无效的跨线程操作。
我觉得有两件事令人费解:
ip.Report
但不是第一次?我没有在这两个电话之间切换线程。您当然希望看到其余的代码。我试图把所有东西都拉回到骨头上:
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace AsyncTestPowerPointAddIn
{
internal partial class ProgressForm : Form
{
public ProgressForm()
{
InitializeComponent();
}
public string Progress
{
set
{
this.ProgressLabel.Text = value;
}
}
private void CancelXButton_Click(object sender, EventArgs e)
{
this.DialogResult = DialogResult.Cancel;
this.Close();
}
public static async Task ExecuteDummyLongOperation()
{
await ExecuteWithProgressAsync(async (ct, ip) =>
{
ip.Report("Hello world!");
await TaskEx.Delay(3000);
ip.Report("Goodbye cruel world!");
await TaskEx.Delay(1000);
});
}
private static async Task ExecuteWithProgressAsync(Func<CancellationToken, IProgress<string>, Task> operation)
{
var cancellationTokenSource = new CancellationTokenSource();
var progress = new Progress<string>();
var operationTask = operation(cancellationTokenSource.Token, progress);
// Don't show the dialog unless the operation takes more than a second
const int TimeDelayMilliseconds = 1000;
var completedTask = TaskEx.WhenAny(TaskEx.Delay(TimeDelayMilliseconds), operationTask).Result;
if (completedTask == operationTask)
await operationTask;
// Show a progress form and have it automatically close when the task completes
using (var progressForm = new ProgressForm())
{
operationTask.ContinueWith(_ => { try { progressForm.Close(); } catch { } }, TaskScheduler.FromCurrentSynchronizationContext());
progress.ProgressChanged += ((o, s) => progressForm.Progress = s);
if (progressForm.ShowDialog() == DialogResult.Cancel)
cancellationTokenSource.Cancel();
}
await operationTask;
}
}
}
表单本身只有一个标签(ProgressLabel
)和一个按钮(CancelXButton
)。
功能区按钮和表单按钮的按钮单击事件处理程序只需调用ExecuteDummyLongOperation
方法。
编辑:更多信息
在@ JamesManning的请求下,我进行了一些跟踪来观察ManagedThreadId的值,如下所示:
await ExecuteWithProgressAsync(async (ct, ip) =>
{
System.Diagnostics.Trace.TraceInformation("A:" + Thread.CurrentThread.ManagedThreadId.ToString());
ip.Report("Hello world!");
System.Diagnostics.Trace.TraceInformation("B:" + Thread.CurrentThread.ManagedThreadId.ToString());
await TaskEx.Delay(3000);
System.Diagnostics.Trace.TraceInformation("C:" + Thread.CurrentThread.ManagedThreadId.ToString());
ip.Report("Goodbye cruel world!");
System.Diagnostics.Trace.TraceInformation("D:" + Thread.CurrentThread.ManagedThreadId.ToString());
await TaskEx.Delay(1000);
System.Diagnostics.Trace.TraceInformation("E:" + Thread.CurrentThread.ManagedThreadId.ToString());
});
这很有意思。从表单调用时,线程ID不会更改。但是,当从功能区调用时,我得到:
powerpnt.exe Information: 0 : A:1
powerpnt.exe Information: 0 : B:1
powerpnt.exe Information: 0 : C:8
powerpnt.exe Information: 0 : D:8
因此,当我们从第一次“返回”时,线程ID正在改变。
我也很惊讶我们在跟踪中看到了“D”,因为之前的调用是异常发生的地方!
答案 0 :(得分:3)
如果当前线程(您调用ExecuteDummyLongOperation()的线程)没有同步提供程序,则这是预期的结果。没有一个, await 运算符之后的延续只能在线程池线程上运行。
您可以通过在await表达式上放置断点来诊断它。检查System.Threading.SynchronizationContext.Current的值。如果它是 null ,则没有同步提供程序,当您从错误的线程更新表单时,您的代码将按预期失败。
我不完全清楚为什么你没有。您可以通过在线程上创建表单来获得提供者,在之前调用该方法。这会自动安装提供程序,即WindowsFormsSynchronizationContext类的实例。在我看来,就像你太晚创建你的ProgressForm一样。