Winforms线程问题,第二个线程无法访问第一个主窗体控件

时间:2020-03-06 14:52:57  来源:igfitidea点击:

我有一个winforms应用程序,问题与线程有关。
由于我正在调用'MyCustomCode(),因此会创建一个新线程并调用该方法
'SomeMethod()'然后访问MessageBox.Show(...)。

问题与线程有关,因为新创建的线程正在尝试访问
在另一个线程上创建的控件。

我收到错误消息:

跨线程操作无效:控件" TestForm"是从创建该线程的线程之外的线程访问的。

public TestForm()
{
    InitializeComponent();

    // custom code
    //
    MyCustomCode();

}

public void SomeMethod()
{

    // ***** This causes an error  ****

    MessageBox.Show(this,   
        ex.Message, 
        "Error", 
        MessageBoxButtons.OK, 
        MessageBoxIcon.Error
    );
}

private void InitializeAutoUpdater()
{
        // Seperate thread is spun to keep polling for updates
        ThreadStart ts = new ThreadStart(SomeMethod);
        pollThread = new Thread(ts);
        pollThread.Start();
}

更新

如果我们看下面的示例http://www.codeproject.com/KB/cs/vanillaupdaterblock.aspx,则CheckAndUpdate方法正在调用MessageBox.Show(..),这就是我的问题所在。我本以为代码不错!

有趣的是,这段代码在星期五工作得很好???

解决方案

我们无法访问来自多个线程的UI元素。

解决此问题的一种方法是调用控件的Invoke方法,并委托使用UI元素(例如消息框)的函数。像这样的东西:

public delegate void InvokeDelegate();

public void SomeMethod()
{

    button1.Invoke((InvokeDelegate)doUIStuff);

}

void doUIStuff()
{
           MessageBox.Show(this,   
                ex.Message, 
                "Error", 
                MessageBoxButtons.OK, 
                MessageBoxIcon.Error
            );
}

我们不应使用BeginInvoke,而应使用Invoke,一旦掌握了这一点,就可以在确实需要的情况下考虑使用BeginInvoke。

  • 使用Control.BeginInvoke或者Control.Invoke方法

或者

  • 使用SynchronizationContext

'*******************************************************************
' Get a new processor and fire it off on a new thread.
'*******************************************************************
fpProc = New Processor(confTable, paramFile, keyCount)
AddHandler fpProc.LogEntry, AddressOf LogEntry_Handler
Dim myThread As System.Threading.Thread = New System.Threading.Thread(AddressOf fpProc.ProcessEntry)
myThread.Start()

然后,在父应用程序中,我们将拥有:

'*************************************************************************
'     Sub: LogEntry_Handler()
'  Author: Ron Savage
'    Date: 08/29/2007
'
' This routine handles the LogEntry events raised by the Processor class
' running in a thread.
'*************************************************************************
Private Sub LogEntry_Handler(ByVal logLevel As Integer, ByVal logMsg As String) Handles fProc.LogEntry
 writeLogMessage(logMsg);
End Sub

我就是做这个的。

为了避免跨线程异常(InvalidOperationException),下面是代码模式:

protected delegate void someGuiFunctionDelegate(int iParam);

protected void someGuiFunction(int iParam)
{
    if (this.InvokeRequired)
    {
        someGuiFunctionDelegate dlg = new 
            someGuiFunctionDelegate(this.someGuiFunction);
        this.Invoke(dlg, new object[] { iParam });
        return;
    }

    //do something with the GUI control here
}

我同意这很烦人,但这是Windows GUI控件不是线程安全的事实的产物。可以使用某处或者其他地方的标志来关闭该异常,但是不要这样做,因为它可能导致极其难以发现错误。

多线程时的第一法则是绝对不能通过工作线程接触UI。有多种实现多线程的方法,很难做到"正确"。

这是一篇简洁的文章,应该可以从辅助线程更新UI

这是一篇冗长的文章,讨论了.NET中的线程深度多线程

为了简单起见,我们可以使用BackGroundWorker类。此类将提供一个框架来处理线程和进度通知事件。ui线程将处理progress事件,并显示我们传回的错误消息。

检查是否需要调用

我特别喜欢递归调用。

public delegate void InvokeDelegate(string errMessage); 

    public void SomeMethod() 
    { 
        doUIStuff("my error message");
    } 

    void doUIStuff(string errMessage) 
    { 
        if (button1.InvokeRequired)
            button1.Invoke((InvokeDelegate)doUIStuff(errMessage)); 
        else
        {
               MessageBox.Show(this,    
                    ex.Message,  
                    errMessage,  
                    MessageBoxButtons.OK,  
                    MessageBoxIcon.Error 
                ); 
        }
    }

我知道这是一篇较旧的文章,但是最近我找到了使用泛型和扩展方法解决此问题的优雅方法。这是作者作品和一些评论的结合。

跨线程Winforms访问的通用方法

http://www.codeproject.com/KB/cs/GenericCrossThread.aspx

public static void Manipulate<T>(this T control, Action<T> action) where T : Control
{
    if (control.InvokeRequired)
    {
        control.Invoke(new Action<T, Action<T>>(Manipulate),
                    new object[] { control, action });
    }
    else
    { action(control); }
}

可以按以下方式调用此方法,为简单起见,我使用了一个标签。

someLabel.Manipulate(lbl => lbl.Text = "Something");