I've found that if you call an async JS function you can't wait for the response currently. I managed to work around this, but before making a pull request for this I wanted to show how I was addressing this and get feedback.
Here is a proposal for a new method called InvokeAsyncJsMethodAsync which creates a task ID for async JS requests and TaskCompletion (there is some additional code here to address other issues I encountered, like #72 and I also needed custom JsonSerializerOptions)
public class MyJsInterlop
{
private int taskCounter = 0;
private Dictionary<string, TaskCompletionSource<string>> asyncTaskCallbacks = new Dictionary<string, TaskCompletionSource<string>>();
//Optional: custom serialization options.
private JsonSerializerOptions? _jsonSerializerOptions;
public MapJsInterlop(JsonSerializerOptions? jsonSerializerOptions = null)
{
_jsonSerializerOptions = jsonSerializerOptions;
}
/// <summary>
/// Handler for when the an Async JavaScript task has completed and needs to notify .NET.
/// </summary>
/// <param name="eventData"></param>
public void AsyncTaskCompleted(string taskId, string result)
{
//Look for the callback in the list of pending callbacks.
if (!string.IsNullOrEmpty(taskId) && asyncTaskCallbacks.ContainsKey(taskId))
{
//Get the callback and remove it from the list.
var callback = asyncTaskCallbacks[taskId];
callback.SetResult(result);
//Remove the callback.
asyncTaskCallbacks.Remove(taskId);
}
}
public async Task<string> InvokeAsyncJsMethodAsync(string methodName, params object[] paramValues)
{
try
{
if (string.IsNullOrEmpty(methodName))
{
throw new ArgumentException($"The method name cannot be null or empty.", nameof(methodName));
}
//Create a callback.
var callback = new TaskCompletionSource<string>();
var taskId = $"{taskCounter++}";
asyncTaskCallbacks.Add(taskId, callback);
string paramJson = GetParamJson(paramValues);
await _webView.EvaluateJavaScriptAsync($"{methodName}({paramJson}, \"{taskId}\");");
return await callback.Task;
}
catch (Exception ex)
{
Debug.WriteLine($"Error invoking async method: {ex.Message}");
return string.Empty;
}
}
public async Task<TReturnType?> InvokeAsyncJsMethodAsync<TReturnType>(string methodName, params object[] paramValues)
{
try
{
var stringResult = await InvokeAsyncJsMethodAsync(methodName, paramValues);
if (string.IsNullOrWhiteSpace(stringResult) || stringResult.Equals("null") || stringResult.Equals("{}"))
{
return default;
}
return JsonSerializer.Deserialize<TReturnType>(stringResult, Constants.MapJsonSerializerOptions);
}
catch (Exception ex)
{
Debug.WriteLine($"Error invoking async method: {ex.Message}");
return default;
}
}
/// <summary>
/// Gets the JSON string and JsonSerializerOptions for the parameters.
/// </summary>
private string GetParamJson(object[] paramValues)
{
string paramJson = string.Empty;
if (paramValues != null && paramValues.Length > 0)
{
paramJson = string.Join(", ", paramValues.Select(v => JsonSerializer.Serialize(v, _jsonSerializerOptions)));
}
return paramJson;
}
}
From the JavaScript side of things I need to send a message back when the async task has completed.
/**
* Notifies .NET code that an async callback in JavaScript has completed.
* @param {string} taskId The task id of the async operation.
* @param {any} result The result of the async operation.
*/
function triggerAsyncCallback(taskId, result) {
//Make sure the result is a string.
if (result && typeof (result) !== 'string') {
result = JSON.stringify(result);
} else {
result = '';
}
HybridWebView.SendInvokeMessageToDotNet('AsyncTaskCompleted', [taskId, result]);
}
Now lets assume I have this simple async JavaScript function:
async function simpleAsyncFunction(taskId){
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("times up!");
}, 300);
});
var result = await myPromise;
triggerAsyncCallback(taskId, result);
}
I can now call this from .NET and wait for it to asynchronously complete.
var result = await InvokeAsyncJsMethodAsync("simpleAsyncFunction", "Bob");
//We will no reach this point until the async JS function has responded.
I've tested this in both Windows and Android with success.
A couple of thoughts for feedback/improvement:
- I tried to keep the .NET method name similar but
InvokeAsyncJsMethodAsync sounds weird. Any suggestions?
- Possibly add timeout logic for the async tasks on the .NET side.
- Possibly wrap the JS method call so the developer doesn't need to be aware of the taskId. Possibly something like this:
await _webView.EvaluateJavaScriptAsync($"(async function() {{var result = await {methodName}({paramJson}); triggerAsyncCallback(\"{taskId}\", result);}})()");
Now that I write the above, I'm thinking that a bit more javascript logic could be added that checks to see if the method is async and if so, then await it. If I could get that working, then we would only need to update the original InvokeJsMethodAsync method and make this a seamless update for users.
I've found that if you call an async JS function you can't wait for the response currently. I managed to work around this, but before making a pull request for this I wanted to show how I was addressing this and get feedback.
Here is a proposal for a new method called
InvokeAsyncJsMethodAsyncwhich creates a task ID for async JS requests and TaskCompletion (there is some additional code here to address other issues I encountered, like #72 and I also needed custom JsonSerializerOptions)From the JavaScript side of things I need to send a message back when the async task has completed.
Now lets assume I have this simple async JavaScript function:
I can now call this from .NET and wait for it to asynchronously complete.
I've tested this in both Windows and Android with success.
A couple of thoughts for feedback/improvement:
InvokeAsyncJsMethodAsyncsounds weird. Any suggestions?Now that I write the above, I'm thinking that a bit more javascript logic could be added that checks to see if the method is async and if so, then await it. If I could get that working, then we would only need to update the original
InvokeJsMethodAsyncmethod and make this a seamless update for users.