28

I have a webservice written in Yii (php framework).

I use C# and Visual Studio 2012 to develop a WP8 application. I added a service reference to my project (Add Service Reference). So I am able to use webservice functions.

   client = new YChatWebService.WebServiceControllerPortTypeClient();

   client.loginCompleted += client_loginCompleted;   // this.token = e.Result;
   client.loginAsync(this.username, this.password); 

   client.getTestCompleted += client_getTestCompleted;
   client.getTestAsync(this.token); 

function getTestAsync and loginAsync return void and both are asynchronous. Is it possible for the functions to return Task<T>? I would like to use async/await keywords in my program.

Brian
  • 24,434
  • 16
  • 74
  • 162
MPeli
  • 530
  • 1
  • 7
  • 18

5 Answers5

34

Assuming that loginAsync returns void, and loginCmpleted event fires when login is done, this is called the Event-based Asynchronous Pattern, or EAP.

To convert EAP to await/async, consult Tasks and the Event-based Asynchronous Pattern. In particular, you'll want to make use of the TaskCompletionSource to convert the event-based model to a Task-based model. Once you've got a Task-based model, you can use C# 5's sexy await feature.

Here's an example:

// Use LoginCompletedEventArgs, or whatever type you need out of the .loginCompleted event
// This is an extension method, and needs to be placed in a static class.
public static Task<LoginCompletedEventArgs> LoginAsyncTask(this YChatWebService.WebServiceControllerPortTypeClient client, string userName, string password) 
{ 
    var tcs = CreateSource<LoginCompletedEventArgs>(null); 
    client.loginCompleted += (sender, e) => TransferCompletion(tcs, e, () => e, null); 
    client.loginAsync(userName, password);
    return tcs.Task; 
}

private static TaskCompletionSource<T> CreateSource<T>(object state) 
{ 
    return new TaskCompletionSource<T>( 
        state, TaskCreationOptions.None); 
}

private static void TransferCompletion<T>( 
    TaskCompletionSource<T> tcs, AsyncCompletedEventArgs e, 
    Func<T> getResult, Action unregisterHandler) 
{ 
    if (e.UserState == tcs) 
    { 
        if (e.Cancelled) tcs.TrySetCanceled(); 
        else if (e.Error != null) tcs.TrySetException(e.Error); 
        else tcs.TrySetResult(getResult()); 
        if (unregisterHandler != null) unregisterHandler();
    } 
}

Now that you've converted the Event-based async programming model to a Task-based one, you can now use await:

var client = new YChatWebService.WebServiceControllerPortTypeClient();
var login = await client.LoginAsyncTask("myUserName", "myPassword");
Judah Gabriel Himango
  • 55,559
  • 37
  • 152
  • 206
  • Thank you.I had to get familiar with terms such as lambda expressions, delegates..." So everything is very new for me. The compiler has a problem with "e => e" so I changed it to "() => e" and there is no option TaskCreationOptions.DetachedFromParent. What should I use instead? – MPeli Jan 02 '13 at 10:20
  • TaskCreateOptions.None should be OK here. Yes, my mistake, it should be () => e, or () => e.Foo, whatever property you want to pull off there. I'll update the code. – Judah Gabriel Himango Jan 02 '13 at 15:36
  • @MPeli If this answer solved it for you, please mark it as the answer. Thanks. – Judah Gabriel Himango Jan 03 '13 at 16:59
  • 2
    This code won't work since user state will be null. The check in TransferCompletion (`e.UserState == tcs`) will always be false. Not sure what the best approach is here, but if tcs is passed as a third argument to `loginAsync`, it appears to work as expected. – user3533716 Mar 07 '17 at 12:47
  • 1
    @user3533716: That looks reasonable to me. I believe Judah's intent was trying to ensure `loginCompleted` would properly synchronize calls and responses in the case that `LoginAsyncTask` was called more than once on the same `YChatWebService.WebServiceControllerPortTypeClient`. It's simpler to guarantee one call per client by instantiating the client inside `LoginAsyncTask`, but Judah's approach allows the caller to construct a custom instance of `YChatWebService.WebServiceControllerPortTypeClient` (e.g., by dynamically modifying the URL). – Brian Nov 27 '19 at 15:47
  • @JudahGabrielHimango - The link you posted is dead. – dcp Mar 10 '21 at 19:47
  • Considering this was posted 8 years ago, that link lasted fairly long! I've updated the post with the correct link. – Judah Gabriel Himango Mar 12 '21 at 18:32
7

While adding your service reference make sure you selected Generate Task based operations in Advanced section. this will create awaitable methods like LoginAsync returning Task<string>

