0

アプリケーションの「進行/キャンセル」メカニズムを作成しました。これにより、長時間実行される操作の実行中にモーダルダイアログを表示し、ダイアログに進行状況を表示させることができます。ダイアログには、ユーザーが操作をキャンセルできるようにするキャンセルボタンもあります。(この部分を手伝ってくれた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);
        });
    }

ランバのパラメータはaCancellationTokenIProgress。です。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」が表示されていることにも驚いています。

4

1 に答える 1

4

これは、ExecuteDummyLongOperation()を呼び出した現在のスレッドに同期プロバイダーがない場合に予想される結果です。1つがないと、 awaitオペレーターの後の継続は、スレッドプールスレッドでのみ実行できます。

これは、await式にブレークポイントを設定することで診断できます。System.Threading.SynchronizationContext.Currentの値を調べます。nullの場合、同期プロバイダーはなく、間違ったスレッドからフォームを更新すると、コードは期待どおりに失敗します。

なぜあなたが持っていないのか私には完全には明らかではありません。メソッドを呼び出すに、スレッド上にフォームを作成することでプロバイダーを取得します。これにより、WindowsFormsSynchronizationContextクラスのインスタンスであるプロバイダーが自動的にインストールされます。ProgressFormの作成が遅すぎるように見えます。

于 2012-10-04T10:31:45.390 に答える