UML软件工程组织

利用辅助线程更新用户界面UI
转自:www.ASPCool.com 原著:Ted Pattison

以前我讨论了如何利用委托(Delegate)实现异步执行一个方法。当时,我展示了如何在一个 Windows Form 应用内部通过调用一个委托对象的 BeginInvoke()方法来实现一个异步的方法调用。你也许还记得我是如何设置一个回调(Callback)方法,并让上述的异步方法在执行完毕后自动地触发这个回调方法。但现在我要停下来说一说如何更新UI(User Interface,用户界面),让用户知道这个工作已经完成。
  在Windows Form的辅助线程中更新UI的元素是一项需要点技巧的工作。因为辅助线程被禁止读写窗体或者窗体控件中的数据属性(Property)。做出这样的限制是因为窗体中的form对象与control对象并非是线程安全的(译注:指不能保证任一时刻仅有一个线程在访问该对象)。只有主线程能直接访问form的或者form中control的数据属性。因此你必须学会在遵守这个规则的前提下更新UI。
  在这里,你将学会如何编写代码实现程序的控制权从正在执行的辅助线程向主线程的切换——使用一种称为“引导”的方法(Marshal,译注:不要把它与.NET中的封送过程混淆,作者只是使用了相同的单词)。通过它,你可以安全可靠地实现UI的更新。一旦从辅助线程切换到主线程中来,你就可以自由改变form的或者form中control的数据属性了。

使用委托进行异步执行

  首先来看看名为Form1的这个Windows Form应用的代码(参见 Figure 1)。这段代码演示了如何异步地执行一个方法。这里的技巧在于将一个目标方法(GetCustomerList())绑定到了一个委托对象上,然后通过调用这个委托对象的BeginInvoke()方法开始该目标方法的异步执行。
  这里使用的技巧也展示了如何将一个回调方法(比如MyCallbackMethod())绑定到第二个委托对象上,然后将该委托对象的一个引用(Reference)作为BeginInvoke()方法的第二个参数传递给它。

TargetHandler.BeginInvoke("CA", CallbackHandler, Nothing)

 使用回调方法最主要的优点在于可以避免不断地轮询该异步方法是否已经执行完毕。当辅助线程完成了GetCustomerList()方法的执行后,它会在返回至由CLR (Common Language Runtime,公共语言运行库)管理的辅助线程池之前紧接着执行MyCallbackMethod()。
  一定要切记:MyCallbackMethod()这样的回调方法是在辅助线程中执行的,而不是在主线程中。这意味着你永远不要企图在类似的回调方法中去实现UI的更新!你所要做的,恰恰是编写代码以实现程序的执行流程从回调方法所在的辅助线程切换回主线程。我会通过一个完整的实例一步步地教会你。

切换至 UI 线程

 我先添加了一个新的自定义方法 UpdateUI()。这个方法在回调方法需要更新UI时被调用。UpdateUI()的原型是特定于该应用的,不具有普遍性(译注:指UpdateUI()的名称、参数表可以自由设置,下文亦有说明)。

''''*** 当需要更新UI时被调用
Sub UpdateUI(StatusMessage As String, Customers As String())
''''*** 这个方法将把控制权切换回主UI线程

