65.9K
CodeProject 正在变化。 阅读更多。
Home

调用事件而不担心跨线程异常

2010年5月12日

CPOL

3分钟阅读

viewsIcon

61989

消除在跨线程调用事件时更改控件属性时出现的 InvalidOperationException 异常。

引言

InvalidOperationException: "跨线程操作无效:从创建它的线程以外的线程访问控件'<name>'"。

当需要在后台线程上完成工作,以使应用程序 UI 能够响应用户,甚至允许用户取消当前操作,同时从后台操作中更改 UI 上的控件的属性时,经常会看到此异常。

我将在此解释一个处理此特定问题的解决方案,提供一个适合使用该解决方案的场景,我在其中使用扩展方法为该问题提供一个通用解决方案。

要实现自己处理跨线程属性更改的自定义用户控件,请参阅有关自定义/用户控件的部分。

背景

应该注意的是,当需要在后台执行特定操作时,BackgroundWorker 绝对是最好的解决方案。

但是,并非总是可以按原样使用 BackgroundWorker,甚至可能需要编写自己的线程处理类。 这就是以下扩展方法派上用场的地方。

问题

考虑这样一种情况:您想包装一个 BackgroundWorker 并提供一个额外的事件,该事件报告 worker 中正在完成的工作的状态消息。

public class ThreadedCall
{
    // The worker being wrapped by our class
    private BackgroundWorker bw;
    // Our new event that will be called
    // whenever our class wants to report a status
    public event StatusChangedEventHandler StatusChanged;

    // The entry point of our class to start background opperation
    public void Start()
    {
        bw = new BackgroundWorker();
        bw.DoWork += new DoWorkEventHandler(bw_DoWork);
        bw.RunWorkerAsync();
    }

    // The method that does the actual work, run inside our wrapped worker
    void bw_DoWork(object sender, DoWorkEventArgs e)
    {
        // Report a status here
        if (StatusChanged!= null)
            StatusChanged.Invoke(this, 
               new StatusChangedEventArgs("Phase 1"));
        Thread.Sleep(100);  // Do some work
        // Report another status here
        if (StatusChanged!= null)
            StatusChanged.Invoke(this, 
               new StatusChangedEventArgs("Phase 2"));
    }
}

我们的事件处理程序代码

// Delegate to eventhandler that takes a StatusChangedEventArgs as paramater
public delegate void StatusChangedEventHandler(object sender, StatusChangedEventArgs e);
public class StatusChangedEventArgs : EventArgs
{
    private string status;
    // The status property added to the eventargs used to supply status to the callee
    public string Status
    {
        get { return status; }
        set { status = value; }
    }
    public StatusChangedEventArgs(string status)
    {
        this.status = status;
    }
}

一些测试我们类的代码(UI 代码)

private void button1_Click(object sender, EventArgs e)
{
    // Instantiate and start or worker wrapper
    ThreadedCall t = new ThreadedCall();
    t.StatusChanged += new StatusChangedEventHandler(t_StatusChanged);
    t.Start();
}

void t_StatusChanged(object sender, StatusChangedEventArgs e)
{
    // The following line will raise an exception!
    textBox1.Text = string.Format("Status: {0}", e.Status);
}

异常

上面给出的示例将在指示的位置收到 InvalidOperationException。 这是完全有效的,因为我们不允许在创建控件的线程以外的其他线程上更改控件的某些属性。

有时,由于偷懒,我使用以下代码禁用这些异常

Form.CheckForIllegalCrossThreadCalls = false; 

但这是非常糟糕的编码实践,会导致控件出现意外行为。 处理此问题的正确方法是在您的被调用者/UI 中创建一个委托,并检查 Control.InvokeRequired 属性,然后使用您的新委托调用 Control.Invoke 以更改所需的属性。

虽然这是正确的,但我发现它很难阅读,并且看起来不如它应有的那样优雅。

解决方案

按照上面提到的正确解决方案,我将方法的调用从 UI 移到一个 扩展方法 中,使其具有极高的可重用性。 这将检查是否需要调用,然后以控件的创建线程而不是后台线程调用委托。

public static class DelegateExpansion
{
    // Prevent CrossThreadException by invoking delegate through target control's thread.
    public static object CrossInvoke(this Delegate delgt,object sender,EventArgs e)
    {
        if (delgt.Target is Control && ((Control)delgt.Target).InvokeRequired)
        {
            return ((Control)delgt.Target).Invoke(delgt, new object[] { sender, e });
        }
        return delgt.Method.Invoke(delgt.Target, new object[] { sender, e });
    }
}

然后需要更改后台代码,使其不使用标准委托 Invoke,而是使用我们新的 CrossInvoke 方法,如下所示

// The method that does the actual work, run inside our wrapped worker
void bw_DoWork(object sender, DoWorkEventArgs e)
{
    // Report a status here
    if (StatusChanged!= null)
        StatusChanged.CrossInvoke(this, 
           new StatusChangedEventArgs("Phase 1"));
    Thread.Sleep(100);  // Do some work
    // Report another status here
    if (StatusChanged!= null)
        StatusChanged.CrossInvoke(this, 
          new StatusChangedEventArgs("Phase 2"));
}

这消除了更改或复杂化我们任何 GUI 代码的需要,使其保持可读性和“干净”。

自定义/用户控件

在设计供其他开发人员使用的控件时,上述解决方案意义不大。 在这种情况下,您可以通过简单地更改属性以适应跨线程调用来使控件更“线程安全”。

示例

private string innerText;
public string Text
{
    get { return innerText; }
    set
    {
        if (this.InvokeRequired)
            this.Invoke(new MethodInvoker(() => { innerText = value; }));
        else innerText = value;
    }
}

这将确保可以访问您的控件属性,而无需外部调用者担心在正确的线程上调用。

历史

  • 2010 年 5 月 12 日 - 原始文章。
  • 2010 年 5 月 13 日 - 添加了“自定义/用户控件”部分。
© . All rights reserved.