From WebView to WebView2 in UWP

In the old days, accessing a local website in UWP was relatively easy. Even if you wanted to load your third-party libs. The basic code you required:

The WebView:

<WebView x:Name="webView1" Source="ms-appx-web:///Assets/index.html"
NavigationStarting="WebView1_NavigationStarting"
NavigationCompleted="WebView1_NavigationCompleted" />

Here you defined the Source and some extra events like NavigationStarting or NavigationCompleted for loading your JS or enabling JS interaction with your third-party lib (that you had to add as a reference in your main project).

Loading your third-party lib:

private void WebView1_NavigationStarting(WebView sender,
WebViewNavigationStartingEventArgs args)
{
    sender.AddWebAllowedObject("CallJSCSharp", new CallJSCSharp());
}

Adding extra code to load additional JS:

private async void WebView1_NavigationCompleted(WebView sender,
WebViewNavigationCompletedEventArgs args)
{
    string filename = $"ms-appx:///Assets/js/lang/en.json";
    StorageFile file = await StorageFile.GetFileFromApplicationUriAsync(
new Uri(filename));
    string contents = await FileIO.ReadTextAsync(file);

    string[] arg = { contents };

    _ = await webView1.InvokeScriptAsync("UpdateData", arg);
}

And this is the function to execute extra functions from my app:


namespace CallJSInterface
{
    private Windows.System.Display.DisplayRequest _displayRequest;
   
    public sealed partial class CallJSCSharp
    {
        public void KeepScreenOn()
        {
            //create the request instance if needed
            if (_displayRequest == null)
            {
                _displayRequest = new Windows.System.Display.DisplayRequest();
            }

            //make a request to put in an active state
            _displayRequest.RequestActive();
        }
    }
}

Load Website

Now, what are all the changes required for the new WebView2? There are several changes to be considered. The first one is to download the lib Microsoft.UI.Xaml from NuGet in your main project:


This lib allows you to add WebView2. It requires two changes:

Add this lib in your XAML's libs:

xmlns:controls="using:Microsoft.UI.Xaml.Controls"

And then, you can add your WebView2:

<controls:WebView2 x:Name="webView1"
NavigationCompleted="WebView1_NavigationCompleted" />

Now comes the tricky parts. The first change is about the Source. With WebView2, we can only load external websites like bing.com or facebook.com. Any local website is going to fail.

The new approach follows creating an asynchronous function that creates a virtual host (it has to be called in the Constructor), where you map your current local website like this:

private CoreWebView2 core_wv2;

private async void LoadLocalPage()
{
    string fakeDomain = "myapp.something";
    string assetsLocation = "Assets";
    string firstPage = "index.html";

    await webView1.EnsureCoreWebView2Async();
    core_wv2 = webView1.CoreWebView2;
    if (core_wv2 != null)
    {
        core_wv2.SetVirtualHostNameToFolderMapping(
            fakeDomain, assetsLocation,
            CoreWebView2HostResourceAccessKind.Allow);

        webView1.Source = new Uri($"https://{fakeDomain}/${firstPage}");
    }
}

Every path that you have in your old app looks like this:

ms-appx-web:///Assets/

must be changed to:

https://myapp.something/

Therefore, if you loaded Bootstrap like this:

ms-appx-web:///Assets/css/boostrap.min.css

Now, it is going to be like this:

https://myapp.something/css/bootstrap.min.css

Invoke 3rd-party Libs from JS

The next part is how to interact with your third-party lib, and this part is complicated.

The first part involves adding a new and very specific C++ project, Windows Runtime Component (C++/WinRT), to your solution that must be called WinRTAdapter.


You must install a lib from NuGet Microsoft.Web.WebView2:


Add as a reference your third-party lib.

Go to your C++ project properties go to Common Properties and choose WebView2:


Here you have to do four changes:
  1. Set Use WebView2 WinRT APIs to No.
  2. Set Use the wv2winrt tool to Yes.
  3. Set Use Javascript case to Yes.
  4. Edit Include filters and add the following ones:
Windows.System.UserProfile
Windows.Globalization.Language
CallJSInterface
CallJSInterface is the name of my third-party's namespace.



You click on OK and build your C++ lib.

After you have built your C++ lib (WinRTAdapter), you must add it to your main project as a reference.

Now, we need to do some changes to be able to invoke the functions from our third-party lib. The first one is to register it. We do it in the same LoadLocalPage() function from before or on NavigationCompleted:

var namespacesName = "CallJSInterface";
var dispatchAdapter = new WinRTAdapter.DispatchAdapter();
core_wv2.AddHostObjectToScript(namespacesName,
dispatchAdapter.WrapNamedObject(namespacesName, dispatchAdapter));

Where CallJSInterface is your namespace. After this, you need to register your function in your JS like this:

var callJS;
if (chrome && chrome.webview) {
    chrome.webview.hostObjects.options.defaultSyncProxy = true;
    chrome.webview.hostObjects.options.forceAsyncMethodMatches = [/Async$/];
    chrome.webview.hostObjects.options.ignoreMemberNotFoundError = true;
    window.CallJSInterface = chrome.webview.hostObjects.sync.CallJSInterface;
    callJS = new CallJSInterface.CallJSCSharp();
}

Where CallJSInterface is one more time your namespace. Now, you can invoke JS like this (the async() is mandatory):

callJS.async().KeepScreenOn()

Inject JS code

The last part is how to load your scripts as before. The old function InvokeScriptAsync("UpdateData", arg) doesn't exit. The best way is to create a class Extension like this:

public static class Extensions
{
    public static async Task<string> ExecuteScriptFunctionAsync(this WebView2 webView2,
string functionName, params object[] parameters)
    {
        string script = functionName + "(";
        for (int i = 0; i < parameters.Length; i++)
        {
            script += JsonConvert.SerializeObject(parameters[i]);
            if (i < parameters.Length - 1)
            {
                script += ", ";
            }
        }
        script += ");";
        return await webView2.ExecuteScriptAsync(script);
    }
}

Now, from the NavigationCompleted, you can load it in the old way:

 _ = await webView1.ExecuteScriptFunctionAsync("UpdateData", arg);

With all these steps, you can do it again your entire process.

Banner credits:

Comments