Can I use WebView2 in Xamarin.Forms? I need to use it in Android, iOS & Windows (UWP), instead of original WebView

Viewed 1477

I googled and only found the use of WebView2 specifically with Win32 applications.

I searched at NuGet Gallery and found [Microsoft.Web.WebView2] (https://www.nuget.org/packages/Microsoft.Web.WebView2/1.0.824-prerelease). This package is only for WinForms and WPF.

Edit (Apr 1, 2022) When I originally asked this question, I had a superficial understanding of the architecture of xamarin.forms applications. What I really meant to ask was: "Is there any cross-platform control, that when used in a xamarin.forms project, uses WebView2 on UWP and the corresponding native WebViews on Android and iOS?" UWP currently uses a WebView that is based on the EDGE browser (pre-Chromium), which is inconvenient.

2 Answers

If you expect to use all native properties, methods and events of WebView2, the answer is that it is impossible and that it won't be possible just like for any other UWP control that is as such limited to UWP.

If you want just to use the Chromium rendering engine, then on Android you already have it. On iOS you can't use it and it is not possible not just in Xamarin.Forms but in anything. Even Google Chrome and Microsoft Edge apps don't use it. On UWP at the moment you need to write your own custom renderer if you want to use WebView2 in Xamarin.Forms. In some future it will likely work without any additional action.

I have implemented a Custom Renderer that implements the Xamarin Forms WebView control and wires everything to WebView2 instead the Edge-based WebView.

Make sure to reference the Microsoft.UI.Xaml package from the UWP project-file. I used v2.8.0, but there may be newer versions. The add the UwpChromiumWebViewRenderer.cs file to your project (code below). The ExportRenderer automatically wires the WebView2 to the existing Xamarin Form WebView control, so it should be a drop-in replacement.

Some important things to know:

  1. Cookies are saved by Edge and are application-scoped.
  2. Developer tools are disabled, because they don't work within UWP programs (it requires creating an external window and that doesn't seem to be supported).
  3. Only tested within the scope of my application, so no promises.
  4. It only seems to work when the WebView2 runtime is installed "global" (not per-user, but per-machine). UWP uses isolated registry access, so it doesn't seem to be able to find the UWP entries when the application is installed in per-user mode.

UwpChromiumWebViewRenderer.cs

using System;
using System.ComponentModel;
using System.Threading.Tasks;
using Microsoft.UI.Xaml.Controls;
using Xamarin.Forms;
using Xamarin.Forms.Platform.UWP;
using Xamarin.Forms.PlatformConfiguration.WindowsSpecific;
using WebView2 = Microsoft.UI.Xaml.Controls.WebView2;
using XFWebView = Xamarin.Forms.WebView;
using XFWinWebView = Xamarin.Forms.PlatformConfiguration.WindowsSpecific.WebView;

[assembly: ExportRenderer(typeof(XFWebView), typeof(RamonDeKlein.UWP.PlatformSpecific.UwpChromiumWebViewRenderer))]
namespace RamonDeKlein.UWP.PlatformSpecific
{
    public class UwpChromiumWebViewRenderer : ViewRenderer<XFWebView, WebView2>, IWebViewDelegate
    {
        private const string LocalScheme = "ms-appx-web:///";

        // Script to insert a <base> tag into an HTML document
        private const string BaseInsertionScript = @"
var head = document.getElementsByTagName('head')[0];
var bases = head.getElementsByTagName('base');
if(bases.length == 0){
    head.innerHTML = 'baseTag' + head.innerHTML;
}";

        private WebView2 _internalWebView;
        private WebNavigationEvent _eventState;
        private bool _updating;

        public void LoadHtml(string html, string baseUrl)
        {
            if (string.IsNullOrEmpty(baseUrl))
                baseUrl = LocalScheme;

            // Generate a base tag for the document
            var baseTag = $"<base href=\"{baseUrl}\"></base>";

            string htmlWithBaseTag;

            // Set up an internal WebView we can use to load and parse the original HTML string
            // Make _internalWebView a field instead of local variable to avoid garbage collection
            _internalWebView = new WebView2();

            // When the 'navigation' to the original HTML string is done, we can modify it to include our <base> tag
            _internalWebView.NavigationCompleted += async (sender, args) =>
            {
                // Generate a version of the <base> script with the correct <base> tag
                var script = BaseInsertionScript.Replace("baseTag", baseTag);

                // Run it and retrieve the updated HTML from our WebView
                await _internalWebView.ExecuteScriptAsync(script);
                htmlWithBaseTag = await _internalWebView.ExecuteScriptAsync("document.documentElement.outerHTML;");

                // Set the HTML for the 'real' WebView to the updated HTML
                Control.NavigateToString(!string.IsNullOrEmpty(htmlWithBaseTag) ? htmlWithBaseTag : html);
                // free up memory after we're done with _internalWebView
                _internalWebView = null;
            };

            // Kick off the initial navigation
            _internalWebView.NavigateToString(html);
        }

