Winforms线程问题,第二个线程无法访问第一个主窗体控件
我有一个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");