One specific area in which you'll want to use asynchronous WinForms applications is when calling Web services. Calling a Web service is similar to passing a message between threads, except that Web services messages travel between machines using standard protocols such as HTTP and XML. [3] Imagine a .NET Web service that calculated digits of pi using some way cool fast pi calculation engine:
Class CalcPiService Inherits System.Web.Services.WebService <WebMethod> _ Function CalcPi(digits As Integer) As String ' Way cool fast pi calculator running on a huge processor... Return pi.ToString() End Function End Class Now imagine a version of the CalcPi program that used the Web service instead of our slow client-side algorithm to calculate pi on giant machines with huge processors (or even better, databases with more digits of pi cached than anyone could ever want or need). Although the underlying protocol of Web services is HTTP- and XML-based and we could form a Web service request fairly readily to ask for the digits of pi we're after, it's simpler to let VS.NET generate a class to make the Web services calls for you. You can do this in the Project menu using the Add Web Reference item. The Add Web Reference dialog allows you to enter the URL of the WSDL (Web Service Description Language) that describes the Web service you'd like to call. For example, after installing the Web service sample that accompanies this book, [4] you can access the WSDL via the following URL:
http://localhost/CalcPiWebService/CalcPiService.asmx?WSDL Accepting the WSDL in the Add Web Reference dialog will generate a client-side Web services proxy class , a helper class that turns your method calls into Web services messages. The generated proxy code for the CalcPi Web service looks like this: Namespace AsyncCalcPi.localhost <WebServiceBindingAttribute(Name="CalcPiServiceSoap", ...)> _ Class CalcPiService Inherits SoapHttpClientProtocol Public Sub New() Me.Url = _ http://localhost/CalcPiWebService/CalcPiService.asmx End Sub <SoapDocumentMethodAttribute(http://tempuri.org/CalcPi, ...)> _ Function CalcPi(digits As Integer) As String ... End Function Function BeginCalcPi(digits As Integer, _ callback As System.AsyncCallback, asyncState As Object) _ As IAsyncResult ... End Function Function EndCalcPi(asyncResult As IAsyncResult) As String ... End Function End Namespace Calling the Web service is now a matter of creating an instance of the proxy and calling the method you're interested in: Dim service As localhost.CalcPiServices = New localhost.CalcPiServices() Sub calcButton_Click(sender As Object, e As System.EventArgs) piTextBox.Text = service.CalcPi(CInt(digitsUpDown.Value)) End Sub Because Web services make calls across machine and often network boundaries, you should assume they'll take a long time, and, if called synchronously, they'll block the UI thread. You can use the standard techniques discussed in this chapter to call Web service methods asynchronously, but as you can tell in the generated proxy code, there's built-in support for asynchronous operations via the BeginXxx/EndXxx method pairs, one for each method on the Web service. The first step in retrofitting the sample application to use the Web service is to call the Web service proxy's BeginCalcPi method: Enum CalcState Pending Calculating Canceled End Enum Dim state As CalcState = CalcState.Pending Dim service As localhost.CalcPiService = New localhost.CalcPiService Sub calcButton_Click(sender As Object, e As EventArgs) Select Case state ' Start a new calculation Case CalcState.Pending ' Allow canceling state = CalcState.Calculating calcButton.Text = "Cancel" ' Start Web service request service.BeginCalcPi( _ CInt(digitsUpDown.Value), _ New AsyncCallBack(PiCalculated), _ Nothing) ' Cancel a running calculation Case CalcState.Calculating state = CalcState.Canceled calcButton.Enabled = False service.Abort() ' Fail all outstanding requests ' Shouldn't be able to press Calc button while it's canceling Case CalcState.Canceled Debug.Assert(False) End Select End Sub Sub PiCalculated(res As IAsyncResult) ... End Sub The BeginCalcPi method takes the method parameters and an instance of an AsyncCallback delegate. The application provides the PiCalculated event handler to match the AsyncCallback signature. The PiCalculated method, which will be called when the Web service returns, is responsible for harvesting results. Also, even though there is no way to get progress from a Web service (all the more reason to call it asynchronously), calls to a Web service can be canceled by a call to the Abort method. Be careful with this one, however, because it will cancel all outstanding requests, not just a specific one. When the AsyncCallback event handler is called, it means that the Web service has returned something: Sub PiCalculated(res As IAsyncResult) Try ShowPi(service.EndCalcPi(res)) Catch ex As WebException ShowPi(ex.Message) End Try End Sub The call to EndCalcPi, passing in the IAsyncResult parameter, provides any results of calling the Web service. If the Web service call was successful, the return value is pi. If, on the other hand, the Web service call was unsuccessful , because it timed out or was canceled, EndCalcPi will throw a WebException. That's why PiCalculated wraps the call to EndCalcPi in a try-catch block. ShowPi will be called on to show either the digits of pi that were calculated by the Web service or the exception message (the result of a canceled Web service call is shown in Figure 14.7). Figure 14.7. The Result of a Canceled Call to the Pi Web Service
The ShowPi method displays the results of the Web service invocation: Delegate Sub ShowPiDelegate(pi As String) Sub ShowPi(pi As String) If Me.InvokeRequired = False Then piTextBox.Text = pi state = CalcState.Pending calcButton.Text = "Calc" calcButton.Enabled = True Else Dim showPI As ShowPiDelegate = _ New ShowPiDelegate(AddressOf ShowPi) Me.BeginInvoke(showPi, New Object() {pi}) End If End Sub Notice that the ShowPi method contains the check to see whether an invoke is required, and it calls Control.BeginInvoke to transfer from the current thread to the UI thread, just as we've been doing for most of this chapter. This is necessary because the Web service completion notification will come in on a thread other than the UI thread. |