从辅助线程更新用户界面
Basic Instincts... 原文出处:MSDN Magazine May 2004 (Basic Instincts) 下载源代码:BasicInstincts0405.exe (130KB) 在我 2004年1月 的专栏里,我讨论了如何利用委托(Delegate)实现异步执行一个方法。当时,我展示了如何在一个 Windows Form 应用内部通过调用一个委托对象的 BeginInvoke()方法来实现一个异步的方法调用。你也许还记得我是如何设置一个回调(Callback)方法,并让上述的异步方法在执行完毕后自动地触发这个回调方法。但现在我要停下来说一说如何更新UI(User Interface,用户界面),让用户知道这个工作已经完成。
TargetHandler.BeginInvoke("CA", CallbackHandler, Nothing) 使用回调方法最主要的优点在于可以避免不断地轮询该异步方法是否已经执行完毕。当辅助线程完成了GetCustomerList()方法的执行后,它会在返回至由CLR (Common Language Runtime,公共语言运行库)管理的辅助线程池之前紧接着执行MyCallbackMethod()。 我先添加了一个新的自定义方法 UpdateUI()。这个方法在回调方法需要更新UI时被调用。UpdateUI()的原型是特定于该应用的,不具有普遍性(译注:指UpdateUI()的名称、参数表可以自由设置,下文亦有说明)。 ''''*** 当需要更新UI时被调用 Sub UpdateUI(StatusMessage As String, Customers As String()) ''''*** 这个方法将把控制权切换回主UI线程 End Sub UpdateUI()方法有两个参数,一个是string类型的StatusMessage,用来传递将要向用户显示的提示字符串;另一个是string类型的数组Customers,其中存储了从异步执行的GetCustomerList()方法返回的一个顾客名称的列表。如果你想在个人的应用程序中使用类似的设计,这里的参数列表并不是必须的,你可以选择与你的应用相关的设计。 Dim handler As New UpdateUIHandler(AddressOf UpdateUI_Impl) Dim args() As Object = { StatusMessage, Customers } Me.BeginInvoke(handler, args) 第一个参数是一个委托对象,该参数允许辅助线程向主线程传递一个委托的引用,告诉主线程应该执行哪一个方法。注意在本文中的这个委托对象指向了一个名为UpdateUI_Impl的方法。第二个参数是一个object类型的数组。这是一个非常灵活的参数,因为它可以让辅助线程传递任意数目的任意类型的参数。在我的示例里,向BeginInvoke()方法传递的object数组中包括了两个元素:一个包括状态信息的字符串,一个存储了顾客名称的字符串数组。 Sub UpdateUI_Impl(StatusMessage As String, Customers As String()) ''''*** 在主UI线程中更新UI控件 Me.sbMain.Panels(0).Text = StatusMessage Me.lstCustomers.DataSource = Customers End Sub
如前所述,我已经设计了一种安全可靠的方式实现UI的更新。但是我还想再说说其他的一些技巧,使之更完善。如果能让UpdateUI()方法直接被form的方法调用就更合适了。但是目前的设计决定了在运行着的主UI线程代码中调用UpdateUI()方法并不特别高效。因为此时UpdateUI()还是要多余地去创建一个委托对象,并调用BeginInvoke()。 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。如果你需要单独下载本文的 Visual Basic.NET 示例应用程序及源码,请点击文章标题下的相应链接。 有任何建议或者意见,请给我 Email:instinct@microsoft.com
Ted Pattison 是教育培训公司 Barracuda .NET 的创立人之一, 该公司帮助其他公司使用 Microsoft 的技术实现协同应用。Ted 也是 《Building Applications and Components with Visual Basic .NET》(Addison-Wesley, 2003)等书的作者。 |
您可以使用 Control.Invoke 从 UI 线程以外的其他线程更新用户界面 (UI)。此方法在 UI 线程上的控件线程上下文中执行代理。.NET Framework 精简版只支持 .NET Framework 完整版中的重载 Control.Invoke 方法。Control.Invoke 只使用一个参数:一个指定在 UI 线程上执行哪个方法的代理。该代理的类型必须为 EventHandler,并且具有以下签名:
void MyFunctionName(object sender, EventArgs e)
需要注意的一点是,如果要在辅助线程中更新 UI,就必须在代码中调用 Application.DoEvents()。调用 Application.DoEvents() 可确保辅助线程激发的任何事件都由该 UI 线程处理。
下面的示例代码说明如何创建辅助线程,然后从 UI 线程和辅助线程更新名为 listBox1 的 ListBox 控件:
// 此变量将保留辅助线程设置的一些文本 public string Message = ""; // 创建辅助线程,然后将项目从 UI 线程添加到 // ListBox 中 public void DoThreading() { // 创建并启动辅助线程 ThreadStart starter = new ThreadStart(this.UpdateListBox); Thread t = new Thread(starter); t.Start(); // 循环 4 次,每次都向 ListBox 中添加一条消息 for(int i = 0; i < 4; i++); { this.listBox1.Items.Add("来自 UI 线程的消息"); this.listBox1.Update(); // 处理 UI 线程中排队的事件 Application.DoEvents(); // 将进程挂起一秒钟 Thread.Sleep(1000); } this.listBox1.Items.Add("来自 UI 线程的上一条消息"); this.listBox1.Update(); } public void UpdateListBox() { for(int j = 0; j < 5; j++) { // 设置要从辅助线程添加到 ListBox 中的 // 消息 this.Message = "辅助线程的循环数 = " + j.ToString(); // 在 ListBox 的线程上下文中调用 WorkerUpdate // 方法 this.listBox1.Invoke(new EventHandler(WorkerUpdate)); Thread.Sleep(700); } } // 为更新 ListBox 从辅助线程中 // 调用的代理 public void WorkerUpdate(object sender, EventArgs e) { this.listBox1.Items.Add(this.Message); this.listBox1.Update(); }
要使用此代码,请执行以下操作:
- 使用 Windows 应用程序模板创建新的智能设备应用程序。
- 将 ListBox 控件添加到 Windows 窗体中(默认名称为 listBox1)。
- 将使用 System.Threading; 的语句添加到 Form1.cs 文件的顶部。
- 将代码粘贴到 Form1 类中。
- 从 Form1 构造函数中调用 DoThreading 方法。