MauiHybridWebView
MauiHybridWebView copied to clipboard
Unable to call async JS method and get result
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 https://github.com/Eilon/MauiHybridWebView/issues/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.