End Sub

 UpdateUI()方法有两个参数,一个是string类型的StatusMessage,用来传递将要向用户显示的提示字符串;另一个是string类型的数组Customers,其中存储了从异步执行的GetCustomerList()方法返回的一个顾客名称的列表。如果你想在个人的应用程序中使用类似的设计,这里的参数列表并不是必须的,你可以选择与你的应用相关的设计。
  接着,我将重写MyCallbackMethod()的实现代码,让它调用UpdateUI()方法。这样应用程序就可以在完成GetCustomerList()的异步调用后开始更新UI。其实现代码参见 Figure 2
  正如你所见到的,对UpdateUI()的调用是在同一个try语句块的EndInvoke()调用之后发生的。在try语句块内进行EndInvoke()的调用是非常重要的,因为这是唯一可以确定异步的方法调用是否已经完成的方法。当成功地完成对EndInvoke()的调用后,MyCallBackMethod()调用UpdateUI()方法更新UI。
  现在让我们仔细看一看UpdateUI()的实现代码。正如我之前解释的,MyCallbackMethod()以同步的方式调用UpdateUI()。这意味着这两个方法都在辅助线程内执行,而不是主线程。因此,UpdateUI()方法也不能直接访问form或者form中的control了。它要做的只是将执行的流程从辅助线程切换到主线程。
  当你准备将程序的执行流程从辅助线程切换到主线程时,你需要调用Windows Forms控件两个不同的公开方法中的一个。这两个方法是Invoke()与BeginInvoke(),定义在System.Windows.Forms命名空间的Control类中。你可以调用任一个Control对象甚至Form对象的Invoke()或者BeginInvoke()方法。在我的示例代码里,我将调用Form对象的BeginInvoke()方法。
  在你开始调用Form或者Control对象的Invoke()或者BeginInvoke()方法时也许会感到有点疑惑。也许你已经猜到了,委托对象也有一对同名方法——Invoke()与BeginInvoke()。尽管如此,两种对象所支持的Invoke()与BeginInvoke()方法是截然不同的。
  对Form或者Control对象的Invoke()或者BeginInvoke()方法的调用将会导致执行流程从辅助线程向主UI线程的切换。这两个方法的区别在于Invoke()的调用是阻塞的(译注:即对Invoke()的调用会导致程序一直等待该方法完成后才继续执行),而BeginInvoke()是非阻塞的。在多数情况下使用BeginInvoke()更高效,因为辅助线程可以继续执行,而不需要等待主UI线程完成UI更新。在本月专栏的这个示例里,我也是基于此选择了BeginInvoke()。
  现在应该开始通过一个对BeginInvoke()方法的调用实现UpdateUI()方法了。正如你在 Figure 3 里所见的,我在UpdateUI()的实现代码里调用了Form对象的BeginInvoke()方法。从更高一层看,辅助线程对BeginInvoke()的调用触发了主线程去运行一个名为UpdateUI_Impl的方法。注意对Form对象BeginInvoke()方法的调用需要两个参数:

Dim handler As New UpdateUIHandler(AddressOf UpdateUI_Impl)
Dim args() As Object = { StatusMessage, Customers }
Me.BeginInvoke(handler, args)

 第一个参数是一个委托对象,该参数允许辅助线程向主线程传递一个委托的引用,告诉主线程应该执行哪一个方法。注意在本文中的这个委托对象指向了一个名为UpdateUI_Impl的方法。第二个参数是一个object类型的数组。这是一个非常灵活的参数,因为它可以让辅助线程传递任意数目的任意类型的参数。在我的示例里,向BeginInvoke()方法传递的object数组中包括了两个元素:一个包括状态信息的字符串,一个存储了顾客名称的字符串数组。
  下面的事情更有趣了。当辅助线程调用BeginInvoke()后,Windows Forms自动在主UI线程中调用这个UpdateUI_Impl()方法,并将BeginInvoke()传递来的那个object类型的数组传递给UpdateUI_Impl()。于是在UpdateUI_Impl()的实现内部,它可以访问状态信息字符串与顾客名称的字符串数组了。而且由于UpdateUI_Impl()方法是在主线程中执行的,所以它可以使用这些参数去直接更新该窗体上的任一个控件的数据属性的值。

