|
|||||||||||||||||||||||
|
|||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
Contents
IntroductionAsyncGen is a utility that generates client classes implementing the event-based asynchronous pattern from annotated classes and interfaces such as the following... using System;
using System.Collections.Generic;
using System.Text;
using AsyncGen;
namespace TestAssembly
{
[GenerateAsyncClientClass("Client")]
public interface IServer
{
[GenerateAsyncOperation(
GenerateCancelMethod = true,
CompletedEventName = "CalculationComplete",
CallbackInterface = typeof(IServerCallbacks))]
double Calculate(double argument,
[TaskID] object userState, IServerCallbacks callbacks);
}
public interface IServerCallbacks
{
[GenerateProgressEvent("CalculationProgressChanged")]
[return:CancelFlag] bool ReportCalculationProgress
(double approximateResult, [ProgressPercentage] int percentage,
[TaskID] object userState);
}
}
... from which AsyncGen will generate the following client class:
A note on terminology: For the remainder of this article, I shall use the terms "client class" and "proxy" interchangeably. BackgroundSince its inception, the .NET framework provided extensive support for asynchronous invocation of operations. Many methods in the .NET BCL (Base Class Library) have asynchronous versions in the form of a pair of methods named In addition to the asynchronous methods provided by the BCL, the asynchronous delegates mechanism allows you to invoke any method asynchronously. The The
For these reasons, a new pattern was introduced in .NET 2.0, known as the Event-Based Asynchronous Pattern, which offers support for all of the above. (See also Asynchronous Programming Design Patterns on MSDN.) So far, the closest thing to a universal implementation of the event-based pattern was the BackgroundWorker class, which allows you to run an arbitrary operation in the background without blocking your main thread, and still be able to receive notifications on the main thread when the operation completes and when there is progress. However, even AsyncGen attempts to provide a universal implementation of the event-based pattern, including all the features described in the MSDN article. To make it suitable for distributed scenarios, AsyncGen employs a client/server approach, which means that the details of the asynchronous invocation are not handled by the same class that implements the logic of the operation, but by a proxy class that uses the original class as a server. The proxy can reside either in the same process as the server or in a separate process. In the latter case, the proxy and the server communicate with each other using .NET Remoting. Internally, the proxy uses asynchronous delegates to implement the operations. Using AsyncGenOverviewTo use AsyncGen, you have to follow these steps:
Command Line SyntaxSynopsisAsyncGen.exe [ options ] <assembly containing annotated classes>
[ <additional assembly needed to compile the generated classes> ... ]
Options/lang:<output language>
The valid values for <output language> are:
The code is generated in the current directory, each class in a separate source file, named class.generated.cs. Note: When you run AsyncGen.exe, make sure that AsyncGenLib.dll is in the current directory. This requirement is easily fulfilled by running AsyncGen.exe in the bin\Debug or bin\Release directory of your project. Annotating Your ClassesBefore you use AsyncGen.exe, you must annotate your source classes and interfaces in a style similar to WCF services, using the following attributes from AsyncGenLib.dll: GenerateAsyncClientClassAttributePlace this attribute on every class or interface for which you want to generate an asynchronous proxy. If you don't specify any constructor arguments or named properties, the name of the proxy class will be the same as the name of the source class or interface with the word 'Client' appended and the leading 'I' stripped from interface names. GenerateAsyncOperationAttributePlace this attribute on every method for which you want to implement the pattern. It has several properties that can be used to customize the names of the generated members, such as the start method and the completion event. One property deserves special attention: the GenerateProgressEventAttributeThis attribute is used on the methods of the callback interface to indicate the type of event that should be raised by each method. TaskIDAttribute, ProgressPercentageAttribute, and CancelFlagAttributeThese attributes are used to identify method parameters that have special significance in the implementation of the event-based pattern. They don't have any constructor arguments or named properties. Callback InterfacesWhile the completion event is automatically raised by the proxy when the server method returns (either normally or abnormally), progress events must be raised by the server itself. This is done using a callback interface. Unlike WCF, which passes the callback interface implicitly to the server, AsyncGen requires the callback interface to be explicitly defined as one of the parameters of the original method, preferably the last one. The callback interface's methods are tied to specific events on the proxy through the You don't have to implement the callback interface. It is automatically implemented by the generated code, as you can see in the following example: // --- Source file: IServer.cs ---
[GenerateAsyncClientClass("Client")]
public interface IServer
{
[GenerateAsyncOperation(CallbackInterface = typeof(IServerCallbacks))]
double Calculate(double argument, [TaskID] object userState,
IServerCallbacks callbacks);
}
public interface IServerCallbacks
{
[GenerateProgressEvent("CalculationProgressChanged")]
[return:CancelFlag] bool ReportCalculationProgress
(double approximateResult, [ProgressPercentage] int percentage,
[TaskID] object userState);
}
// --- Source file: Client.generated.cs ---
public partial class Client : ClientBase<TestAssembly.IServer>
{
// ...
public void CalculateAsync(double argument, object userState)
{
this._calculateTracker.CreateOperation(userState);
CalculateDelegate d = new CalculateDelegate(this.server.Calculate);
d.BeginInvoke(argument, userState, this._calculateTracker,
new System.AsyncCallback(this._calculateTracker.PostOperationCompleted),
userState);
}
// ...
public event AsyncGen.ProgressChangedEventHandler<double> CalculationProgressChanged;
// ...
private class CalculateTracker : OperationTracker<IServer, Client,
CalculateDelegate, double>, TestAssembly.IServerCallbacks
{
// ...
bool IServerCallbacks.ReportCalculationProgress
(double approximateResult, int percentage, object userState)
{
this.PostProgress<double>(new System.Threading.SendOrPostCallback
(this.OnCalculationProgressChanged), percentage,
approximateResult, userState);
return this.IsOperationCancelled(userState);
}
}
// ...
}
When the server calls
Note the absence of the callback parameter from CancellationBy default, AsyncGen doesn't generate a Cancel method. If your server supports cancellation, you should set the The ability to cancel an operation requires the server to poll a boolean flag which can be raised by the client at any point during the lifetime of the operation. You have two options here:
Overloaded MethodsThe most trivial use of overloading in C# is to provide default values for one or more arguments. Typically in this scenario, the method has one main overload which takes all the possible parameters and implements the logic of the operation, and several simple overloads that call the main one and supply default values for some of the arguments. In the following code, for instance, the first overload of using System;
using System.Collections.Generic;
using System.Text;
using AsyncGen;
namespace TestAssembly
{
[GenerateAsyncClientClass("OverloadedClient")]
public class Server
{
[GenerateAsyncOperation]
public int DoSomething(int m)
{
return DoSomething(m, 17);
}
[GenerateAsyncOperation]
public int DoSomething(int m, int n)
{
return 42;
}
}
}
When you generate the asynchronous proxy for this class, you would probably want it to contain two methods named There are two ways to achieve the desired result:
Note that When more than one overload of the same method implements complex logic, you must take the second approach. Caveat: If the overloads have different Synchronous InvocationThe generated proxy also contains a synchronous version of the server's method, without the callback interface parameter. If you want to invoke the operation synchronously, use this method instead of calling the server's method directly. Note that you can still raise progress events, but if you attempt to do this in a typical WinForms application you will run into one of the following problems, depending on the value of the
Still, there are several scenarios where a synchronously-invoked method can safely (and usefully) raise progress events:
This last scenario occurs in practice when you need to invoke an operation that consists of a sequence of stages, each of which has to report its progress to the user, without blocking the main thread. Lifetime ManagementWhen the proxy and the server communicate with each other via .NET Remoting, both of them are subject to lifetime management. If one of them goes offline before the other one is done using it, you will get the following exception: "Object ... has been disconnected or does not exist at the server." To ensure that the server doesn't go offline prematurely, you should set its lifetime limits by doing one of the following:
To ensure that the proxy doesn't go offline before the server does, your server should sponsor its proxies. If your server implements the ISponsor interface, it will automatically be registered by the proxy as its sponsor. class Server : MarshalByRefObject, ISponsor
{
TimeSpan ISponsor.Renewal(ILease lease)
{
return TimeSpan.FromMinutes(2);
}
}
To learn more about lifetime management, see [1]. The Demo Project
To see how changes to the annotated types affect the generated code, use TestAssembly.csproj in AsyncGen.sln. Since the Debug settings are not saved in the *.csproj file, you will have to configure the Start Options yourself:
Once you complete this configuration, pressing F5 or Ctrl+F5 in Visual Studio will compile TestAssembly.csproj and run AsyncGen.exe on TestAssembly.dll. To see how the proxy works in a distributed application, open the demo solution, EventBasedAsync.sln. This solution contains three projects:
Again, you have to configure some settings which are not saved in the *.sln and *.csproj files:
When you press F5 or Ctrl+F5 in Visual Studio, both the server and the client come up. When you press "Submit Request(s)," the client sends one or more simultaneous requests to the server, depending on the number in the "How many?" box. For each submitted request, a new row is added to the In addition to demonstrating the functionality of the proxies generated by AsyncGen, this project also demonstrates the behavior of the .NET thread pool. The server provides a primitive command line interface to allow you to configure its thread pool's size limits. The following commands are available:
In a distributed application, the client's requests are handled by threads from the completion port portion of the server's thread pool, so the number of worker threads has no effect on the performance of the application. If, on the other hand, you move the server into the same AppDomain as the client, the requests will be handled by worker threads. In this case, you should see that the performance is only affected by the number of worker threads and not by the number of completion port threads. Design and ImplementationAsyncGenLib.dllOther than the attribute classes described above, AsyncGenLib.dll contains several classes that facilitate the implementation of the generated proxies:
How the Client Class is ImplementedFor each annotated method, AsyncGen.exe generates two
Internally, each If a method is overloaded, each overload gets its own tracker class and a corresponding field. The names of these members are constructed using the base name specified for each overload, so the base names must be different for each method. (See "Handling Overloaded Methods" above). The program ensures that if multiple overloads specify the same name for any of the following members, only one member will actually be added to the main class:
The tracker class, which is As an elegant alternative to relaying the event through a handler in the main class, it is possible for the main class to define for every Points of Interest.NET Remoting.NET Remoting supports asynchronous invocation via IMessageSink.AsyncProcessMessage and several similar methods, which are implemented by every stage ("message sink," in Remoting jargon) of the Remoting pipeline, or "sink chain." The same client code can handle both local servers (objects hosted by the local AppDomain) and remote servers (objects hosted by a different AppDomain, process, or machine). For example, consider the following code: DoSomethingDelegate d = new DoSomethingDelegate(server.DoSomething);
d.BeginInvoke(new AsyncCallbac(server_DoSomethingCompleted), this);
Normally, It is important to note that the underlying connection will still be synchronous. This means that separate connections will be made for each concurrently running asynchronous call.[1] In effect, when the server resides in a different process, the operation is performed using the server's thread pool. On the client side, only one thread per channel is blocked rather than one thread for every concurrent invocation of the operation. The client's thread pool is only used for executing the callbacks. Ideally, the callbacks should return quickly enough to avoid using more than one thread simultaneously, which in .NET 2.0 means they have to return within 500 milliseconds (see .NET's ThreadPool Class - Behind The Scenes by Marc Clifton). WCFWCF has an analogous mechanism, but instead of delegates (which are hard-wired to .NET Remoting) you have to use SvcUtil with the /async switch to generates two additional methods in the proxy class:[2] [OperationContract(AsyncPattern = true,
Action = "<original action name>",
ReplyAction = "<original response name>")]
IAsyncResult Begin<Operation>(<in arguments>,
AsyncCallback callback, object asyncState);
<returned type> End<Operation>(<out arguments>, IAsyncResult result);
Currently AsyncGen doesn't support WCF. Adding such support will require some extensions to AsyncGenLib.dll, and AsyncGen.exe will either have to invoke SvcUtil /async itself or rely on the pre-existence of WCF asynchronous proxies for all the processed types. If you implement the event-based pattern using WCF, you might also want to take advantage of WCF's built-in support for callback interfaces. Complete ExampleHere is the code generated from the C#//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:2.0.50727.1433
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace TestAssembly
{
using System;
using System.ComponentModel;
using System.Diagnostics;
using AsyncGen;
public partial class Client : ClientBase<TestAssembly.IServer>
{
private CalculateTracker _calculateTracker;
public Client(TestAssembly.IServer server) :
base(server)
{
this._calculateTracker = new CalculateTracker(this.server, this);
this._calculateTracker.OperationCompleted +=
new AsyncGen.AsyncCompletedEventHandler<double>
(this._calculateTracker_OperationCompleted);
this._calculateTracker.CalculationProgressChanged +=
new AsyncGen.ProgressChangedEventHandler<double>
(this._calculateTracker_CalculationProgressChanged);
}
public event AsyncGen.AsyncCompletedEventHandler<double> CalculationComplete;
public event AsyncGen.ProgressChangedEventHandler<double>
CalculationProgressChanged;
public double Calculate(double argument, object userState)
{
double value;
this._calculateTracker.CreateOperation(userState);
try
{
value = this.server.Calculate(argument, userState,
this._calculateTracker);
}
finally
{
this._calculateTracker.CompleteOperation(userState);
}
return value;
}
public void CalculateAsync(double argument, object userState)
{
this._calculateTracker.CreateOperation(userState);
CalculateDelegate d = new CalculateDelegate(this.server.Calculate);
d.BeginInvoke(argument, userState, this._calculateTracker,
new System.AsyncCallback(this._calculateTracker.PostOperationCompleted),
userState);
}
public void CalculateAsyncCancel(object userState)
{
if (this._calculateTracker.TryCancelOperation(userState))
{
return;
}
throw new System.ArgumentException();
}
private void _calculateTracker_CalculationProgressChanged
(object sender, AsyncGen.ProgressChangedEventArgs<double> args)
{
if ((this.CalculationProgressChanged != null))
{
this.CalculationProgressChanged(this, args);
}
}
private void _calculateTracker_OperationCompleted
(object sender, AsyncGen.AsyncCompletedEventArgs<double> args)
{
if ((this.CalculationComplete != null))
{
this.CalculationComplete(this, args);
}
}
private class CalculateTracker : OperationTracker<IServer, Client,
CalculateDelegate, double>, TestAssembly.IServerCallbacks
{
public CalculateTracker(IServer server, Client client) :
base(server, client)
{
}
public event AsyncGen.ProgressChangedEventHandler<double>
CalculationProgressChanged;
protected override void CallEndInvoke(CalculateDelegate d,
System.IAsyncResult iar, out double output)
{
output = d.EndInvoke(iar);
}
protected virtual void OnCalculationProgressChanged(object args)
{
if ((this.CalculationProgressChanged != null))
{
this.CalculationProgressChanged(this.client,
((AsyncGen.ProgressChangedEventArgs<double>)(args)));
}
}
bool IServerCallbacks.ReportCalculationProgress
(double approximateResult, int percentage, object userState)
{
this.PostProgress<double>(new System.Threading.SendOrPostCallback
(this.OnCalculationProgressChanged), percentage,
approximateResult, userState);
return this.IsOperationCancelled(userState);
}
}
delegate double CalculateDelegate(double argument, object userState,
TestAssembly.IServerCallbacks callbacks);
}
}
Visual Basic'------------------------------------------------------------------------------
' <auto-generated>
' This code was generated by a tool.
' Runtime Version:2.0.50727.1433
'
' Changes to this file may cause incorrect behavior and will be lost if
' the code is regenerated.
' </auto-generated>
'------------------------------------------------------------------------------
Option Strict Off
Option Explicit On
Imports TestAssembly
Imports AsyncGen
Imports System
Imports System.ComponentModel
Imports System.Diagnostics
Namespace TestAssembly
Partial Public Class Client
Inherits ClientBase(Of IServer)
Private _calculateTracker As CalculateTracker
Public Sub New(ByVal server As IServer)
MyBase.New(server)
Me._calculateTracker = New CalculateTracker(Me.server, Me)
AddHandler Me._calculateTracker.OperationCompleted, _
AddressOf Me._calculateTracker_OperationCompleted
AddHandler Me._calculateTracker.CalculationProgressChanged, _
AddressOf Me._calculateTracker_CalculationProgressChanged
End Sub
Public Event CalculationComplete As _
AsyncGen.AsyncCompletedEventHandler(Of Double)
Public Event CalculationProgressChanged As _
AsyncGen.ProgressChangedEventHandler(Of Double)
Public Function Calculate(ByVal argument As Double, _
ByVal userState As Object) As Double
Dim value As Double
Me._calculateTracker.CreateOperation(userState)
Try
value = Me.server.Calculate(argument, userState, Me._calculateTracker)
Finally
Me._calculateTracker.CompleteOperation(userState)
End Try
Return value
End Function
Public Sub CalculateAsync(ByVal argument As Double, ByVal userState As Object)
Me._calculateTracker.CreateOperation(userState)
Dim d As CalculateDelegate = AddressOf Me.server.Calculate
d.BeginInvoke(argument, userState, Me._calculateTracker, _
AddressOf Me._calculateTracker.PostOperationCompleted, userState)
End Sub
Public Sub CalculateAsyncCancel(ByVal userState As Object)
If Me._calculateTracker.TryCancelOperation(userState) Then
Return
End If
Throw New System.ArgumentException
End Sub
Private Sub _calculateTracker_CalculationProgressChanged_
(ByVal sender As Object, ByVal args As _
AsyncGen.ProgressChangedEventArgs(Of Double))
RaiseEvent CalculationProgressChanged(Me, args)
End Sub
Private Sub _calculateTracker_OperationCompleted(ByVal sender As Object, _
ByVal args As AsyncGen.AsyncCompletedEventArgs(Of Double))
RaiseEvent CalculationComplete(Me, args)
End Sub
Private Class CalculateTracker
Inherits OperationTracker(Of IServer, Client, CalculateDelegate, Double)
Implements IServerCallbacks
Public Sub New(ByVal server As IServer, ByVal client As Client)
MyBase.New(server, client)
End Sub
Public Event CalculationProgressChanged As _
AsyncGen.ProgressChangedEventHandler(Of Double)
Protected Overrides Sub CallEndInvoke(ByVal d As CalculateDelegate, _
ByVal iar As System.IAsyncResult, ByRef output As Double)
output = d.EndInvoke(iar)
End Sub
Protected Overridable Sub OnCalculationProgressChanged(ByVal args As Object)
RaiseEvent CalculationProgressChanged(Me.client, CType_
(args,AsyncGen.ProgressChangedEventArgs(Of Double)))
End Sub
Public Function ReportCalculationProgress_
(ByVal approximateResult As Double, ByVal percentage As Integer, _
ByVal userState As Object) As Boolean Implements _
IServerCallbacks.ReportCalculationProgress
Me.PostProgress(Of Double)(AddressOf Me.OnCalculationProgressChanged, _
percentage, approximateResult, userState)
Return Me.IsOperationCancelled(userState)
End Function
End Class
Delegate Function CalculateDelegate(ByVal argument As Double, _
ByVal userState As Object, ByVal callbacks As IServerCallbacks) As Double
End Class
End Namespace
C++/CLI// --- Source file: Client.h ---
#pragma once
#using <mscorlib.dll>
using namespace System::Security::Permissions;
[assembly:SecurityPermissionAttribute(SecurityAction::RequestMinimum,
SkipVerification=false)];
namespace TestAssembly {
using namespace System;
using namespace System::ComponentModel;
using namespace System::Diagnostics;
using namespace AsyncGen;
using namespace System;
ref class Client;
public ref class Client : public ClientBase<TestAssembly::IServer^ >
{
private : ref class CalculateTracker;
private: TestAssembly::Client::CalculateTracker^ _calculateTracker;
private : delegate System::Double CalculateDelegate
(System::Double argument, System::Object^ userState,
TestAssembly::IServerCallbacks^ callbacks);
public: event AsyncGen::AsyncCompletedEventHandler
<System::Double >^ CalculationComplete;
public: event AsyncGen::ProgressChangedEventHandler
<System::Double >^ CalculationProgressChanged;
public: Client(TestAssembly::IServer^ server);
public: System::Double Calculate
(System::Double argument, System::Object^ userState);
public: System::Void CalculateAsync
(System::Double argument, System::Object^ userState);
public: System::Void CalculateAsyncCancel(System::Object^ userState);
private: System::Void _calculateTracker_CalculationProgressChanged
(System::Object^ sender,
AsyncGen::ProgressChangedEventArgs<System::Double >^ args);
private: System::Void _calculateTracker_OperationCompleted
(System::Object^ sender,
AsyncGen::AsyncCompletedEventArgs<System::Double >^ args);
private : ref class CalculateTracker :
public OperationTracker<IServer^, TestAssembly::Client^,
TestAssembly::Client::CalculateDelegate^, System::Double >,
public TestAssembly::IServerCallbacks
{
public: event AsyncGen::ProgressChangedEventHandler
<System::Double >^ CalculationProgressChanged;
public: CalculateTracker(IServer^ server, TestAssembly::Client^ client);
protected: virtual System::Void CallEndInvoke
(TestAssembly::Client::CalculateDelegate^ d,
System::IAsyncResult^ iar, System::Double %output) override;
protected: virtual System::Void OnCalculationProgressChanged
(System::Object^ args);
public: virtual System::Boolean ReportCalculationProgress
(System::Double approximateResult, System::Int32 percentage,
System::Object^ userState) sealed;
};
};
}
// --- Source file: Client.cpp ---
#include "StdAfx.h"
#include "Client.h"
namespace TestAssembly
{
inline Client::Client(TestAssembly::IServer^ server) :
ClientBase<TestAssembly::IServer^ >(server)
{
this->_calculateTracker = (gcnew TestAssembly::Client::CalculateTracker
(this->server, this));
this->_calculateTracker->OperationCompleted +=
gcnew AsyncGen::AsyncCompletedEventHandler<System::Double >
(this, &TestAssembly::Client::_calculateTracker_OperationCompleted);
this->_calculateTracker->CalculationProgressChanged +=
gcnew AsyncGen::ProgressChangedEventHandler<System::Double >(this,
&TestAssembly::Client::_calculateTracker_CalculationProgressChanged);
}
inline System::Double Client::Calculate(System::Double argument,
System::Object^ userState)
{
System::Double __identifier(value);
this->_calculateTracker->CreateOperation(userState);
try
{
__identifier(value) = this->server->Calculate
(argument, userState, this->_calculateTracker);
}
finally
{
this->_calculateTracker->CompleteOperation(userState);
}
return __identifier(value);
}
inline System::Void Client::CalculateAsync(System::Double argument,
System::Object^ userState)
{
this->_calculateTracker->CreateOperation(userState);
TestAssembly::Client::CalculateDelegate^
d = gcnew TestAssembly::Client::CalculateDelegate
((cli::safe_cast<TestAssembly::IServer^ >(this->server)),
&TestAssembly::IServer::Calculate);
d->BeginInvoke(argument, userState, this->_calculateTracker,
gcnew System::AsyncCallback(this->_calculateTracker,
&TestAssembly::Client::CalculateTracker::PostOperationCompleted),
userState);
}
inline System::Void Client::CalculateAsyncCancel(System::Object^ userState)
{
if (this->_calculateTracker->TryCancelOperation(userState))
{
return;
}
throw (gcnew System::ArgumentException());
}
inline System::Void Client::_calculateTracker_CalculationProgressChanged
(System::Object^ sender,
AsyncGen::ProgressChangedEventArgs<System::Double >^ args)
{
this->CalculationProgressChanged(this, args);
}
inline System::Void Client::_calculateTracker_OperationCompleted
(System::Object^ sender,
AsyncGen::AsyncCompletedEventArgs<System::Double >^ args)
{
this->CalculationComplete(this, args);
}
inline Client::CalculateTracker::CalculateTracker
(IServer^ server, TestAssembly::Client^ client) :
OperationTracker<IServer^, TestAssembly::Client^,
TestAssembly::Client::CalculateDelegate^,
System::Double >(server, client)
{
}
inline System::Void Client::CalculateTracker::CallEndInvoke
(TestAssembly::Client::CalculateDelegate^ d,
System::IAsyncResult^ iar, System::Double %output)
{
output = d->EndInvoke(iar);
}
inline System::Void Client::CalculateTracker::OnCalculationProgressChanged
(System::Object^ args)
{
this->CalculationProgressChanged(this->client,
(cli::safe_cast<AsyncGen::ProgressChangedEventArgs
<System::Double >^ >(args)));
}
inline System::Boolean Client::CalculateTracker::ReportCalculationProgress
(System::Double approximateResult, System::Int32 percentage,
System::Object^ userState)
{
this->PostProgress<System::Double >
(gcnew System::Threading::SendOrPostCallback(this,
&TestAssembly::Client::CalculateTracker::OnCalculationProgressChanged),
percentage, approximateResult, userState);
return this->IsOperationCancelled(userState);
}
}
History
References[1] Ingo Rammer, Advanced .NET Remoting (C# Edition), Apress © 2002 [2] Juval Löwy, Programming WCF Services, O'Reilly © 2007
| ||||||||||||||||||||||