从其他线程引发事件






4.17/5 (10投票s)
一个通用函数,
引言
大多数情况下,引发事件是为了让拥有该对象的对象有机会显示任何类型的更改。“显示任何类型的更改”总是意味着访问一个控件。但是,当从一个侧线程引发事件时,当我们试图显示任何内容时,就会出现令人讨厌的“CrossThreadCall
-Exception”。简单地说,禁止从另一个线程访问控件(异常告诉我们)。跨线程控件访问需要“转移”到从主线程访问。这可以通过 Control.Invoke
机制来完成。这意味着:创建一个访问控件的方法,创建一个它的委托,并将该委托传递给一个 Control.Invoke()
调用(在文章 如何解决“跨线程操作无效” 中很好地完成了)。
但是 - 如果我的对象想要引发一个事件 - 它没有可以传递委托的控件!
为此,它只需使用应用程序的主窗体(这也是一个控件)。
现在,我们可以找到一个通用的、有效的解决方案,用于任何跨线程事件的引发。
前提条件:我们必须根据框架约定设计我们的事件,以实现事件。
这意味着,一个完整的指定事件由 3 部分组成
- 一个类“
MyEventArgs
”,继承自EventArgs
- 事件声明为“
EventHandler(Of MyEventArgs)
” - 一个“
OnMyEvent(e As MyEventArgs)
” -Sub
,它引发事件
EventArgs
是 EventArgs.Empty
,那么在 III 中不需要处理任何参数)- 一个“
OnMyEvent()
” - 无参数的Sub
,它引发事件,提交System.EventArgs.Empty
好的,为了让这个模式通用,快速看一下 III - OnMyEvent()
它要么具有 System.Action
- 委托的签名,要么具有 System.Windows.Forms.MethodInvoker
- 委托的签名。
将这些部分组合在一起,创建一个“通用的有效事件调用器”
Public Sub InvokeAction(Of T)( _
ByVal anAction As System.Action(Of T), _
ByVal Arg As T, _
Optional ByVal ThrowMainFormMissingError As Boolean = True)
If Not ThrowMainFormMissingError AndAlso _
Application.OpenForms.Count = 0 Then Return
With Application.OpenForms(0)
If .InvokeRequired Then 'if Invoking is required...
.Invoke(anAction, Arg) '...pass delegate and argument to Invoke()
Else '...otherwise...
anAction(Arg) '...call the delegate directly
End If
End With
End Sub
作为奖励:通过可选的传递 ThrowMainFormMissingError=False
,如果没有任何 OpenForm
可用(例如,应用程序正在关闭),我们可以抑制异常。
(但通常情况下,忘记它吧。)
使用 MethodInvoker
- 委托的相同过程
Public Sub InvokeMethod( _
ByVal aMethod As System.Windows.Forms.MethodInvoker, _
Optional ByVal ThrowMainFormMissingError As Boolean = True)
If Not ThrowMainFormMissingError AndAlso _
Application.OpenForms.Count = 0 Then Return
With Application.OpenForms(0)
If .InvokeRequired Then
.Invoke(aMethod)
Else
aMethod()
End If
End With
End Sub
Using the Code
根据框架的常用模式设计您的 XYEvent
(仅在“OnXYEvent()
” - Sub
中引发它)
为了从侧线程引发它,调用
InvokeAction(AddressOf OnXYEvent, new XYEventArgs())
而不是
OnXYEvent(new XYEventArgs())
代码示例
我简单地展示了整个类“CountDown
”。它包含了我提到的所有内容
- 事件“
Tick
”提交一个用户定义的EventArgs
- 事件“
Finished
”提交EventArgs.Empty
- 两者都从一个侧线程引发
Imports System.Threading
Public Class CountDown
Public Class TickEventArgs : Inherits EventArgs
Public ReadOnly Counter As Integer
Public Sub New(ByVal Counter As Integer)
Me.Counter = Counter
End Sub
End Class 'TickEventArgs
Public Event Tick As EventHandler(Of TickEventArgs)
Protected Overridable Sub OnTick(ByVal e As TickEventArgs)
RaiseEvent Tick(Me, e)
End Sub
Public Event Finished As EventHandler
Protected Overridable Sub OnFinished()
RaiseEvent Finished(Me, EventArgs.Empty)
End Sub
'System.Threading.Timer calls back from a side-thread
Private _AsyncTimer As New System.Threading.Timer( _
AddressOf AsyncTimer_Callback, _
Nothing, Timeout.Infinite, Timeout.Infinite)
Private _Counter As Integer
Public Sub Start(ByVal InitValue As Integer)
_Counter = InitValue
_AsyncTimer.Change(0, 1000) 'Execute first callback immediately
End Sub
Private Sub AsyncTimer_Callback(ByVal state As Object)
InvokeAction(AddressOf OnTick, New TickEventArgs(_Counter)) 'raise Tick
'to try a thread-unsafe call, comment out the line before,
' and uncomment the line after
'OnTick(New TickEventArgs(_Counter))
If _Counter = 0 Then
_AsyncTimer.Change(Timeout.Infinite, Timeout.Infinite) 'stop timer
InvokeMethod(AddressOf OnFinished) 'raise Finished
End If
_Counter -= 1 'do a countdowns job
End Sub
End Class
关注点
- 正如您所看到的,对
InvokeAction()
的调用不需要指定TypeParameter
。它从传递的参数推断出来。TypeParameter
-效果是强制执行,如果首先传递一个Action(Of EventArgs)
,则第二个参数只接受EventArgs
(而不是废话)。 - 这些调用器不仅对传输事件引发器有用,而且还可以将任何 System.Action 或 MethodInvoker 调用 传输到主线程。
Control.Invoke()
速度很慢。不要在侧线程引发的事件处理程序中填充TreeView
的数百个节点。
(无抄袭)
我已经在 另一个 VB.NET - 平台 上发布了这个问题。