Open rbrundritt opened 1 month ago
Looks like it is pretty easy to determine if a JS function is async or not. For example:
async function test() {
return 1;
}
if(test[Symbol.toStringTag] === 'AsyncFunction'){
console.log('is async');
} else {
console.log('inot async');
}
So with that in mind, the EvaluateJavaScriptAsync
could be updated to the following within the existing InvokeJsMethodAsync
method to the following:
await _webView.EvaluateJavaScriptAsync($"(var params = [{paramJson}]; if({methodName}[Symbol.toStringTag] === 'AsyncFunction'){{ async function() {{ var result = await {methodName}(...params}); triggerAsyncCallback(\"{taskId}\", result); }} }} else {{ return {methodName}(...params); }})
We would need to still add the task creation logic.
@rbrundritt ooooh this looks interesting! I think I was just noticing this same issue while testing the .NET MAUI official HybridWebView, so I'll try out your suggestion and see if I can get it working end-to-end!
I saw somewhere, I think it was for WebView1, that you could just add an await in front of the async function method name and it should work. I'm going to test that now. That would be much better than using this task ID approach I started out with.
I'm pretty sure I tried doing await
but it seemed the function 'returned' immediately and you wouldn't get the result back at all.
So I think that probably the JS code has to do the await and then send some kind of message back to C#, and then C# can process the result.
This looked promising: https://stackoverflow.com/questions/76817407/calling-async-function-in-webview2
But in the end it still didn't work for me. It looks like there is a known bug in WebView2: https://github.com/MicrosoftEdge/WebView2Feedback/issues/2295 There workaround sounds to be to use "DevTools protocol to evaluate your JavaScript" but that doesn't sound ideal. Also not sure if there is similar issues with the other platform browsers.
On a related not I also found that the EvaluateJavaScriptAsync
method fails silently if your JSON has any properties with HTML strings or special characters (unicode). I tried different JSON serializer options but those didn't work (also didn't like the idea of setting the encoding to System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
). Here is a related thread: https://stackoverflow.com/questions/58003293/dotnet-core-system-text-json-unescape-unicode-string
I did however find a simple solution. I take the serialized paramJson
string and convert it to base64, then in the EvaluateJavaScriptAsync
method I convert back to a normal string then JSON object using JSON.parse and
atob`. Here is the code:
paramJson = Convert.ToBase64String(Encoding.UTF8.GetBytes(paramJson));
return await _webView.EvaluateJavaScriptAsync($"{methodName}(...JSON.parse('[' + atob('{paramJson}') + ']'))");
After a bunch of testing I'm going to use the task completion method for now in my app as that seems to be more reliable. I have to testing it a bit more on other platforms. To simplify my code I combined the logic into a single InvokeJsMethodAsync
method like this:
public async Task<string> InvokeJsMethodAsync(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 = UniqueId.Get("asyncMapTask");
asyncTaskCallbacks.Add(taskId, callback);
string paramJson = GetParamJson(paramValues);
//Base64 encode parameters to workaround some known issues in the WebView2 control. (html strings and special characters cause EvaluateJavaScriptAsync to fail silently.
paramJson = Convert.ToBase64String(Encoding.UTF8.GetBytes(paramJson));
await _webView.EvaluateJavaScriptAsync($"(async () => {{var paramJson = JSON.parse('[' + atob('{paramJson}') + ']'); var result = ({methodName}[Symbol.toStringTag] === 'AsyncFunction') ? await {methodName}(...paramJson) : {methodName}(...paramJson); triggerAsyncCallback(\"{taskId}\", result); }})()");
return await callback.Task;
}
catch (Exception ex)
{
Debug.WriteLine($"Error invoking async method: {ex.Message}");
return string.Empty;
}
}
The following does convert the paramJson
to a base64 string. I'm sure this does create a bit of overhead, however, in testing it seems to be negligible (I played around with sending and receiving data that's around ~50mb in size using the above code).
I'm using the following to generate the paramJson
string initially:
private string GetParamJson(object[] paramValues)
{
JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions
{
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault
};
string paramJson = string.Empty;
if (paramValues != null && paramValues.Length > 0)
{
paramJson = string.Join(", ", paramValues.Select(v => JsonSerializer.Serialize(v, jsonSerializerOptions)));
//New line characters do not currently work with EvaluateJavaScriptAsync. https://github.com/dotnet/maui/issues/11905
//We don't need formatted JSON, and we don't want new line characters in string properties as that's likely to cause issues anyways.
paramJson = paramJson.Replace("\r\n", " ").Replace("\n", " ");
}
return paramJson;
}
In my app I don't want null property values to be written. I suspect this may not be the case for everyone, thus it would be good to have a way for people to set both a default serializer and optionally pass in a custom serializer when calling InvokeJsMethodAsync
Thank you @rbrundritt ! I've been inspired by your code suggestions and I think I have something working that will be part of my PR https://github.com/dotnet/maui/pull/23769 for the official HybridWebView version. I need to test a bunch more things but I do have an end-to-end scenario working with actual async JavaScript code!
Alright I pushed a major update to https://github.com/dotnet/maui/pull/23769 , in particular this last commit: https://github.com/dotnet/maui/pull/23769/commits/02eb87638a60dd0da24b88dc0121146d6831c515
I still have cleanup to do, but I've tested it on Windows and Android quite a bit and it seems to work! I'll test iOS/Mac later.
@rbrundritt thank you so much for all your help on this!
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)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:
InvokeAsyncJsMethodAsync
sounds 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
InvokeJsMethodAsync
method and make this a seamless update for users.