Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Turn a sync method into an async one

Tags:

c#

async-await

I'm trying to turn a synchronous method from some old code into an asynchronous method, but I'm having some trouble understand. From all the videos and tutorials I've read they seem to be creating two methods: one the actual function, the other a wrapper and then it's the wrapper called on the UI.

Here is my code:

private async Task<bool> login(String username, String password)
{
        var tcs = new TaskCompletionSource<RestSharp.IRestResponse>();

        RestSharp.RestRequest request = new RestSharp.RestRequest("/accounts/login/", RestSharp.Method.GET);
        RestSharp.IRestResponse response = client.Execute(request);

        // Make the login request
        request = new RestSharp.RestRequest("/accounts/login/", RestSharp.Method.POST);
        request.AddParameter("username", username);
        request.AddParameter("password", password);

        response = client.Execute(request);

        // Return loggin status
        dom = response.Content;
        return dom["html"].HasClass("logged-in"); 
}

For some reason when I try to call the method on the UI thread from a button click, it's asking me to make the button event async.

txtLog.AppendText("Before Await");

Task<bool> result = await login("","");

txtLog.AppendText("After Await");
txtLog.AppendText("Result: " + result.toString());

Do I need a wrapper method that is also set to async which makes the call to login?

like image 906
James Jeffery Avatar asked Aug 10 '13 21:08

James Jeffery


2 Answers

To answer your 2nd part first, yes you need to mark the event for the button async, if you want to use the keyword await in your code you must declare the function async.

2ndly if a function uses async without having an await inside of it the code will not be run asynchronously, you either need to create a task and run your synchronous method inside of it or rewrite the method to be asynchronous.

As task method:

private async void button1_Click(object sender, EventArgs e)
{
    txtLog.AppendText("Before Await");

    //Note I changed from "Task<bool>" to "bool", await is like calling ".Result" 
    //  on a task but not blocking the UI, so you store the type you are waiting for.
    bool result = await Task.Run(() => login("","")); //You would still use your old login code before you tried to make it async, it requires no modifications.

    txtLog.AppendText("After Await");
    txtLog.AppendText("Result: " + result.ToString());
}

Rewriting the function method:

private async void button1_Click(object sender, EventArgs e)
{
    txtLog.AppendText("Before Await");

    //Note I changed from "Task<bool>" to "bool", await is like calling ".Result" 
    //  on a task but not blocking the UI, so you store the type you are waiting for.
    bool result = await login("",""); 

    txtLog.AppendText("After Await");
    txtLog.AppendText("Result: " + result.ToString());
}

private Task<bool> login(String username, String password)
{
    var tcs = new TaskCompletionSource<bool>();

    // Make the login request
    var request = new RestSharp.RestRequest("/accounts/login/", RestSharp.Method.POST);
    request.AddParameter("username", username);
    request.AddParameter("password", password);

    client.ExecuteAsync(request, (response, handle) =>
        {
            try
            {
                // Return loggin status
                var dom = response.Content;

                //dom["html"] did not have a .HasClass in my tests, so this code may need work.
                tcs.TrySetResult(dom["html"].HasClass("logged-in")); 
            }
            catch(Exception ex)
            {
                tcs.TrySetException(ex);
            }
        });

    return tcs.Task;
}

In my "rewrite method" what I am doing is I am using ExecuteAsync witch is part of IRestClient. That function calls a callback method when it completes, in the callback method I call tcs's SetResult to report back the result I wanted.

You could expand this further by taking in a CancellationToken and if the token is raised call Abort() on RestRequestAsyncHandle, however if we do this we need to bring the async back in to the function and await the result so we can clean up after the cancellation token registration.

private Task<bool> login(String username, String password)
{
    return login(username, password, CancellationToken.None);
}

private async Task<bool> login(String username, String password, CancellationToken cancelToken)
{
    var tcs = new TaskCompletionSource<bool>();

    // Make the login request
    var request = new RestSharp.RestRequest("/accounts/login/", RestSharp.Method.POST);
    request.AddParameter("username", username);
    request.AddParameter("password", password);

    var asyncHandle = client.ExecuteAsync(request, (response, handle) =>
        {
            try
            {
                // Return loggin status
                var dom = response.Content;

                tcs.TrySetResult(dom["html"].HasClass("logged-in")); 
            }
            catch(Exception ex)
            {
                tcs.TrySetException(ex);
            }
        });

    //if the token is canceled it will call `asyncHandle.Abort()` for us.
    using(cancelToken.Register(() =>
        {
           if(tcs.TrySetCanceled(cancelToken))
               asyncHandle.Abort();
        }))
    {
        return await tcs.Task;
    }
}
like image 140
Scott Chamberlain Avatar answered Oct 20 '22 09:10

Scott Chamberlain


Your button handler uses the await keyword, which requires that it be made async. The await keyword basically partitions the method at the await, turning the part after the await into a delegate that continues when the awaited Task completes. The method returns immediately after encountering the await.

Your login function doesn't use await. It doesn't need the async keyword.

like image 24
Mud Avatar answered Oct 20 '22 08:10

Mud