I4V
  • 33,572
  • 3
  • 63
  • 78
  • This is using an event based model, so `FromAsync` won't help. – Servy Dec 31 '12 at 15:47
  • @Servy I used the url in the question and generated the awaitable functions automatically. Anything wrong? – I4V Dec 31 '12 at 15:52
  • I am unable to select Generate Task based operations because it is greyed out. It seems that it is disabled for WP8 projects. See [this topic](http://stackoverflow.com/questions/13266079/wp8-sdk-import-service-reference-with-task-based-operations-not-possible) – MPeli Dec 31 '12 at 16:14
7

I've had to do this a couple of times over the last year and I've used both @Judah's code above and the original example he has referenced but each time I've hit on the following problem with both: the async call works but doesn't complete. If I step through it I can see that it will enter the TransferCompletion method but the e.UserState == tcs will always be false.

It turns out that web service async methods like the OP's loginAsync have two signatures. The second accepts a userState parameter. The solution is to pass the TaskCompletionSource<T> object you created as this parameter. This way the e.UserState == tcs will return true.

In the OP, the e.UserState == tcs was removed to make the code work which is understandable - I was tempted too. But I believe this is there to ensure the correct event is completed.

The full code is:

public static Task<LoginCompletedEventArgs> RaiseInvoiceAsync(this Client client, string userName, string password)
{
    var tcs = CreateSource<LoginCompletedEventArgs>();
    LoginCompletedEventHandler handler = null;
    handler = (sender, e) => TransferCompletion(tcs, e, () => e, () => client.LoginCompleted -= handler);
    client.LoginCompleted += handler;

    try
    {
        client.LoginAsync(userName, password, tcs);
    }
    catch
    {
        client.LoginCompleted -= handler;
        tcs.TrySetCanceled();
        throw;
    }

    return tcs.Task;
}

Alternatively, I believe there is a tcs.Task.AsyncState property too that will provide the userState. So you could do something like:

if (e.UserState == taskCompletionSource || e.UserState == taskCompletionSource?.Task.AsyncState)
{
    if (e.Cancelled) taskCompletionSource.TrySetCanceled();
    else if (e.Error != null) taskCompletionSource.TrySetException(e.Error);
    else taskCompletionSource.TrySetResult(getResult());
    unregisterHandler();
}

This was what I tried initially as it seemed a lighter approach and I could pass a Guid rather than the full TaskCompletionSource object. Stephen Cleary has a good write-up of the AsyncState if you're interested.

Digbyswift
  • 9,945
  • 2
  • 36
  • 65
0

(Copied from OP, per https://meta.stackexchange.com/a/150228/136378 )

Answer:

Following code seems to work.

internal static class Extension
{
    private static void TransferCompletion<T>(
        TaskCompletionSource<T> tcs, System.ComponentModel.AsyncCompletedEventArgs e, 
        Func<T> getResult)
    {
        if (e.Error != null)
        {
            tcs.TrySetException(e.Error);
        }
        else if (e.Cancelled)
        {
            tcs.TrySetCanceled();
        }
        else
        {
            tcs.TrySetResult(getResult());
        }
    }

    public static Task<loginCompletedEventArgs> LoginAsyncTask(
        this YChatWebService.WebServiceControllerPortTypeClient client,
        string userName, string password)
    {
        var tcs = new TaskCompletionSource<loginCompletedEventArgs>();
        client.loginCompleted += (s, e) => TransferCompletion(tcs, e, () => e);
        client.loginAsync(userName, password);
        return tcs.Task;
    }
}

I call it this way

client = new YChatWebService.WebServiceControllerPortTypeClient();
var login = await client.LoginAsyncTask(this.username, this.password);
Brian
  • 24,434
  • 16
  • 74
  • 162
  • This code may function incorrectly if a single client instance is used to make multiple simultaneous calls to `LoginAsyncTask`. The `loginCompleted` event will fire all subscribed events at once on each completion. This can be resolved either by using a separate client for each call (using an extension method on client is a bad idea, in this approach). It can also be resolved by collating the callbacks with the calls via the userstate parameter (this is the approach which https://stackoverflow.com/a/14103474/18192 tries to use). – Brian Nov 27 '19 at 15:38
-4

If you want to be able to await the methods, they should return Task. You cannot await a method that returns void. If you want them to return a value, like int they should return Task<int> then the method should return int.

public async Task loginAsync(string username, string password) {}

Then you can call

Task t = loginAsync(username, password);
//login executing
//do something while waiting

await t; //wait for login to complete
Nick Bray
  • 1,865
  • 12
  • 18
  • He's asking *how* to do that. He doesn't know how an event based model can return a task. – Servy Dec 31 '12 at 15:45
  • Take the same method you had and just put Task instead of void. Then you can call await in your program. No extra work needed. How to use it is a different story. – Nick Bray Dec 31 '12 at 15:48
  • 2
    You can't just change the return type of the method and be done. The question is specifically asking how to change the implementation of the method so that it actually returns a task. The question is asking how you create a `Task` that is completed when the data is ready, and you don't answer that. See Judah's answer for how you actually implement it. – Servy Dec 31 '12 at 15:49