Sub UpdateUI_Impl(StatusMessage As String, Customers As String())
''''*** 在主UI线程中更新UI控件
Me.sbMain.Panels(0).Text = StatusMessage
Me.lstCustomers.DataSource = Customers
End Sub

Control.InvokeRequired

 如前所述,我已经设计了一种安全可靠的方式实现UI的更新。但是我还想再说说其他的一些技巧,使之更完善。如果能让UpdateUI()方法直接被form的方法调用就更合适了。但是目前的设计决定了在运行着的主UI线程代码中调用UpdateUI()方法并不特别高效。因为此时UpdateUI()还是要多余地去创建一个委托对象,并调用BeginInvoke()。
  现在让我们修改一下UpdateUI()的实现,让它可以判断目前是运行在辅助线程还是主UI线程中。对此你可以通过访问Control类的一个公共数据属性InvokeRequired进行判断。这样的编码技巧给UpdateUI()方法提供了机会,当它在主线程中运行时可以选择合适的时候直接同步地调用UpdateUI_Impl()。

Sub UpdateUI(ByVal StatusMessage As String, ByVal Customers As String())
	If Me.InvokeRequired Then
		''''*** 切换至主UI线程
		Dim handler As New UpdateUIHandler(AddressOf UpdateUI_Impl)
		Dim args() As Object = {StatusMessage, Customers}
		
		Me.BeginInvoke(handler, args)
	Else
		''''*** 已经在主UI线程中运行了,直接调用
		UpdateUI_Impl(StatusMessage, Customers)
	End If
End Sub

 你现在已经看到了所有需要的步骤,以实现在辅助线程中执行一个异步的方法,以及采用一种安全可靠的方式实现UI更新。完整的过程请参见 Figure 4

附:

Figure 1 异步的方法执行

Class Form1 : Inherits Form

'*** create delegate object to execute method asynchronously
Private TargetHandler As GetCustomerListHandler _
= AddressOf DataAccessCode.GetCustomerList

'*** create delegate object to service callback from CLR
Private CallbackHandler As AsyncCallback _
= AddressOf MyCallbackMethod

Sub cmdExecuteTask_Click(sender As Object, e As EventArgs) _
Handles cmdExecuteTask.Click

'*** execute method asynchronously with callback
TargetHandler.BeginInvoke("CA", CallbackHandler, Nothing)
End Sub

'*** CLR calls this method when asynchronous method completes
'*** note this method runs on worker thread (not the UI thread)
Sub MyCallbackMethod(ByVal ar As IAsyncResult)
Dim retval As String()
retval = TargetHandler.EndInvoke(ar)
'*** add code here to trigger updating of user interface
End Sub

End Class

Class DataAccessCode
Shared Function GetCustomerList(ByVal State As String) As String()
'*** call across network to DBMS or Web services to retrieve data
'*** imagine; this method takes several seconds to complete
End Function
End Class


--------------------------------------------------------------------------------
Figure 2 调用UpdateUI

'*** this method fires at completion of asynchronous call
Sub MyCallbackMethod(ByVal ar As IAsyncResult)
Try
'*** retrieve return value from async call
Dim retval As String()
retval = TargetHandler.EndInvoke(ar)
'*** begin process to update UI
UpdateUI("Task complete", retval)
Catch ex As Exception
Dim msg As String
msg = "Error: " & ex.Message
UpdateUI(msg, Nothing)
End Try
End Sub


--------------------------------------------------------------------------------
Figure 3 调用BeginInvoke

Sub UpdateUI(StatusMessage As String, Customers As String())
'*** switch control over to primary UI thread
Dim handler As New UpdateUIHandler(AddressOf UpdateUI_Impl)
Dim args() As Object = {StatusMessage, Customers}
'*** call BeginInvoke method of Form object
Me.BeginInvoke(handler, args)
End Sub

Delegate Sub UpdateUIHandler(StatusMessage As String, _
Customers As String())

Sub UpdateUI_Impl(StatusMessage As String, Customers As String())
'*** update user interface controls from primary UI thread
Me.sbMain.Panels(0).Text = StatusMessage
Me.lstCustomers.DataSource = Customers
End Sub


--------------------------------------------------------------------------------
Figure 4 在线程间切换

Public Class Form1 : Inherits Form

'*** create delegate object to execute method asynchronously
Private TargetHandler As GetCustomerListHandler _
= AddressOf DataAccessCode.GetCustomerList

'*** create delegate object to service callback from CLR
Private CallbackHandler As AsyncCallback _
= AddressOf MyCallbackMethod

Sub cmdExecuteTask_Click(sender As Object, e As EventArgs) _
Handles cmdExecuteTask.Click
'*** execute method asynchronously with callback
UpdateUI("Starting task...", Nothing)
TargetHandler.BeginInvoke("CA", CallbackHandler, Nothing)
End Sub

'*** callback method runs on worker thread and not the UI thread
Sub MyCallbackMethod(ByVal ar As IAsyncResult)
Try
Dim retval As String()
retval = TargetHandler.EndInvoke(ar)
UpdateUI("Task complete", retval)
Catch ex As Exception
Dim msg As String
msg = "Error: " & ex.Message
UpdateUI(msg, Nothing)
End Try
End Sub

'*** can be called from any method on form to update UI
Sub UpdateUI(StatusMessage As String, Customers As String())
'*** check to see if thread switch is required
If Me.InvokeRequired Then
Dim handler As New UpdateUIHandler(AddressOf UpdateUI_Impl)
Dim args() As Object = {StatusMessage, Customers}
Me.BeginInvoke(handler, args)
Else
UpdateUI_Impl(StatusMessage, Customers)
End If
End Sub

'*** delegate used to switch control over to primary UI thread
Delegate Sub UpdateUIHandler(StatusMessage As String, _
Customers As String())

'*** this method always runs on primary UI thread
Sub UpdateUI_Impl(StatusMessage As String, Customers As String())
Me.sbMain.Panels(0).Text = StatusMessage
Me.lstCustomers.DataSource = Customers
End Sub

End Class

 

 

版权所有:UML软件工程组织