Surviving TPL, Delegates, Threads, and Invokes - winforms

I'm facing a serious problem of deadlock in a multithreaded desktop/windows application. I fear I'm not using the correct approach to delegates in a very async environment. Also, even though I "sink" my events into the calling UI thread, if possible, I still have to Invoke on the UI thread to see some action. Follows, is the details.
The application is basically a client for users of an online file storage service. That service exposes functionality through REST calls. I first created a managed code wrapper DLL for such calls that allows for a .NET consumer to create a static instance of this DLL and call the functions. I'll take the file upload operation as an example.
Now, in the wrapper, here is the public interface for a file upload:
public Int32 UploadFile(FileSystemObject FolderToUploadTo, FileInfo LocalFileAndPath, OperationProgressReportEventHandler onOperationProgressReport, FileSystemObjectUploadCompletedEventHandler onOperationCompleted) {
Int32 ReplyNumber = 0;
try {
var TheOperation = new UploadFileObjectOperation(FolderToUploadTo, LocalFileAndPath, _User.APIKey) {
onProgressReport = onOperationProgressReport,
onUploadCompleted = onOperationCompleted
};
//Add it to the pool of operations
OperationPool.Add(TheOperation);
//Start the operation through the factory
OperationFactory.StartNew(() => {
TheOperation.Start();
});
//Chain the *actual* TPL Task to flush after usage
TheOperation.InnerTask.ContinueWith(t => {
t.Dispose(); //Dispose the inner task
OperationPool.Remove(TheOperation); //Remove the operation from the pool
TheOperation = null; //Nullify the Operation
});
ReplyNumber = TheOperation.TaskId;
}
catch {
ReplyNumber = 0;
}
return ReplyNumber;
}
As you can see, the actual UI application, that will refer this DLL, will be sending delegates for progress and completed to the operation(s). Now, the body of the operation itself:
public class UploadFileObjectOperation : BaseOperation, IDisposable {
//Store
public FileSystemObjectUploadCompletedEventHandler onUploadCompleted;
//Constructors
//Disposing stuff
protected override void PerformWork() {
try {
//Init the WebClient
UploadClient.UploadProgressChanged += (UploadProgressChanged_s, UploadProgressChanged_e) => {
//This is my event in base class being raised
ReportProgress(UploadProgressChanged_e.ProgressPercentage, UploadProgressChanged_e);
};
UploadClient.UploadFileCompleted += (UploadFileCompleted_s, UploadFileCompleted_e) => {
if (UploadFileCompleted_e.Error != null) {
throw new ApplicationException("Upload failed. " + UploadFileCompleted_e.Error.Message);
}
JObject JSONLiveObject = JObject.Parse(Encoding.UTF8.GetString(UploadFileCompleted_e.Result));
if (String.Compare((String)JSONLiveObject["status"], Constants._CONST_RESTRESPONSE_STATUS_VALUE_FAIL, false) == 0) {
throw new ApplicationException("Upload response failed. " + (String)JSONLiveObject["result"]["message"]);
}
//Eureka! Success! We have an upload!
//This is my event being raised
UploadTaskCompleted(new UploadFileObjectOperationEventArg {
Error = null,
ResultSource = OperationResultSource.Fresh,
Status = OperationExitStatus.Success,
TaskId = TaskId,
UploadedFileSystemObject = _UploadedFile
});
};
//Start the async upload
UploadClient.UploadFileAsync(AddressOfRESTURI, UploadingMethod, _FileToUpload.FullName);
}
catch (OperationCanceledException exp_Canceled) {
UploadTaskCompleted(new UploadFileObjectOperationEventArg {
Error = exp_Canceled,
ResultSource = OperationResultSource.Fresh,
Status = OperationExitStatus.Canceled,
TaskId = TaskId,
UploadedFileSystemObject = _UploadedFile
});
// To ensure that the calling code knows the task was canceled
//throw;
}
catch (Exception exp) {
UploadTaskCompleted(new UploadFileObjectOperationEventArg {
Error = exp,
ResultSource = OperationResultSource.Fresh,
Status = OperationExitStatus.Error,
TaskId = TaskId,
UploadedFileSystemObject = _UploadedFile
});
// If the calling code also needs to know.
//throw;
}
}
protected void UploadTaskCompleted(UploadFileObjectOperationEventArg arg) {
if (onUploadCompleted == null)
return;
//Sinking into calling UI thread, if possible
if (onUploadCompleted.Target is Control) {
Control targetForm = onUploadCompleted.Target as Control;
targetForm.Invoke(onUploadCompleted, new object[] { arg });
}
else {
onUploadCompleted(arg);
}
Status = OperationRunningStatus.Completed;
}
}
The PerformWork() raises the two events: Progress reporting and completion. Note that while raising an event, I check if can get a route to the calling thread and push the event directly so to avoid invokes at UI.
Now, lets see how I'm using all of the above in a desktop client:
private void UploadFile(FileInfo DraggedFileInfo, FileSystemObject ParentDefination) {
SessionLifetimeStuff.APICore.UploadFile(ParentDefination, DraggedFileInfo,
(PercentageCompleted) => {
#region Progress
this.InvokeEx(f => {
UpdateTaskProgress(newTaskQueue.OID, PercentageCompleted.Progress, PercentageCompleted);
});
#endregion
}, (Result) => {
#region Completion
this.InvokeEx(f => {
switch (Result.Status) {
case OperationExitStatus.Success:
Console.WriteLine(String.Format("File: {0} uploaded to {1}", Result.UploadedFileSystemObject.DocumentFullname, Result.UploadedFileSystemObject.FolderId));
break;
case OperationExitStatus.Canceled:
DialogManager.ShowDialog(DialogTypeEnum.Warning, "Dropbox", "Upload canceled.", null, this);
break;
case OperationExitStatus.Error:
DialogManager.ShowDialog(DialogTypeEnum.Error, "Dropbox", "Upload failed.", Result.Error, this);
break;
}
});
#endregion
});
}
I'm using an extension method I found on Stackoverflow for adding the Invoking functionality:
public static class InvokeExtensions {
public static void InvokeEx<T>(this T #this, Action<T> action) where T : Control {
if (#this.InvokeRequired) {
#this.Invoke(action, new object[] { #this });
}
else {
if (!#this.IsHandleCreated)
return;
if (#this.IsDisposed)
throw new ObjectDisposedException("#this is disposed.");
action(#this);
}
}
public static IAsyncResult BeginInvokeEx<T>(this T #this, Action<T> action)
where T : Control {
return #this.BeginInvoke((Action)(() => #this.InvokeEx(action)));
}
public static void EndInvokeEx<T>(this T #this, IAsyncResult result)
where T : Control {
#this.EndInvoke(result);
}
}
In my code, i have commented out the invokes as I though i don't need then as the events being raised are coming in sinked. However, i realized that my UI was not doing anything at all. So, I added the InvokeEx({ code; }) and my UI started to shoe activity.
Now, why do I need to invoke?
If I attempt different operations from UI, eventually, my UI freezes although the application still behaves as normally functioning.
I found an old article at http://msdn.microsoft.com/en-us/library/ff649143.aspx#scag-ch06_topic4 that described the usage of delegates and I see that there is a IAsyncResult involved.
Could someone point me as to where I'm going wrong here?
Update:
Ok, with the invoking code commented on the UI, I get no activity at all. But upon using the this.InvokeEx or wrapping a work in this.BeginInvokeEx, I get UI updates but after a while, here are the two exceptions occurring (in this order):
Invoke or BeginInvoke cannot be called on a control until the window handle has been created.
A Task's exception(s) were not observed either by Waiting on the Task or accessing its Exception property. As a result, the unobserved exception was rethrown by the finalizer thread.

Related

How do I make sure the UI is updated during long running processes in a WPF application?

In a WPF app that follows the MVVM pattern, I've run across a common issue where a user clicks on a button which fires an event in the ViewModel. This event should enable a "Please Wait" spinner animation, do some processing which may take a few seconds, then hide the spinner. I'm not really sure of a good pattern I can use to make sure the spinner animation always appears.
As an example, I have a login process which does the following:
Displays spinner (set property on VM to true, spinner is bound to it)
Attempt to connect to server (can take a few seconds depending on connection)
On a failure, display a failure message
On success, save off some info about the user so it's available to the rest of the app.
What I'm finding is that the spinner never actually appears. I have tried wrapping the longer-running process in a Task.Run call, but that hasn't seemed to help.
Here's an approximation of what the code looks like:
// When true, spinner should be visible
protected bool _authenticatingIsVisible = false;
public bool AuthenticatingIsVisible
{
get { return _authenticatingIsVisible; }
set
{
_authenticatingIsVisible = value;
NotifyOfPropertyChange(() => AuthenticatingIsVisible);
}
}
public void Login()
{
try
{
AuthenticationIsVisible = true;
AuthCode result = AuthCode.NoAuthenticated;
Task.Run(() => { result = _client.Authenticate() }).Wait();
AuthenticationIsVisible = false;
if (result == AuthCode.Authenticated)
{
// Bit of misc. code to set up the environment
// Another check to see if something has failed
// If it has, displays a dialog.
// ex.
var error = new Error("Something Failed", "Details Here", Answer.Ok);
var vm = new DialogViewModel() { Dialog = error };
_win.ShowDialog(vm);
return;
}
else
{
DisplayAuthMessage(result);
}
}
finally
{
AuthenticationIsVisible = false;
}
}
The proper way would be not to block the UI thread (which is what you are doing right now with .Wait()), and use AsyncAwait instead.
private Task<AuthCode> Authenticate()
{
return Task.Run<AuthCode>(()=>
{
return _client.Authenticate();
});
}
public async void Login()
{
AuthenticationIsVisible = true;
AuthCode result = await Authenticate();
AuthenticationIsVisible = false;
}

Nancy perform AfterRequest action after OnError

I want to add some request timings to the response headers of a request to a Nancy module. I've added some before/after request handlers into the RequestStartup and added the headers no problem (example below) and all was good. I've also added an OnError handler to the ApplicationStartup, to catch errors and return a nice formatted Json response.
pipelines.BeforeRequest += ctx =>
{
ctx.Items.Add("X-RequestStart", DateTime.Now.ToString());
return null;
};
pipelines.AfterRequest += ctx =>
{
var now = DateTime.Now;
try
{
//Not got around to forcing the culture on the datetime parsing yet...
var startTime = DateTime.Parse(ctx.Items["X-RequestStart"].ToString());
ctx.Response.Headers.Add("X-RequestStart", startTime.ToString(CultureInfo.InvariantCulture));
ctx.Response.Headers.Add("X-RequestComplete", now.ToString(CultureInfo.InvariantCulture));
ctx.Response.Headers.Add("X-RequestDuration", (now - startTime).ToString());
}
catch (Exception)
{
ctx.Response.Headers.Add("X-RequestComplete", now.ToString(CultureInfo.InvariantCulture));
}
};
pipelines.OnError += (ctx, exception) =>
{
return ErrorResponse.FromException(exception);
};
What I am noticing however, is that when I have an error thrown, the AfterRequest action is not performed - thus I have no timing headers in to the error response. I've tried moving the before/after request handling to the application startup, but this has no effect either.
The question is in two parts really, firstly, is it possible to get the framework to perform an AfterRequest action after the OnError action has been performed, or is the pipeline set up in a way to prevent this, and secondly, should these before/after request actions be part of the RequestStartup or ApplicationStartup? ApplicationStartup seemed sensible for error handling, whereas RequestStartup seemed sensible for interacting with the response headers as it should be on a per request basis, but I'm not sure if there is a convention for this, or if my assumptions were incorrect.
Unfortunately, this does not seem possible in NancyFx. I took a detailed look at the source code, specifically DefaultRequestDispatcher. Dispatch catches any exceptions thrown during route processing, calls ResolveErrorResult, then gets the response from the negotiator. There does not appear to be an extensibility point to modify responses generated in this way.
In my opinion, this is an oversight that the developers should consider addressing.
public async Task<Response> Dispatch(NancyContext context, CancellationToken cancellationToken)
{
// TODO - May need to make this run off context rather than response .. seems a bit icky currently
var resolveResult = this.Resolve(context);
context.Parameters = resolveResult.Parameters;
context.ResolvedRoute = resolveResult.Route;
try
{
context.Response = await ExecuteRoutePreReq(context, cancellationToken, resolveResult.Before)
.ConfigureAwait(false);
if(context.Response == null)
{
context.Response = await this.routeInvoker.Invoke(resolveResult.Route, cancellationToken,
resolveResult.Parameters, context)
.ConfigureAwait(false);
if (context.Request.Method.Equals("HEAD", StringComparison.OrdinalIgnoreCase))
{
context.Response = new HeadResponse(context.Response);
}
}
await this.ExecutePost(context, cancellationToken, resolveResult.After, resolveResult.OnError)
.ConfigureAwait(false);
}
catch(Exception ex)
{
context.Response = this.ResolveErrorResult(context, resolveResult.OnError, ex);
if (context.Response == null)
{
throw;
}
}
return context.Response;
}
private Response ResolveErrorResult(NancyContext context, Func<NancyContext, Exception, dynamic> resolveResultOnError, Exception exception)
{
if (resolveResultOnError != null)
{
var flattenedException = exception.FlattenInnerExceptions();
var result = resolveResultOnError.Invoke(context, flattenedException);
if (result != null)
{
return this.negotiator.NegotiateResponse(result, context);
}
}
return null;
}

Invoking a Multi-Threaded DLL at Run-Time

All, I call a .NET DLL containing a WinForm at run-time from a WinForm C# application. To do this I use the following:
DLL = Assembly.LoadFrom(strDllPath);
classType = DLL.GetType(String.Format("{0}.{1}", strNamespaceName, strClassName));
if (classType != null)
{
if (bDllIsWinForm)
{
classInst = Activator.CreateInstance(classType);
Form dllWinForm = (Form)classInst;
dllWinForm.Show();
// Invoke required method.
MethodInfo methodInfo = classType.GetMethod(strMethodName);
if (methodInfo != null)
{
object result = null;
result = methodInfo.Invoke(classInst, new object[] { dllParams });
return result == null ? String.Empty : result.ToString();
}
}
}
This is invoking the WinForm DLL and the required method fine for serial methods within the DLL. However, I am now invoking a multi-threaded DLL, and calling the following method:
public async void ExecuteTest(object[] args)
{
Result result = new Result();
if (!BuildParameterObjects(args[0].ToString(), args[1].ToString()))
return;
IProgress<ProgressInfo> progressIndicator = new Progress<ProgressInfo>(ReportProgress);
List<Enum> enumList = new List<Enum>()
{
Method.TestSqlConnection,
Method.ImportReferenceTables
};
Task task = Task.Factory.StartNew(() =>
{
foreach (Method method in enumList)
{
result = Process.ProcessStrategyFactory.Execute(Parameters.Instance, progressIndicator,
Process.ProcessStrategyFactory.GetProcessType(method));
if (!result.Succeeded)
{
// Display error.
return;
}
}
});
await task;
Utilities.InfoMsg("VCDC run executed successfully.");
}
But this is returning control to the caller right away due to the await (which is expected). However, the return causes the calling method to exit which closes the DLL WinForm.
What is the best method to keep the DLL WinForm active/open?
Thanks for your time.
Edit. Following the suggestion by Stephen below I have decided to turn my DLL intery method type to Task<object> and set up a continuation as follows
if (classType != null)
{
if (bDllIsWinForm)
{
// To pass object array to constructor use the following.
// classInst = Activator.CreateInstance(classType, new object[] {dllParams});
classInst = Activator.CreateInstance(classType);
dllWinForm = (Form)classInst;
dllWinForm.Show();
// Invoke required method.
MethodInfo methodInfo = classType.GetMethod(strMethodName);
if (methodInfo != null)
{
object result = null;
result = methodInfo.Invoke(classInst, new object[] { dllParams });
if (result != null)
{
if (result.GetType() == typeof(Task<object>))
{
Task<object> task = (Task<object>)result;
task.ContinueWith(ant =>
{
object innerResult = task.Result;
return innerResult == null ? String.Empty : innerResult.ToString();
});
}
return result.ToString();
}
return String.Empty;
}
}
}
I decided to set up the continuation instead of the await to avoid the chaining that would occur with the await keyword - that is making the calling method (that calling the DLL of type Task<String> etc. up the call stack.
The DLL entry method now becomes:
public Task<object> ExecuteTest(object[] args)
{
Task<object> task = null;
Result result = new Result();
if (!BuildParameterObjects(args[0].ToString(), args[1].ToString()))
return task;
IProgress<ProgressInfo> progressIndicator = new Progress<ProgressInfo>(ReportProgress);
List<Enum> enumList = new List<Enum>()
{
Method.TestSqlConnection,
Method.ImportReferenceTables
};
task = Task.Factory.StartNew<object>(() =>
{
foreach (Method method in enumList)
{
result = Process.ProcessStrategyFactory.Execute(Parameters.Instance, progressIndicator,
Process.ProcessStrategyFactory.GetProcessType(method));
if (!result.Succeeded)
{
// Display error.
}
task.Wait(5000); // Wait to prevent the method returning too quickly for testing only.
}
return null;
});
return task;
}
But this causes the DLL WinForm to be show for a split second and then disapear. i even attempted to make the Form dllWinForm global to keep the refernce to the object active, but this also has not worked. I want to note that the call to the DLL (N.B. the calling method is already running on a background thread-pool thread).
Any further helps is appreciated.
Change the return type of Execute to Task and await it.
It's difficult to guess what you have in dll but eventually your dll code + exposed in your question:
dllWinForm.Show();
should eventually have been after juxtaposing:
new Thread
(
() => new Form().ShowDialog()
)
.Start();
Probably, you should change dllWinForm.Show(); by dllWinForm.ShowDialog().Start();
ShowDialog(), in contrast with Show(), starts its own message pumping and returns only when explicitly closed.
Update (in respnse to comment):
It is not absolutely necessary to launch a form from UI.
Since you are on .NET 4.5, it is probably simpler to use WPF (instead of Windows Form) form.
Here is the code for WPF form which, in order to adjust for Windows Form, you should change Dispatcher parts by initializing WindowsFormsSynchronizationContext
Though, IMO, WinForms code will be much more complex.

Exception thrown when using TaskSourceCompletion in Silverlight 5

I'm trying to wrap the Event Async Programming model used in RIA Services in a Task.
I have followed the standard way of using a TaskCompletionSource and implemented the following extension method:
public static Task<IEnumerable<T>> LoadAsync<T>(this DomainContext source, EntityQuery<T> query) where T : Entity
{
TaskCompletionSource<IEnumerable<T>> taskCompletionSource = new TaskCompletionSource<IEnumerable<T>>();
source.Load(
query,
loadOperation =>
{
if (loadOperation.HasError && !loadOperation.IsErrorHandled)
{
taskCompletionSource.TrySetException(loadOperation.Error);
loadOperation.MarkErrorAsHandled();
}
else if (loadOperation.IsCanceled)
{
taskCompletionSource.TrySetCanceled();
}
else
{
taskCompletionSource.TrySetResult(loadOperation.Entities);
}
},
null);
return taskCompletionSource.Task;
}
I then use this in the following way:
var task = _context.LoadAsync(_context.GetPlayersQuery());
task.Start();
task.Result;
The problem though is that I get an InvalidOperationException stating that "Start may not be called on a promise-style task". I have tried not starting the task, but then the loadOperation callback never fires.
Can anyone see what I am doing wrong here?
Thanks in advance
Problem is solved. Under the hood the DomainContext.Load() method is already operating in an asynchronous manner. There must have been some conflict with trying to wrap an already asynchronous method in a task.
However, even if I still follow the EAP correctly with the code below, I still get the InvalidOperationException of 'start cannot be called on a promise-style task'
public static Task<IEnumerable<T>> LoadAsync<T>(this DomainContext source, EntityQuery<T> query) where T : Entity
{
TaskCompletionSource<IEnumerable<T>> taskCompletionSource = new TaskCompletionSource<IEnumerable<T>>();
var loadOperation = source.Load(query);
loadOperation.Completed += (obj, args) =>
{
if (loadOperation.HasError && !loadOperation.IsErrorHandled)
{
taskCompletionSource.TrySetException(loadOperation.Error);
loadOperation.MarkErrorAsHandled();
}
else if (loadOperation.IsCanceled)
{
taskCompletionSource.TrySetCanceled();
}
else
{
taskCompletionSource.TrySetResult(loadOperation.Entities);
}
};
return taskCompletionSource.Task;
}
Try this instead
var result = await _context.LoadAsync(_context.GetPlayersQuery());
Try using
task.ContinuewWith(Action<Task<T>> continuation)
That worked for me, as I too got that exception when using task.Start

Using Rx to simplify an asynchronous Silverlight web service request

I have written a simplified Silverlight client library for my WCF web service using Rx, however I notice sometimes I'm missing completed events.
public IObservable<XElement> GetReport(string reportName)
{
return from client in Observable.Return(new WebServiceClient())
from request in Observable.ToAsync<string>(client.GetReportDataAsync)(reportName)
from result in Observable.FromEvent<GetReportDataCompletedEventArgs>(client, "GetReportDataCompleted").Take(1)
from close in this.CloseClient(client)
select result.EventArgs.Result;
}
I believe the issue is caused by the fact that the web service is called and returns prior to subscribing to the completed event. I can't figure out how to get Rx to subscribe to the event prior to the Async call. I tried StartWith but that requires that the input and output types be the same, any ideas?
Seems like the best answer is to use Observable.CreateWithDisposable()
e.g.
public IObservable<XElement> GetReport(string reportName)
{
return from client in Observable.Return(new WebServiceClient())
from completed in Observable.CreateWithDisposable<GetReportDataCompletedEventArgs>(observer =>
{
var subscription = Observable.FromEvent<GetReportDataCompletedEventArgs>(client, "GetReportDataCompleted")
.Take(1)
.Select(e => e.EventArgs)
.Subscribe(observer);
client.GetReportDataAsync(reportName);
return subscription;
})
from close in this.CloseClient(client)
select completed.Result;
}
To make this easier to work with I refactored the CreateWithDisposable into a common function that can be used with all my web service calls, including automatically determining the event name from the event args type:
private IObservable<T> CallService<T>(ICommunicationObject serviceClient, Action start) where T : AsyncCompletedEventArgs
{
if (typeof(T) == typeof(AsyncCompletedEventArgs))
{
throw new InvalidOperationException("Event arguments type cannot be used to determine event name, use event name overload instead.");
}
string completedEventName = typeof(T).Name.TrimEnd("EventArgs");
return CallService<T>(serviceClient, start, completedEventName);
}
private IObservable<T> CallService<T>(ICommunicationObject serviceClient, Action start, string completedEventName) where T : AsyncCompletedEventArgs
{
return Observable.CreateWithDisposable<T>(observer =>
{
var subscription = Observable.FromEvent<T>(serviceClient, completedEventName).Take(1).Select(e => e.EventArgs).Subscribe(observer);
start();
return subscription;
});
}
// Example usage:
public IObservable<XElement> GetReport(string reportName)
{
return from client in Observable.Return(new WebServiceClient())
from completed in this.CallService<GetReportDataCompletedEventArgs>(client, () => client.GetReportDataAsync(reportName))
from close in this.CloseClient(client)
select completed.Result;
}
/// <summary>
/// Asynchronously closes the web service client
/// </summary>
/// <param name="client">The web service client to be closed.</param>
/// <returns>Returns a cold observable sequence of a single success Unit.</returns>
private IObservable<AsyncCompletedEventArgs> CloseClient(WebServiceClient client)
{
return this.CallService<AsyncCompletedEventArgs>(client, client.CloseAsync, "CloseCompleted");
}
Hope this helps someone else!
I need to use general WebClient.DownloadStringAsync so here my version.
First, wrap the event:
public static IObservable<IEvent<DownloadStringCompletedEventArgs>>
GetDownloadStringObservableEvent(this WebClient wc)
{
return Observable.FromEvent<DownloadStringCompletedEventArgs>(
wc, "DownloadStringCompleted");
}
Then create the extension method:
public static IObservable<string> GetDownloadString(this WebClient wc, Uri uri)
{
return Observable.CreateWithDisposable<string>(
observer => {
// Several downloads may be going on simultaneously. The token allows
// us to establish that we're retrieving the right one.
Guid token = Guid.NewGuid();
var stringDownloaded = wc.GetDownloadStringObservableEvent()
.Where(evt => ((Guid)evt.EventArgs.UserState) == token)
.Take(1); //implicitly unhooks handler after event is received
bool errorOccurred = false;
IDisposable unsubscribe =
stringDownloaded.Subscribe(
// OnNext action
ev => {
// Propagate the exception if one is reported.
if (ev.EventArgs.Error != null) {
errorOccurred = true;
observer.OnError(ev.EventArgs.Error);
} else if (!ev.EventArgs.Cancelled) {
observer.OnNext(ev.EventArgs.Result);
}
},
// OnError action (propagate exception)
ex => observer.OnError(ex),
// OnCompleted action
() => {
if (!errorOccurred) {
observer.OnCompleted();
}
});
try {
wc.DownloadStringAsync(uri, token);
} catch (Exception ex) {
observer.OnError(ex);
}
return unsubscribe;
}
);
}
Usage is simple:
wc.GetDownloadString(new Uri("http://myservice"))
.Subscribe(resultCallback , errorCallback);

Resources