        public void LoadUrl(string url)
        {
            var uri = new Uri(url, UriKind.RelativeOrAbsolute);
            if (!uri.IsAbsoluteUri)
                uri = new Uri(LocalScheme + url, UriKind.RelativeOrAbsolute);

            Control.Source = uri;
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                TearDown(Control);
                if (Element != null)
                {
                    Element.EvalRequested -= OnEvalRequested;
                    Element.EvaluateJavaScriptRequested -= OnEvaluateJavaScriptRequested;
                    Element.GoBackRequested -= OnGoBackRequested;
                    Element.GoForwardRequested -= OnGoForwardRequested;
                    Element.ReloadRequested -= OnReloadRequested;
                }
            }

            base.Dispose(disposing);
        }

        protected override void OnElementChanged(ElementChangedEventArgs<XFWebView> e)
        {
            base.OnElementChanged(e);

            if (e.OldElement != null)
            {
                var oldElement = e.OldElement;
                oldElement.EvalRequested -= OnEvalRequested;
                oldElement.EvaluateJavaScriptRequested -= OnEvaluateJavaScriptRequested;
                oldElement.GoBackRequested -= OnGoBackRequested;
                oldElement.GoForwardRequested -= OnGoForwardRequested;
                oldElement.ReloadRequested -= OnReloadRequested;
            }

            if (e.NewElement != null)
            {
                if (Control == null)
                {
                    var webView = new WebView2();
                    Connect(webView);
                    SetNativeControl(webView);
                }

                var newElement = e.NewElement;
                newElement.EvalRequested += OnEvalRequested;
                newElement.EvaluateJavaScriptRequested += OnEvaluateJavaScriptRequested;
                newElement.GoForwardRequested += OnGoForwardRequested;
                newElement.GoBackRequested += OnGoBackRequested;
                newElement.ReloadRequested += OnReloadRequested;
                Load();
            }
        }

        protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            base.OnElementPropertyChanged(sender, e);

