繁体   English   中英

处理功能区单击事件时,为什么会收到InvalidOperationException(跨线程操作无效)?

[英]Why do I get an InvalidOperationException (cross-thread operation not valid) when processing ribbon click events?

我为我的应用程序创建了一个“进度/取消”机制,通过该机制,我可以在执行长时间运行的操作时显示模式对话框,并让该对话框显示进度。 该对话框还具有取消按钮,以允许用户取消操作。 (感谢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的参数是CancellationTokenIProgress 在此示例中,我没有使用CancellationToken ,但是IProgress.Report方法正在为进度/取消表单上的标签控件设置文本。

如果我从表单上的按钮单击处理程序启动此长时间运行的操作,它将正常工作。 但是,我现在发现,如果我从VSTO PowerPoint加载项中的功能区按钮的单击事件处理程序中启动操作,它将在第二次调用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”,因为在此之前的调用就是发生异常的地方!

如果当前线程(您在其上调用ExecuteDummyLongOperation()的线程)没有同步提供程序,则这是预期的结果。 如果没有一个,则await运算符之后的继续只能在线程池线程上运行。

您可以通过在await表达式上放置一个断点来诊断这一点。 检查System.Threading.SynchronizationContext.Current的值。 如果为null ,则没有同步提供程序,并且当您从错误的线程更新表单时,代码将按预期失败。

我尚不完全清楚为什么您没有一个。 调用方法之前 ,可以通过在线程上创建表单来获取提供程序。 这将自动安装提供程序,即WindowsFormsSynchronizationContext类的实例。 在我看来,您创建ProgressForm太晚了。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM