Search code examples
javascriptmauiwinui-3webview2

AddWebAllowedObject not available for Webview2


We are currently migrating some xamarin forms apps to .net Maui. Some of them use a webview and need a javascript bridge so that the website can call c# code in the app. This is working fine for android, but making the windows app work is harder. In Uwp a Webview was used and this had the AddWebAllowedObject method. In win UI 3 (used for windows in MAUI) a WebView2 is used and this does not have this method anymore. I've found this while browsing the internet. https://gist.github.com/mqudsi/ceb4ecee76eb4c32238a438664783480 . This is a solution but here the bridge is returning promises, this was not the case when the AddWebAllowed on the WebView was used. (And it is also not the case for the bridge used on android). We use an internal website that is displayed in the app. It is the same website for android as for windows so the calling of the javascript bridge should be the same. Any ideas how I can get this to work on Webview2?


Solution

  • I have created a ticket to add the AddWebAllowedObject extension method to WebView2. So far I did not see any progress on this, but you can track the ticket here: https://github.com/MicrosoftEdge/WebView2Feedback/issues/3823

    As I needed a solution I did make some changes to the website code so the bridge could also communicate with answers that were promises. As stated in the case I used this implementation to start with.: https://gist.github.com/mqudsi/ceb4ecee76eb4c32238a438664783480 I did need to make some changes to make it work over here. In the initial script I need to use callbackindex++ later in the script to make it work. Wel this is the code I finally got (I am just using methods):

    static class WebView2Extensions {
    
    private struct WebMessage
    {
        public Guid Guid { get; set; }
    }
    
    private struct MethodWebMessage
    {
        public string Id { get; set; }
        public string Method { get; set; }
        public string Args { get; set; }
    }
    
    public static List<TypedEventHandler<WebView2, CoreWebView2WebMessageReceivedEventArgs>> _handlers = new List<TypedEventHandler<WebView2, CoreWebView2WebMessageReceivedEventArgs>>();
    
    public static async Task AddWebAllowedObject<T>(this WebView2 webview, string name, T @object)
    {
        var sb = new StringBuilder();
        sb.AppendLine($"window.{name} = {{ ");
    
        // Test webview for our sanity
        await webview.ExecuteScriptAsync($@"console.log(""Sanity check from iMessage"");");
    
        var methodsGuid = Guid.NewGuid();
        var methodInfo = typeof(T).GetMethods(BindingFlags.Public | BindingFlags.Instance);
        var methods = new Dictionary<string, MethodInfo>(methodInfo.Length);
        foreach (var method in methodInfo)
        {
            var functionName = $"{char.ToLower(method.Name[0])}{method.Name.Substring(1)}";
            sb.AppendLine($@"{functionName}: function() {{ window.chrome.webview.postMessage(JSON.stringify({{ guid: ""{methodsGuid}"", id: this._callbackIndex, method: ""{functionName}"", args: JSON.stringify([...arguments]) }})); const promise = new Promise((accept, reject) => this._callbacks.set(this._callbackIndex++, {{ accept: accept, reject: reject }})); return promise; }},");
            methods.Add(functionName, method);
        }
    
        var propertiesGuid = Guid.NewGuid();
        var propertyInfo = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
        var properties = new Dictionary<string, PropertyInfo>(propertyInfo.Length);
        // Add a map<int, (promiseAccept, promiseReject)> to the object used to resolve results
        sb.AppendLine($@"_callbacks: new Map(),");
        // And a shared counter to index into that map
        sb.Append($@"_callbackIndex: 0,");
    
        sb.AppendLine("}");
        try
        {
            await webview.ExecuteScriptAsync($"{sb}").AsTask();
        }
        catch (Exception)
        {
        }
    
        var handler = (TypedEventHandler<WebView2, CoreWebView2WebMessageReceivedEventArgs>)(async (_, e) =>
        {
            var jsonString = e.TryGetWebMessageAsString();
            var message = JsonConvert.DeserializeObject<WebMessage>(jsonString);
            if (message.Guid == methodsGuid)
            {
                var methodMessage = JsonConvert.DeserializeObject<MethodWebMessage>(jsonString);
                
                var method = methods[methodMessage.Method];
                try
                {
                    var result = method.Invoke(@object, JsonConvert.DeserializeObject<object[]>(methodMessage.Args));
                    var acceptString = "";
                    if (result is object)
                    {
                        var resultType = result.GetType();
                        dynamic task = null;
                        if (resultType.Name.StartsWith("TaskToAsyncOperationAdapter")
                            || resultType.IsInstanceOfType(typeof(IAsyncInfo)))
                        {
                            // IAsyncOperation that needs to be converted to a task first
                            if (resultType.GenericTypeArguments.Length > 0)
                            {
                                var asTask = typeof(WindowsRuntimeSystemExtensions)
                                    .GetMethods(BindingFlags.Public | BindingFlags.Static)
                                    .Where(method => method.GetParameters().Length == 1
                                        && method.Name == "AsTask"
                                        && method.ToString().Contains("Windows.Foundation.IAsyncOperation`1[TResult]"))
                                    .FirstOrDefault();
                                asTask = asTask.MakeGenericMethod(resultType.GenericTypeArguments[0]);
                                task = (Task)asTask.Invoke(null, new[] { result });
                            }
                            else
                            {
                                task = WindowsRuntimeSystemExtensions.AsTask((dynamic)result);
                            }
                        }
                        else
                        {
                            var awaiter = resultType.GetMethod(nameof(Task.GetAwaiter));
                            if (awaiter is object)
                            {
                                task = (dynamic)result;
                            }
                        }
                        if (task is object)
                        {
                            result = await task;
                        }
    
                        if (result is string stringResult)
                        {
                            acceptString = String.IsNullOrEmpty(stringResult) ? String.Empty :$"\"{stringResult}\"";
                        }
                        else
                        {
                            acceptString = JsonConvert.SerializeObject(result,
                                    new JsonSerializerSettings
                                    {
                                        ContractResolver = new CamelCasePropertyNamesContractResolver()
                                    });
                        }
                    }
                    else
                    {
                        acceptString = result?.ToString() ?? String.Empty;
                    }
                    var responseScript = $"{name}._callbacks.get({methodMessage.Id}).accept({acceptString}); {name}._callbacks.delete({methodMessage.Id});";
                    await webview.ExecuteScriptAsync(responseScript);
                    
                }
                catch (Exception ex)
                {
                    var json = JsonConvert.SerializeObject(ex, new JsonSerializerSettings() { Error = (_, e) => e.ErrorContext.Handled = true });
                    await webview.ExecuteScriptAsync($@"{name}._callbacks.get({methodMessage.Id}).reject(JSON.parse({json})); {name}._callbacks.delete({methodMessage.Id});");
                }
            }
        });
    
        _handlers.Add(handler);
        webview.WebMessageReceived += handler;
    }}
    

    if you for example have a class called for example WebCommunicator => The call would be something like:

     webView2.AddWebAllowedObject<WebCommunicator>("someBridgeName",WebCommunicator);
    

    If you have methods in WebCommunicator that will return an Object, do not serialize this to a json string (the AddWebAllowedObject method will handle that), just return an object.