アプリケーションの「進行/キャンセル」メカニズムを作成しました。これにより、長時間実行される操作の実行中にモーダルダイアログを表示し、ダイアログに進行状況を表示させることができます。ダイアログには、ユーザーが操作をキャンセルできるようにするキャンセルボタンもあります。(この部分を手伝ってくれた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);
});
}
ランバのパラメータはaCancellationToken
とIProgress
。です。CancellationToken
この例ではを使用していませんが、IProgress.Report
メソッドは進行状況/キャンセルフォームのラベルコントロールのテキストを設定しています。
フォームのボタンクリックハンドラーからこの長時間実行操作を開始すると、正常に機能します。ただし、VSTO PowerPointアドインのリボンボタンのクリックイベントハンドラーから操作を開始すると、への2回目の呼び出しで失敗することがわかりましたip.Report
(のテキストを設定しようとした時点で)ラベルコントロール)。この場合、InvalidOperationException
無効なクロススレッド操作があると恐ろしく思います。
私が困惑していることが2つあります。
- リボンのボタンをクリックして操作を呼び出したときに問題が発生するのに、フォームのボタンをクリックして操作を呼び出したときに問題が発生しないのはなぜですか?
ip.Report
最初の呼び出しではなく、2回目の呼び出しで問題が発生するのはなぜですか?私はこれらの2つの呼び出しの間でスレッドを切り替えていません。
もちろん、残りのコードを見たいと思うでしょう。私はすべてをむき出しの骨に戻そうとしました:
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」が表示されていることにも驚いています。