            if (e.PropertyName == XFWebView.SourceProperty.PropertyName)
            {
                if (!_updating)
                    Load();
            }
            else if (e.PropertyName == XFWinWebView.ExecutionModeProperty.PropertyName)
            {
                ValidateExecutionMode();
            }
        }

        private void Connect(WebView2 webView)
        {
            if (webView == null)
                return;

            webView.CoreWebView2Initialized += OnCoreWebView2Initialized;
            webView.CoreProcessFailed += OnCoreProcessFailed;
            webView.NavigationStarting += OnNavigationStarting;
            webView.NavigationCompleted += OnNavigationCompleted;
            webView.WebMessageReceived += OnWebMessageReceived;
        }

        private void TearDown(WebView2 webView)
        {
            if (webView == null)
                return;

            webView.CoreWebView2Initialized -= OnCoreWebView2Initialized;
            webView.CoreProcessFailed -= OnCoreProcessFailed;
            webView.NavigationStarting -= OnNavigationStarting;
            webView.NavigationCompleted -= OnNavigationCompleted;
            webView.WebMessageReceived -= OnWebMessageReceived;
        }

        private static void OnCoreWebView2Initialized(WebView2 sender, CoreWebView2InitializedEventArgs args)
        {
            sender.CoreWebView2.Settings.AreDevToolsEnabled = false; /* UWP cannot open the window anyway */
            sender.CoreWebView2.Settings.IsWebMessageEnabled = true;
        }

        private static void OnCoreProcessFailed(WebView2 sender, Microsoft.Web.WebView2.Core.CoreWebView2ProcessFailedEventArgs args)
        {
            Xamarin.Forms.Internals.Log.Warning(nameof(XFWebView), $"Unable to load WebView2 (reason: {args.Reason}, error: {args.ProcessFailedKind}, process description: {args.ProcessDescription}");
        }

        private void OnNavigationStarting(WebView2 sender, Microsoft.Web.WebView2.Core.CoreWebView2NavigationStartingEventArgs e)
        {
            var url = e.Uri;
            if (!string.IsNullOrEmpty(url))
            {
                var args = new WebNavigatingEventArgs(_eventState, new UrlWebViewSource { Url = url }, url);

                Element.SendNavigating(args);
                e.Cancel = args.Cancel;

                // reset in this case because this is the last event we will get
                if (args.Cancel)
                    _eventState = WebNavigationEvent.NewPage;
            }
        }

        private async void OnNavigationCompleted(WebView2 sender, Microsoft.Web.WebView2.Core.CoreWebView2NavigationCompletedEventArgs e)
        {
            SendNavigated(new UrlWebViewSource { Url = sender.Source.AbsoluteUri }, _eventState,  e.IsSuccess ? WebNavigationResult.Success : WebNavigationResult.Failure);

            UpdateCanGoBackForward();

            if (e.IsSuccess && IsJavaScriptAlertEnabled)
            {
                var script = "window.alert = function(msg){{window.chrome.webview.postMessage(JSON.stringify(msg));}};";
                await Control.ExecuteScriptAsync(script);
            }
        }

        private async void OnWebMessageReceived(WebView2 sender, Microsoft.Web.WebView2.Core.CoreWebView2WebMessageReceivedEventArgs args)
        {
            if (IsJavaScriptAlertEnabled)
                await new Windows.UI.Popups.MessageDialog(args.TryGetWebMessageAsString()).ShowAsync();
        }
        private async void OnEvalRequested(object sender, Xamarin.Forms.Internals.EvalRequested eventArg)
        {
            await Control.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal,
                async () =>
                {
                    try
                    {
                        await Control.ExecuteScriptAsync(eventArg.Script);
                    }
                    catch (Exception exc)
                    {
                        Xamarin.Forms.Internals.Log.Warning(nameof(XFWebView), $"Eval of script failed: {exc} Script: {eventArg.Script}");
                    }
                });
        }

        private async Task<string> OnEvaluateJavaScriptRequested(string script)
        {
            return await Control.ExecuteScriptAsync(script);
        }

        private void OnGoBackRequested(object sender, EventArgs eventArgs)
        {
            if (Control.CanGoBack)
            {
                _eventState = WebNavigationEvent.Back;
                Control.GoBack();
            }

            UpdateCanGoBackForward();
        }

        private void OnGoForwardRequested(object sender, EventArgs eventArgs)
        {
            if (Control.CanGoForward)
            {
                _eventState = WebNavigationEvent.Forward;
                Control.GoForward();
            }

            UpdateCanGoBackForward();
        }

        private void OnReloadRequested(object sender, EventArgs eventArgs)
        {
            Control.Reload();
        }

        private void Load()
        {
            Element.Source?.Load(this);
            UpdateCanGoBackForward();
        }


        private void SendNavigated(UrlWebViewSource source, WebNavigationEvent navEvent, WebNavigationResult result)
        {
            try
            {
                _updating = true;
                ((IElementController) Element).SetValueFromRenderer(XFWebView.SourceProperty, source);
            }
            finally
            {
                _updating = false;
            }

            Element.SendNavigated(new WebNavigatedEventArgs(navEvent, source, source.Url, result));

            UpdateCanGoBackForward();
            _eventState = WebNavigationEvent.NewPage;
        }

        private void UpdateCanGoBackForward()
        {
            var wvc = (IWebViewController)Element;
            wvc.CanGoBack = Control.CanGoBack;
            wvc.CanGoForward = Control.CanGoForward;
        }

        private void ValidateExecutionMode()
        {
            if (Element.IsSet(XFWinWebView.ExecutionModeProperty) && Element.On<Xamarin.Forms.PlatformConfiguration.Windows>().GetExecutionMode() != WebViewExecutionMode.SameThread)
                throw new NotSupportedException("WebView can only run on the same thread as the UI");
        }

        private bool IsJavaScriptAlertEnabled => Element.On<Xamarin.Forms.PlatformConfiguration.Windows>().IsJavaScriptAlertEnabled();
    }
}
Related