One of the basic building blocks in .NET is the delegate. Delegates are often described as type-safe function pointers. They are function pointers because they enable you to store a reference that "points" to a class method. They are type-safe because every delegate is defined with a fixed method signature. For example, you might create a delegate variable that can point to any method that takes two string parameters and returns an integer value. This is the delegate signature. You can't use this delegate to store a reference to a method with a different signature (for example, one that takes two string parameters and has no return value). The two core delegate classes are as follows:
Suppose, for example, that you have a project that contains the following object: Public Class ProductList Public Function GetTotal() As Decimal ' (Code omitted.) End Function End Class To create a delegate that points to GetTotal, you need to define the function signature. We'll call this GetTotalDelegate. Typically, you define this delegate inside the class consumer. Public Delegate Function GetTotalDelegate() As Decimal Now you can create a delegate variable based on GetTotalDelegate that can point to any function that accepts no parameters and returns a decimal result. Listing 6-1 is a code example that creates a ProductList object and a GetTotalDelegate that points to the appropriate GetTotal method. Listing 6-1 Invoking a method through a delegate' Declare the object. Dim List As New ProductList() ' Declare the delegate variable. Dim GetTotalPointer As New GetTotalDelegate(AddressOf List.GetTotal) ' Call List.GetTotal() through the delegate. ' This is the line that invokes the delegate. Dim Total As Decimal Total = GetTotalPointer() So far, this is only an example of .NET basics. However, delegates are more interesting under the hood because they include built-in support for asynchronous execution. Essentially, it works like this: Every time you define a delegate, .NET generates a custom delegate class. When you create a delegate and "point" it to a method, the custom delegate object stores a reference to the appropriate method. When you invoke the delegate, you are actually using the Invoke method in the custom delegate class. The Invoke method provides synchronous execution, but the delegate class also includes two other methods BeginInvoke and EndInvoke that enable you to call the referenced method asynchronously on another thread. These asynchronous methods don't work the same as the basic Invoke method. BeginInvoke returns immediately, allowing your code to continue its work. BeginInvoke also returns a special IAsyncResult object that you can examine to determine when the asynchronous operation is complete. To pick up the results at a later time, you submit this IAsyncResult object to the EndInvoke method, which waits for the operation to complete if it hasn't already finished and returns the information in which you're interested. Listing 6-2 shows the same code rewritten to use asynchronous delegates. Listing 6-2 Invoking a method asynchronously through a delegate' Declare the object. Dim List As New ProductList() ' Declare the delegate variable. Dim GetTotalPointer As New GetTotalDelegate(AddressOf List.GetTotal) ' Call List.GetTotal() through BeginInvoke(). ' Note that BeginInvoke always takes two extra parameters, and ' returns immediately. Dim AsyncResult As IAsyncResult AsyncResult = GetTotalPointer.BeginInvoke(Nothing, Nothing) ' (Perform some other work while the GetTotal() method is executing.) ' Retrieve the final result using the IAsyncResult. Dim Total As Decimal = GetTotalPointer.EndInvoke(AsyncResult) Figure 6-3 diagrams how this code works. Figure 6-3. Calling a method asynchronously with a delegate
If the GetTotal method hasn't finished by the time you call EndInvoke, your code just waits for it to finish. In some cases, you might want to know whether the method is actually complete (and ready for pickup) before you call EndInvoke so that you won't risk a long wait. You can make this determination by examining the IsCompleted property of the IAsyncResult object, as shown in Listing 6-3. Checking this information repeatedly (for example, in a loop) is known as polling. Part of the reason this delegate approach is so straightforward is because the asynchronous method doesn't have any other dependencies. It doesn't need to communicate its result or read information from another object. Thanks to this independence, you won't need to worry about synchronization or concurrency problems. Listing 6-3 Polling to determine when an asynchronous method is completeDim List As New ProductList() Dim GetTotalPointer As New GetTotalDelegate(AddressOf List.GetTotal) ' Call List.GetTotal() through BeginInvoke(). Dim AsyncResult As IAsyncResult AsyncResult = GetTotalPointer.BeginInvoke(Nothing, Nothing) ' Loop until the method is complete. Do Until AsyncResult.IsCompleted ' (Perform some additional work here.) Loop ' Retrieve the final result. Dim Total As Decimal = GetTotalPointer.EndInvoke(AsyncResult) Table 6-1 shows the full set of IAsyncResult properties. Note that polling is rarely the most efficient approach. Generally, it's a better idea to use a callback or a wait handle. We'll look at these options parameters later in this chapter.
Note You won't find Invoke, BeginInvoke, and EndInvoke in the class library reference for the System.Delegate type because these methods aren't a part of the basic delegate types. They are methods that .NET creates automatically when you define a delegate. (Technically, they are part of the custom delegate class that .NET builds behind the scenes.) If you think about it, you'll realize that this is a necessary fact of life because the BeginInvoke and EndInvoke have different signatures depending on the underlying method they are calling. For example, BeginInvoke always takes the parameters of the original method, along with two extra parameters, and EndInvoke always uses the return value of the original method. Thus, there's no way that they can be defined generically. Asynchronous Remoting CallsWith .NET Remoting, the process is exactly the same. You can create a delegate variable that points to the method of a remote object and call it by using BeginInvoke. Technically, you aren't calling the remote object asynchronously you're calling the transparent proxy asynchronously. (Remember, the transparent proxy is created automatically by the .NET Remoting infrastructure to manage communication between components in different application domains.) The proxy waits for the response from the remote object on a separate thread, as shown in Figure 6-4. Figure 6-4. An asynchronous call to a remote component
Asynchronous Web Service CallsIt is possible to use delegates in much the same way with XML Web services, by creating a delegate variable that points to a proxy class method and calling this delegate asynchronously. However, you don't need to go to this extra work. When .NET generates a proxy class, it automatically adds additional methods that allow the XML Web service to be called asynchronously. Suppose, for example, that you have the following Web method: <WebMethod> _ Public Function GetStockQuote(ByVal Ticker As String) As Decimal ' (Code omitted.) End Function The proxy class will include a corresponding GetStockQuote method that calls this method synchronously. However, the proxy class will also include BeginGetStockQuote and EndGetStockQuote methods that call the Web service asynchronously. Listing 6-4 shows these three methods in the proxy class. Listing 6-4 Asynchronous support in the Web service proxy classPublic Function GetStockQuote(ByVal Ticker As String) As Decimal Dim results() As Object = Me.Invoke("GetStockQuote", _ New Object() {Ticker}) Return CType(results(0), Decimal) End Function Public Function BeginGetStockQuote(ByVal Ticker As String, _ ByVal callback As AsyncCallback, ByVal asyncState As Object) _ As IAsyncResult Return Me.BeginInvoke("GetStockQuote", _ New Object() {Ticker}, callback, asyncState) End Function Public Function EndGetStockQuote(ByVal asyncResult As IAsyncResult) _ As Decimal Dim results() As Object = Me.EndInvoke(asyncResult) Return CType(results(0), Decimal) End Function These methods mirror the BeginInvoke and EndInvoke methods. BeginStockQuote accepts the original string parameter, plus two optional ones, and returns an IAsyncResult instance. EndGetStockQuote accepts the IAsyncResult object and returns the GetStockQuote decimal return value. To provide this magic, you'll notice that the proxy class code calls its own BeginInvoke method. This is actually one of the methods inherited from the base class SoapHttpClientProtocol (found in the System.Web.Services.Protocols namespace). All proxy classes that use SOAP communication derive from this class, and so they all have the ability to call Web methods asynchronously. |