Calling Async or Task returning Methods

.NET supports async code that can be called with async and await. Behind the scenes .NET uses a state machine for these functions that access methods that actually a return an object of type Task or Task<T>.

There are several ways to call Task based async code:

  • Using the .Result property of Task Methods
  • Using loBridge.InvokeTaskMethodAsync() to use Callbacks for results

Task Operations in .NET

In .NET you can make async calls like this:

public async Task<string> MakeHttpCall(string url)
{
    var client = new WebClient();
    string http  = await client.DownloadStringTaskAsync(url);
    return http;
}

which is asynchronous and depends on the .NET compiler magic.

Synchronous Async using .Result in .NET and FoxPro

We can't do this same exact code in FoxPro because FoxPro doesn't support the asynchronous pattern used by .NET, but we can use the actual underlying Task API to force the async call to be run synchronously:

public string MakeHttpCall(string url)
{
    var client = new WebClient();
    
    var task = client.DownloadStringTaskAsync(url); // returns immediately
    
    // waits until Result is available
    string http = task.Result;
    
    return http;
}

By calling the async method and capturing the task you are running the task in the background, but then calling .Result to return the result causes the app to wait for completion blocking the main thread.

The latter code can be simulated in FoxPro with the following:

loBridge = GetwwDotnetBridge()

loClient = loBridge.CreateInstance("System.Net.WebClient")

*** execute and returns immediately
loTask = loBridge.InvokeMethod(loClient,"DownloadStringTaskAsync","https://west-wind.com")
? loTask  && object

*** Waits for completion
lcHtml = loBridge.GetProperty(loTask,"Result")
? lcHtml

Note that you have to call InvokeMethod() rather than directly accessing the .Result object because the the result is of Task<string> in this case and generic types cannot be processed directly via COM and require the .NET proxy to convert the value for you.

.Result Warning

Microsoft doesn't recommend calling .Result or .Wait() or .WaitAll() on Task as it blocks the original UI thread and there is potential for an application to freeze while multiple tasks wait for completion and are blocked. Use this feature carefully.

For more info on how async works with wwDotnetBridge check out this blog post:

.Result - It's not Safe and not really Async

While .Result provides a potentially unsafe shortcut to 'await' an asynchronous operation, it is both not safe if multiple async requests are running and more importantly it's not actually async for the caller as it blocks the main thread while waiting for the result.

In effect you are turning an async operation into a synchronous operation. That's fine if you absolutely must call some API that returns an async result and you do it only in special cases.

It's not OK however, if you have many overlapping asynchronous calls or if the operation that is async actually takes a bit of time.

For that you want to use real Async operation, which you can accomplish with .InvokeTaskMethodAsync() that uses callback handlers.

Doing it right: Actual Async Callbacks with InvokeTaskMethodAsync()

wwDotnetBridge includes a method called .InvokeTaskMethodAsync() that lets you call an async method and instead of waiting for the result synchronously, you get called back when the async call completes.

This is a little more involved than making a synchronous call and more similar to using raw .NET Task code (or Promise APIs in JavaScript).

The idea is that you:

  • Call the method in question
  • Pass in parameters
  • Pass in a Callback Object
  • When the Task Operation completes, OnCompleted or OnError fire on the Callback object

Here's what this looks like in code:

do wwDotNetBridge
loBridge = GetwwDotnetBridge()

loClient = loBridge.CreateInstance("System.Net.WebClient")

*** Callback Handler: Defined below
loCallback = CREATEOBJECT("HttpCallback")

*** This returns immediately
loBridge.InvokeTaskMethodAsync(loCallback, 
                               loClient,
                              "DownloadStringTaskAsync",
                              "https://west-wind.com")

*** runs immediately (ie. doensn't wait for result)
? "Callback started..."

*** Simulate application still running
WAIT WINDOW 
RETURN

You then also need a CallbackHandler object that is based on AsyncCallbackEvents. This a message result object that has OnCompleted and OnError methods that are called on completion of the async operation.

*** Handle result values in this object
*** The callback object is called back 
*** when the method completes or fails
DEFINE CLASS HttpCallback as AsyncCallbackEvents

*** Returns the result of the method and the name of the method name
FUNCTION OnCompleted(lvResult,lcMethod)
? "Success: " + lcMethod 
? LEFT(lvResult,2000)
ENDFUNC

* Returns an error message, a .NET Exception and the method name
FUNCTION OnError(lcMessage,loException,lcMethod)
? "Error: " + lcMethod
? lcMessage
ENDFUNC

ENDDEFINE

OnCompleted is called if the async call succeeds and returns the result value and method name as a parameter. OnError is called if anything goes wrong during the async call and returns a message and .NET Exception object along with the calling method name.

Note that there can be many errors:

  • Invalid parameters
  • Invalid method signature called
  • Failure during the call
  • Async threading error

All of these are handled by OnError.

Keep Callback Handlers simple

It's important that the callback handler methods should not be long running as these calls are blocking and if you have many of these firing at the same time you can get into potential dead lock states due to FoxPro's limited STA threading. This feature is pushing FoxPro's threading logic to its limit and officially this type of callback scenario is not recommended. However, as long as you keep the callback methods fast and as non-blocking as possible this scheme works reliably.

It's recommended that if you do any significant code you schedule that code onto the main FoxPro thread. So instead of doing complex processing in the callback handler, pick up the result value and store it in an accessible property or variable, and then trigger processing from your main FoxPro thread. This minimizes the code that runs off a non-FoxPro .NET thread and prevents blocking on the .NET end.


© West Wind Technologies, 2024 • Updated: 08/18/24
Comment or report problem with topic