异步处理函数和 Web 服务调用
如何异步处理函数和 Web 服务调用。
引言
我一直在进行的一个项目要求实现异步处理,特别是针对耗时较长的 Web 服务调用。因此,我开始研究微软异步处理应用程序块,并阅读有关多线程等文章。我偶然发现的一篇文章是 Roy Osherove 关于《在 .NET 1.1 中实现 .NET 2.0 的 Background Worker 进程》的博客。这似乎正是我所需要的,特别是当我阅读了 Joval Löwy 的原始文章后,但有三点不同。它没有提到如何使用它来调用 Web 服务;它只能传递一个参数,因此对于执行更复杂的函数,您需要将参数作为数组或哈希表传递;而且它是用 C# 编写的,而我的项目是 VB.NET,所以我无法将其放入我的代码中。是的,我知道我可以在 VB.NET 中引用 C# 程序集或项目,但为了我的解决方案更清晰,我需要将整个代码移植到 VB.NET。
背景
在 .NET 中,异步调用模式已经被探索了很多次,并且在 David Hill 的《为 Windows Forms 应用程序创建简化的异步调用模式》等文章中有详细描述。
Whidbey 将在 System.ComponentModel
命名空间中引入一个名为 BackgroundWorker
的组件,它封装了异步调用的复杂性,并提供了一个简单的组件,可以从代码调用,也可以拖放到设计器中的 Form 上。它公开了用于执行异步函数、更新进度、取消执行和完成执行的方法和事件。
如上所述,Joval Löwy 和 Roy Osherove 已经用 C# 为 .NET v1.1 实现了一个该类的版本,这对于许多人来说已经足够了。我已经将其移植到了 VB.NET,并进行了一些调整,以便在进行子类化时抛出与 Whidbey 版本类似的异常。然后我对其进行了扩展,并演示了如何使用它来异步调用 Web 服务。
移植到 VB.NET
这并非没有遇到一些奇怪的问题。
引发事件的最佳实践告诉我们,在引发事件之前,我们应该始终测试事件是否不为 null
。也就是说,事件必须有处理程序。所有文章都给出了类似这样的代码示例
if(SomeEvent != null)
{
SomeEvent(this, args);
}
BackgroundWorker
的 C# 实现都使用了这种标准。但在 VB.NET 中该如何实现呢?如果您尝试执行语句 SomeEvent != Nothing
,您会收到错误:'Public Event SomeEvent(sender As Object, e As System.EventArgs)' is an event, and cannot be called directly. Use a 'RaiseEvent' statement to raise an event.
这没什么用!
事实证明,在 VB.NET 中,要访问派生的代理对象以测试它是否为 Nothing
,只需在事件名称的末尾添加“Event
”。但请不要在 IntelliSense 中查找它,因为它不会在那里!生成的 VB.NET 代码是
if Not SomeEventEvent is Nothing Then
SomeEventEvent(Me, args)
End If
为什么我使用了 SomeEventEvent(Me, args)
而不是 RaiseEvent SomeEvent(Me, args)
?
委托的默认方法是 Invoke
方法,而 VB.NET 中的 RaiseEvent
只是在该委托上调用此方法,这使得我的代码语言无关性更强。这更多是个人偏好。
注意:源代码下载包含 VB.NET 移植版本和我创建的子类。
子类化 BackgroundWorker
这有两个目的。首先,当 Whidbey 发布时,切换到调用本机 BackgroundWorker
将只需要在子类中进行八处代码更改,而不是在我的整个应用程序中进行大量更改。其次,它使我们能够向 BackgroundWorker
添加功能,在这种情况下,就是能够向异步函数传递多个参数。
首先,三个事件参数类,DoWorkEventArgs
、ProgressChangedEventArgs
和 RunWorkerCompletedEventArgs
都必须进行子类化,以及它们相关的处理程序委托。然后 BackgroundWorker
类本身就可以进行子类化。
我们必须处理底层事件并抛出我们子类化的版本。
Public Shadows Event DoWork As DoWorkEventHandler
Public Shadows Event ProgressChanged As ProgressChangedEventHandler
Public Shadows Event RunWorkerCompleted As RunWorkerCompletedEventHandler
DoWork
事件是最简单的,尽管您需要小心地将子类化的事件参数重新分配给原始事件参数。
Private Sub BackgroundWorker_DoWork(ByVal sender As Object, _
ByVal e As VS2005.DoWorkEventArgs) Handles MyBase.DoWork
If Not DoWorkEvent Is Nothing Then
Dim args As New DoWorkEventArgs(e.Argument)
DoWorkEvent(sender, args)
e.Cancel = args.Cancel
If Not e.Cancel Then
e.Result = args.Result
End If
End If
End Sub
然而,ProgressChanged
事件必须调用我们移植的 BackgroundWorker
中的 ProcessDelegate
函数的副本。
Private Sub BackgroundWorker_ProgressChanged(ByVal sender As Object, _
ByVal e As VS2005.ProgressChangedEventArgs) Handles MyBase.ProgressChanged
If Not ProgressChangedEvent Is Nothing Then
Me.ProcessDelegate(ProgressChangedEvent, Me, _
New ProgressChangedEventArgs(e.ProgressPercentage, Nothing))
End If
End Sub
这是因为 Whidbey 版本没有这个方法,所以如果我们调用基类函数,那么我们将失去轻松升级的能力。
然而最糟糕的是 RunWorkerCompleted
事件。RunWorkerCompletedEventArgs
类在访问 Result
属性且该属性将返回 null 时会抛出异常,我们必须解决这个问题才能将事件参数传递到我们子类化的事件参数中。微软已经为 Whidbey 更改了委托的一些内部工作方式。
在 .NET 1.1 中,如果我们不处理异常,它会导致回调委托被调用两次。第二次,会因为无法调用 EndInvoke
两次而抛出新的异常。
在 Whidbey 中,异常会沿着调用堆栈冒泡,直到被处理。
为了解决这个问题,我们只在未取消或未出错的情况下读取 Result
属性的值。
Private Sub BackgroundWorker_RunWorkerCompleted(ByVal sender As Object, _
ByVal e As VS2005.RunWorkerCompletedEventArgs) _
Handles MyBase.RunWorkerCompleted
If Not RunWorkerCompletedEvent Is Nothing Then
Dim result As Object = Nothing
If e.Cancelled = False AndAlso e.Error Is Nothing Then
result = e.Result
End If
Me.ProcessDelegate(RunWorkerCompletedEvent, Me, _
New RunWorkerCompletedEventArgs(result, e.Error, e.Cancelled))
End If
End Sub
最后,我们想扩展这个类。我们只需重载 RunWorkerAsync
方法,并向其传递一个类型为 Object()
的 ParamArray
。
Public Overloads Sub RunWorkerAsync(ByVal ParamArray arguments As Object())
MyBase.RunWorkerAsync(arguments)
End Sub
Using the Code
使用此类进行基本的异步调用很简单,只需连接 DoWork
、ProgressChanged
和 RunWorkerCompleted
事件,然后调用 RunWorkerAsync
方法,就像我在演示应用程序中展示的那样。
然而,进行异步 Web 服务调用有点奇怪。您以与正常情况相同的方式连接事件,并在 DoWork
事件处理程序中,**同步**调用 Web 服务。这正是调用代理类自动生成的 BeginWebMethod
/EndWebMethod
方法时所发生的情况。它会创建一个新线程并在那里进行常规的同步调用。
当然,异步线程在调用期间将被锁定,因此您需要在返回时检查是否有取消挂起。您的 RunWorkerCompleted
处理程序必须确保仅当 cancelled 为 False
时才执行任何操作。
在数据开始返回之前,无法监视 Web 服务调用的进度,届时您可以监视接收数据流的进度。然而,为了演示的目的,我只是设置了一个计时器,每秒更新十次进度条。这足以让用户认为正在发生一些事情,而确实也正在发生,只是不在他们的机器上!
历史
- 2004 年 7 月 15 日:初始版本