Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • JavaScript • Angular
Contact   •   Articles   •   Products   •   Support   •   Search
Ad-free experience sponsored by:
ASPOSE - the market leader of .NET and Java APIs for file formats – natively work with DOCX, XLSX, PPT, PDF, images and more

Chromium WebView2 Control and .NET to JavaScript Interop - Part 2


:P
On this page:

This is Part 2 of a 3 part post series:

In Part 1 of this article I talked about the basics of using the WebView2 control in a WPF application by dropping the control on a form, setting the browser environment and using the .Source property or .NavigateToString() to set the content of the browser.

In this installment I'll talk about how to interact with the WebView's rendered HTML page for basic manipulation of the HTML DOM in a rendered page, as well as calling JavaScript code in the loaded HTML document from .NET and calling back from the JavaScript code into .NET code.

Accessing the HTML DOM and Manipulating Content

If you've used the old IE WebBrowser control before, you're familiar the webBrowser.Document property which allowed direct access to the HTML DOM via .NET API wrapped around the IE COM object interfaces. It allowed for deep interaction with the loaded HTML page, using a .NET API.

Unfortunately, the WebView2 control has no direct interface to the DOM. Instead you need to use a string based scripting function to essentially evaluate a statement or block of statements of JavaScript code.

Currently ExecuteScriptAsync() is the only way to interact with the loaded document.

A quick Review of WebView2 Initialization

To keep this post somewhat self-contained, here's a quick review of how to use the control and initialize it in a .NET application (for more detail see Part1):

  • Ensure the WebView2 Runtime is installed
  • Add the Microsoft.Web.WebView2 NuGet Package
  • Add the WebView Control to your Form, UserControl or other Container Control
  • Set up the WebView Environment during Startup
  • Set the webView.Source property to navigate
  • Or use webView.NavigateToString() to display HTML content from string

To put the control into a WPF Window:

<Window
	xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
/>
...
<DockPanel x:Name="DockWrapper" Grid.Row="2" Panel.ZIndex="2" >
	
    <wv2:WebView2 Name="webView"  Visibility="Collapsed" 
                  Source="{Binding Url}"
    />
</DockPanel>

And to initialize:

// WPF Window
public DomManipulation()
{
    InitializeComponent();

    // get notified when page has loaded
    webView.NavigationCompleted += WebView_NavigationCompleted;
    
    // force to async initialization
    InitializeAsync();
}

async void InitializeAsync()
{
    // must create a data folder if running out of a secured folder that can't write like Program Files
    var path = System.IO.Path.Combine(System.IO.Path.GetTempPath(),"MarkdownMonster_Browser");
    var env = await  CoreWebView2Environment.CreateAsync(userDataFolder: path);
    
    // NOTE: this waits until the first page is navigated - then continues
    //       executing the next line of code!
    await webView.EnsureCoreWebView2Async(env);

    // Optional: Map a folder from the Executable Folder to a virtual domain
    // NOTE: This requires a Canary preview currently (.720+)
    webView.CoreWebView2.SetVirtualHostNameToFolderMapping(
        "test.editor", "HtmlSample",
        CoreWebView2HostResourceAccessKind.Allow);
	
    // You can then navigate the file from disk with the domain
    webView.Source = new Uri(@"https://test.editor/index.html"); 
	
    webView.CoreWebView2.OpenDevToolsWindow();
}

This example initializes the WebView2 control by specifying the location for browser state and then initializes the control, maps a folder and then navigates to a URL.

The code maps a local folder to a virtual domain name (which is optional) which allows navigating a local folder as if it were a WebSite.

Using a virtual domain has a number of advantages over using file:///c:/folder/index.html style syntax:

  • You can run XHR requests in JavaScript code
  • You don't run into zone security issues
  • Pages behave as a Web app would, not file based one

That said, you can still navigate to a file Uri on disk too:

webView.Source = new Uri(Path.Combine(Environment.CurrentDirectory,"HtmlSample\\index.html"));

Make sure the Document is Loaded!

Once you've navigated the document, it loads asynchronously. If you plan on interacting with the DOM or call into JavaScript code, you have to make sure the document is loaded before you start interacting with the document.

To do this you have to either:

  • Load the page and interact with it later
    UI interactions that occur after the document have fully loaded don't need any special attention, unless load is really slow. You can just load and let the UI delay ensure that the page is loaded before you interact with it.

  • Interact with the Page at Load Time
    If however you need to modify the document, or pass data into it on startup, you need to ensure that the page has loaded before you can interact with it. There are events to tell you when the document is ready for interaction.

Two events exist to notify you when the page is done loading:

  • NavigationCompleted
  • CoreWebView2.DOMContentLoaded (not available yet in release SDK)

For current SDK and Control release only NavigationCompleted is available. Using WebView.NavigationCompleted event you can ensure the document is loaded:

public DomManipulation()
{
    InitializeComponent();
    webView.NavigationCompleted += WebView_NavigationCompleted;
    ...
    
    InitializeAsync();
}

private async void WebView_NavigationCompleted(object sender, CoreWebView2NavigationCompletedEventArgs e)
{
    // Now it's safe to interact with the DOM
    var msg = "Hello from a newly loaded .NET window.";
    await webView.ExecuteScriptAsync($"document.getElementById('Notice').innerText = '{msg}';");
}

In the future with WebView2 SDK (721+) a new event CoreWebView2.DOMContentLoaded is available:

webView.CoreWebView2.DOMContentLoaded += CoreWebView2_DOMContentLoaded;

Once available I would recommend DOMContentLoaded as it more specifically ensures that the document has in fact loaded. However, out of necessity I've been using NavigationCompleted to date and that has worked without problems.

Using ExecuteScriptAsync to manipulate the DOM

In the code snippet above you can see a simple example of ExecuteScriptAsync() fired during document startup. That code basically inserts some text into an element in the live HTML DOM.

The signature of ExecuteScriptAsync() looks like this:

public Task<string> ExecuteStringAsync(string scriptToExecute)

Note that this method only accepts a single string parameter which must hold the code to execute as well as any 'data' passed. That's the messy part: Making sure that parameters and data values are encoded properly into a string value, when passed from .NET to JavaScript.

This is a pain, especially if you have to write more than a single statement. Yuk! 💩

But, as bad as this 'API' interface is, it's functional and has good performance. In Markdown Monster I do a fair bit of interaction between .NET and JavaScript and using this limited API gets the job done, even if it's awkward. I'll also show some ways to mitigate the messiness of Interop calls in Part 3 via some abstractions.

The method returns an optional string result for any functions or script code that returns a value or object. You can only return values that are JSON serializable - you can't return a function for example. Functions or script that return no value, return null and you can just ignore the result.

Important: Any result values are returned as JSON encoded strings.

So if you call JavaScript function that returns a complex value or even a string it'll be JSON encoded and you have decode it.

The result type is of Task<string> which means the method is always called asynchronously and expects await or Task handling. If sequence matters make sure you use await or Task continuation blocks to ensure things execute in the correct order. If you don't use await or Task continuations, there's no guarantee that methods will execute or complete in the same order started.

There are a couple of common scenarios where ExecuteScritpAsync() is used:

  • Interacting with DOM Directly
  • Calling into global JavaScript code

Accessing DOM Content

The most basic scenario is directly interacting with the already loaded document and the only way we can do this is via ExecuteScriptAsync().

The following examples come from the HtmlSample page, which has a few buttons that operate on the document and pokes values into the page:

Here are some of the DOM access operations:

During Page Load

modifying DOM content

private async void WebView_NavigationCompleted(object sender, CoreWebView2NavigationCompletedEventArgs e)
{
    // Now it's safe to interact with the DOM after load
    var msg = "Hello from a newly loaded .NET window.";
    await webView.ExecuteScriptAsync($"document.getElementById('Notice').innerText = '{msg}';");
}

This example, simply assigns some text to a DOM element.

From a button Click

interacting with a form and clicking a button

private async void SetInput_Click(object sender, RoutedEventArgs e)
{
    // You can run multiple commands one after the other
    var script = "document.getElementById('txtName').value = 'Dotnetter';";
    await webView.ExecuteScriptAsync(script);

    script = "document.getElementById('btnSayHello').click();";
    await webView.ExecuteScriptAsync(script);
}        

This example assigns a value to the textbox and then simulates a click on the button. Note that I'm running 2 operations as two separate script operations. If you do this, make sure you use await or a Task continuation to make sure the commands run in the correct order and complete one after the other. Without the await both run simultaneously which in this case may produce the wrong result (ie. click happening before the value is set).

The above code is contrived of course and you can simplify it by running it as a single script operation instead:

private async void SetInput_Click(object sender, RoutedEventArgs e)
{
    // alternately you can also combine the operations into a single
    // multi-command script that executes all at once
    var script =@"document.getElementById('txtName').value = 'Dotnetter';
                 document.getElementById('btnSayHello').click();";
    await webView.ExecuteScriptAsync(script);
}

Interoperating between .NET and JavaScript

The previous section gives you some idea how the .NET → JavaScript interaction works using ExecuteScriptAsync() which is the only way to call into the Document and JavaScript code. In this section I'll expand on this concept and then also show how you can do the reverse and call from JavaScript into .NET

Calling a JavaScript Global Function from .NET

The process for calling JavaScript code directly is not very different from invoking the DOM manipulation code I used in the previous section.

The difference is that here I'll call global JavaScript code of the page by accessing global functions or any other global object. Objects have to be in global scope to be accessible by ExecuteScriptAsync().

If you control the content of the page you're interacting with, or you are generating it as part of your application, one way to reduce the messiness of ExecuteScriptAsync() string formatting is to consolidate multiple JavaScript operations as JavaScript functions or helpers inside of the JavaScript document. So rather than screwing around with scripting the JavaScript from .NET with complex string encoding, you can create a wrapper function in JavaScript and call that with a parameter which can be a simple value, or a complex object serialized as JSON.

Keeping with the simplistic HTML page from above, I can use a global JavaScript function in the HTML page like this:

<script>
    var $btn = document.getElementById("btnSayHello");
    $btn.addEventListener("click",(ev)=> ShowMessage());

    // global function - callable with ExecuteScriptAsync
    function showMessage(txt) {
        var $notice = document.getElementById("Notice");
        if(!txt)
            txt = document.getElementById("txtName").value;
        else
            document.getElementById("txtName").value = txt;

        
        var text =  "Hello " + txt + ". Time is: " + new Date().toLocaleTimeString();
        $notice.innerText = text;

        return text ;
    }
</script>

To call that from .NET:

private async void CallFunction_Click(object sender, RoutedEventArgs e)
{
    // If possible the better way to do this is create a function in the page
    // and execute that to combine commands.
    //
    // it'll be easier to debug and easier to pass a single context to
    // instead of passing the same context information to many commands
    var msg = "Hello Mr. .NET Interop";

    // data needs to be encoded as JSON to embed as string
    msg = JsonSerializationUtils.Serialize(msg);

    var script = $"showMessage({msg})";
    
    string text = await webView.ExecuteScriptAsync(script);

    // Important: Results are serialized JSON - including string results
    text = JsonSerializationUtils.Deserialize<string>(text);

    MessageBox.Show(text);
}

Again, contrived example here, but imagine you're running a bit more code to manipulate the document and you can see that the code calling from .NET is a lot simpler making a single function call as opposed to multiple JavaScript commands running in the JavaScript function in the doucment. You remove the bulk of the data encoding, plus you gain the ability to debug the code, which is impossible with scripted code passed in via ExecuteScriptAsync().

The main operation is:

string jsonResult = await webView.ExecuteScriptAsync($"ShowMessage({msg})");

One important thing to note is that the 'data' passed and retrieved has to be JSON encoded. For values embedded, JSON creates strings that are recognizable by JavaScript as values. This applies even to simple values like strings which can contain characters that need to be encoded in order to be used as a string literal in JavaScript. Likewise objects can be encoded as JSON values that can be evaluated. This applies both to parameters you pass to function calls as well as data assignments which all have to be literal values if you inject them from .NET using strings.

Likewise any result that comes back from the JavaScript call - the final text message that is displayed and returned - is returned as a JSON encoded value. In this case it's a string. If you don't deserialize the string you'd get the \n\n in the string instead of the linebreaks:

So it's vitally important to always deserialize any results that comes back from the ExecuteScriptAsync().

If the idea of calling many global function is abhorrent to you, you can also reduce global functionality down into one global object map that holds all the other global functions or data in JavaScript:

<script>
// one global object to consolidate many operations
window.page = {
    data: { ... },
    msg: "My hair's on fire!",
	showMessage: function() { ... }
}
</script>

Then:

await ExecuteScript("page.msg = 'My hair\'s still on fire in .NET';" + 
                    "page.showMessage('hello crazy world');");

Calling .NET Code from JavaScript

You can also call .NET code from JavaScript in the WebView2. There are two mechanisms available to do this:

  • WebMessageReceived event
    This is a string based message API that allows you to essentially push raw string messages into .NET via an event. It's up to the event handler to decide what to do with the incoming string and how to parse it. With a little bit of infrastructure code, you can build a messaging solution that routes actions to specific methods in an Interop class for example.

  • Host Objects passed into JavaScript
    You can use a function to pass a .NET object into JavaScript and expose it to script code via well known host object that JavaScript code can call. This allows more control as you can set property values and call functions directly, although what you can pass as parameters is limited.

Using the WebMessageReceived - one way Messaging into .NET

The CoreWebView2.WebMessageReceived event can be used to send one-way messages into .NET from JavaScript. It's a simple messaging API that basically allows you to pass a string to .NET, but there's no way to return anything back to JavaScript using this API.

In it's simplest form you can use postMessage() in JavaScript like this:

window.page = {
    sendMessage: function (action, data) {
        window.chrome.webview.postMessage('Hello .NET from JavaScript');
    }

You can pass either a string or an object/value to postMessage(). In .NET I can pick up this message using the WebMessageReceived event handler. If you pass a non-string value/object, it's serialized into JSON and you can retrieve it using e.WebMessageAsJson. Strings can be retrieved with e.TryGetWebMessageAsString() in .NET. I find this confusing as this is ambiguous - when is a string JSON or just a string? For this reason I always use strings and explicitly JSON encode and decode on each end.

The most basic syntax looks like this:

// to hook up in ctor or InitializeAysnc()
webView.WebMessageReceived += WebView_WebMessageReceived;

// to handle
private void WebView_WebMessageReceived(object sender, CoreWebView2WebMessageReceivedEventArgs e)
{
	// retrieve raw message (passed as string in JS)
    var string = e.TryGetWebMessageAsString();
    
    // or get a serialized object (passed as object/value in JS)
    // var json = e.WebMessageAsJson;    
    
    // ... do something with the string
}

More realistically though you'd want to handle multiple operations and need to differentiate message types and to do this I personally like to send a JSON message object in the following format:

window.page = {
    sendMessage: function (action, data) {
        var msgObject = { 
        	action: action, 
        	data: data 
        };
        var json = JSON.stringify(msgObject);
        window.chrome.webview.postMessage(json);
    }

By separating the data and the message 'action' I can pick out the action, and based on that 'route' the request to an appropriate handler. By using JSON explicitly when creating the message and receiving it, I can sidestep the confusing auto-JSON conversion that works differently for strings and non-string values. By always passing a JSON string I know that the message is always a JSON string and always will have an action associated with it.

With that I can pick up messages and route them based on the action:

private void WebView_WebMessageReceived(object sender, CoreWebView2WebMessageReceivedEventArgs e)
{
    var json = e.TryGetWebMessageAsString();  // always a JSON string
    if (string.IsNullOrEmpty(json))
        return;

    // retrieve just the action out of the JSON
    var action = WebMessage.ParseAction(json) ?? string.Empty;

    // route and handle messages
    if (action == "showMessage")
    {
        var msg = WebMessage<string>.Parse(json);
        MessageBox.Show(this, msg.Data, "WPF Message from JavaScript",
            MessageBoxButton.OK, MessageBoxImage.Information);
    }
    else if (action == "updateEditor") 
    {
       // ... do something else
    }
    else 
    {
        MessageBox.Show(this, 
            "Message sent from JavaScript without an action:\n\n" + 
            json, "WPF Message from JavaScript",
            MessageBoxButton.OK, MessageBoxImage.Information);
    }
}

If you only need to send messages that are one-way from JS into .NET, and you don't need to retrieve result values, this simple mechanism is the preferred way to call back into .NET according to a Microsoft response. Although it requires serialization to pass data back and forth the mechanism is quite efficient and the COM based messaging we'll discuss next actually uses this same mechanism for the message transport with additional COM overhead.

Using this message based approach allows for quite a bit of flexibility - in fact you could easily route messages, to method calls in an object via reflection for easy message to method mapping for example.

The drawback of this feature is that it's one-way - you can't return a value back after the .NET code has run, so the only way for the .NET code to let JavaScript know it's done is to call back into the document using ExecuteScriptAsync() in a separate Interop call.

Using WebView2 Host Objects - two-way messaging into .NET

The other approach to calling into .NET from JavaScript is to use Host Objects, which is a mechanism that allows you to proxy .NET objects into the WebView control, where they can be accessed to pass data to and receive data back from .NET.

This approach is closer to what we could do in the old IE WebBrowser control where you pretty much could pass any object into the DOM via a JavaScript call, and you could attach it as you needed to.

You explicitly specify that you want to share an object in JavaScript when the control is loaded or each time the control refreshes (if necessary).

Start by creating a class in .NET:

public class DomManipulationDotnetInterop
{
    public string Name { get; set; } = "Rick";
    public string Message { get; set; } = "Hello {0} from .NET. Time is: {1}";

    public string SayHello(string name)
    {
        string msg = string.Format(Message, name, DateTime.Now.ToString("t"));

        MessageBox.Show( msg, "WPF Message from JavaScript",
                        MessageBoxButton.OK, MessageBoxImage.Information);
        return msg;
    }
}

Then in the application startup you can share an instance of it:

// in ctor if it doesn't change for each page, or in `NavigationCompleted`
DotnetInterop = new DomManipulationDotnetInterop();
webView.CoreWebView2.AddHostObjectToScript("dotnet", DotnetInterop);

This now exposes the host object in JavaScript and can be accessed via:

  • window.chrome.webview.hostObjects.dotnet
  • window.chrome.webview.hostObjects.sync.dotnet

In JavaScript code:

// async - requires that .NET Method is async
var msg = window.chrome.webview.hostObjects.dotnet.SayHello("Rick")
				.then( function(msg) { alert(msg); }, 
                       function() { alert('failed'); })

// sync
var msg = window.chrome.webview.hostObjects.sync.dotnet.SayHello("Rick")
alert(msg);

Don't Cache Host Objects

In current previews it's not recommended to cache a host objects beyond a single JavaScript closure context. Caching the objects caused me various problems where the host object would fail and disconnect with very weird behavior when there were overlapping calls to the host object. Make sure you always reload from the base hostObjects instance which ensures a safe instance.

There are two 'versions' of the exposed host object: An async and sync one. The recommendation is to use async whenever possible, but you can also make sync calls.

Unfortunately there are currently problems with async calls into .NET that return values. Basically if you call a .NET async method that returns a value, the value returned is not passed back to JavaScript. However, you can create a sync method in .NET and return a value fine, and you can call the sync method from JavaScript asynchronously (use async/await or a Promise). Currently this is a known issue, but not clear whether this will get fixed in the future or be marked as 'by design'.

Async Result Problems

In the current version there seem to be problems with async calls that return values - I've not been able to get JavaScript await or Promise results to return me a value. I can get the .NET code to fire, but the results don't seem to make it back into JavaScript.

Due to these problems I've been using the following approach:

  • Async signatures for any methods that don't return a value
    This works for fire and forget requests as well as async requests where you need to await for completion, but not a for a result value. Whether you use async should be dictated whether the method does anything that requires async operation.

  • Sync signatures for any methods that return a value
    Sync signatures always work both for sync and async calls from JavaScript. Only sync methods work for returning a value back to JavaScript.

For now this is by design, but this may get fixed in the future. For now it's probably best to implement all JavaScript called methods in .NET as sync methods unless it doesn't return a value or it explicitly needs async access.

Passing Objects to JavaScript

The good news is that you can pass simple objects back from .NET into JavaScript and access those objects in JavaScript code. If the object values can serialize you should be able to access the properties directly in JavaScript.

Here's a simplistic example. In CSharp create a message object and method on the Interop object that can return it:

public class MessageInfo
{
    public string Message { get; set; } = "Test Message";

    public string Type { get; set; } = "Information";

    public int Number { get; set; } = 10;

    public bool IsActive {get; set; } = true;
}

public class DomManipulationDotnetInterop
{
    // ...
    
    public MessageInfo GetMessageInfo()
    {
        return new MessageInfo()
        {
            Message = $"Message from .NET at {DateTime.Now:t}"
        };
    }
}

Then in JavaScript you can retrieve this object like this:

getMessageInfo: function() {
    var msgObject = window.chrome.webview.hostObjects.sync.dotnet.GetMessageInfo();

    document.getElementById('MessageInfo').innerHTML =
        msgObject.Message +
        "<br/>" +
        msgObject.Number +
        "<br/>" +
        "Active: " +
        msgObject.IsActive +
}

The JavaScript message receives this object and you can access the simple properties on this object. You can also nest objects, but there are serialization limitations here so not everything will work. Try to stick to simple types that can be translated easily into JSON as that's what the proxy uses to marshal the values.

Note that the object you get back in msgObject is not a standard JavaScript object, but rather a proxy object. So while you can access the properties by name or indexer as shown above, you can't iterate over them.

Here's a modified version that demonstrates:

getMessageInfo: function() {
    var msgObject = window.chrome.webview.hostObjects.sync.dotnet.GetMessageInfo();

    // note object cannot be parsed - It's a proxy!
    var msg = "";
    for(var prop in msgObject) {
         msg += prop +
         ": " + msgObject[prop] +
             "<br />";
     }
    msg = "Message Object is a Proxy - properties can't be iterated:<br/><br/>" + msg;

    document.getElementById('MessageInfo').innerHTML =
        msgObject.Message +   // direct access works
        "<br/>" +
        msgObject.Number +
        "<br/>" +
        "Active: " +
        msgObject["IsActive"] +   // Property  Indexer works
        "<br/ ><br />" +
        msg;
}

Notice the for loop iterates over the properties to get a list of properties. But this does not give Message, Number, IsActive etc. but rather Proxy properties:

Passing Objects to .NET

Unfortunately passing data back to .NET is not as flexible and only provides superficial object support, as you can only pass string (or no) parameters to .NET methods.

This means you can't officially pass objects, or even more than a single parameter! You can only send what can be represented as a string.

The following .NET signatures cannot be called from JavaScript directly:

// does not work
public string PassMessageInfo(MessageInfo msg)
{
    MessageInfo msgInfo = $"{msg.Message} {msg.Type} {msg.Number} {msg.Inactive}"
    return msgInfo;
}
// does not work 
public string PassMessageInfo(string message, string type, int number)
{
    return $"{message} {type} {number}  ";
}

Both fail with No such interface supported when passing an object, or individual parameters for each of the values.

The only option that works here, is to pass a single string parameter which can be JSON object. For anything but single string parameters JSON is the parameter of choice.

In .NET you can set up the called method like this:

public string PassMessageInfo(string jsonMessageInfo)
{
    var msg = JsonSerializationUtils.Deserialize<MessageInfo>(jsonMessageInfo);
    string message = $"{msg.Message} {msg.Type} {msg.Number} {msg.IsActive}";
    return message;
}

And you call it like it from JavaScript:

passMessageInfo: function () {
    var msgObject = {
        Message: "Hello from JavaScript",
        Type: "Warning",
        Number: 20,
        IsActive: true
    };

    var json = JSON.stringify(msgObject);
    var msg = window.chrome.webview.hostObjects.sync.dotnet.PassMessageInfo(json);
    document.getElementById("ReturnedMessageInfoString").innerText = msg;
}

This is tedious to do, but for me personally not a huge issue as callbacks from JavaScript into .NET tend to be few, while calls from .NET into JavaScript which are more common and better supported via Host Objects are much more common.

Summary

Phew - lot of information here packed into Part 2 for Interop between .NET and the document and JavaScript code and vice versa. There are a lot of options, and unfortunately quite a few unexpected and inconsistent ways that the various interop APIs behave. I hope this article can serve as an introductory reference for what works, although I expect that some of these interface may end up changing to be more flexible with time. Remember although there are 'release' versions out there, the .NET WPF and WinForms control interfaces and .NET SDK part are still in preview.

Basic document interaction and calling of JavaScript functions is supported via ExecuteScriptAsync() and while this string based API can be a messy, it's highly functional and allows for a lot of flexibility in calling all sorts of logic in the HTML document and JavaScript. I've been integrating this functionality in two of my applications with very good results and performance is surprisingly good even with the manual JSON serialization.

For accessing .NET from JavaScript you have several options including using the message based, one-way WebMessageReceived event handler that allows simple message based string messages to be passed from JavaScript to .NET. Host Objects allow exposing .NET objects to JavaScript to provide data sharing and for JavaScript code to call into .NET code with relative ease. There are some tricky issues to deal with still regarding sync and async access, but these issues are likely related to the preview status of the WebView2 control. You also have to deal with limitations of how data can be passed to .NET only via a single string parameter - most likely requiring JSON serialization to pass more extensive data between JavaScript and .NET. It's less of a limitation as just an unexpected inconsistency.

All in all these APIs are simple enough to use and although there are still some preview issues to deal with, I've found the interfaces to perform very well and work reliably for what is currently supported. Unfortunately it's not clear what features will still improve in the future as the documentation for Interop currently is minimal. The road map is not very clear. This post summarizes some of my experiments to figure out what works and what doesn't as trial and error seems to be the only way to figure some of the behaviors out as I go.

This concludes part 2 of this post series about the WebView2 control about Interaction and Interop. In part 3 I'll look at a real world example of how I integrated the WebView control into Markdown Monster and how I abstracted the Interop features to make it easier to call both into JavaScript and .NET.

Resources

this post created and published with the Markdown Monster Editor
Posted in WebView  .NET  WPF  Windows  

The Voices of Reason


 

Ted
February 10, 2021

# re: Chromium WebView2 Control and .NET to JavaScript Interop - Part 2

Great article.

Have you had any luck with accessing iFrame portion of DOM or calling postMessage from inside an iFrame?


Rick Strahl
February 10, 2021

# re: Chromium WebView2 Control and .NET to JavaScript Interop - Part 2

@Ted - I don't see what that wouldn't work, but you have to do it in JavaScript code inside of the page. There's no direct interface to the host, so it's all through messages, so as long as you can access the IFrame in JavaScript you should be able to post message to the Host (although you may need a separate host object for the IFrame content - unless you can pull the content into the peer frame and post it from there).


Ralph
February 11, 2021

# re: Chromium WebView2 Control and .NET to JavaScript Interop - Part 2

@Rick: A perhaps useful addition to your post: When deserializing an object sent from JS to .NET with "postMessage(msg);" and using the property "e.WebMessageAsJson", the "\" characters inserted by WebView2 and the quotation marks at beginning and end of the string must be removed to get a .NET object via System.Text.Json.JsonSerializer.Deserialize (Of myNetObject) (e.WebMessageAsJson). For what reason WebView2 modifies the JSON string in this way has not yet become clear to me ...


Ali
March 09, 2021

# re: Chromium WebView2 Control and .NET to JavaScript Interop - Part 2

Hi Rick, That's a great article. Thanks for sharing that. Have a question. How we could prevent WebView2 from caching (CSS & js)?


Nepa
March 17, 2021

# re: Chromium WebView2 Control and .NET to JavaScript Interop - Part 2

Popped back here looking for a link to part 3. Any time scales when this will go live? PS: Well written series with good technical detail, thanks.


Jussi Palo
March 17, 2021

# re: Chromium WebView2 Control and .NET to JavaScript Interop - Part 2

Thanks, good stuff. Do you know why it is not possible to inject STYLE tags using ExecuteScriptAsync? Tried it via CoreWebView2_DOMContentLoaded and WebView2_NavigationCompleted and neither worked. The below JS works great if I inspect the WebView2 control and execute it in Console.

var style=document.createElement('style');style.type='text/css';style.innerHTML='h1{background-color:#fff}';document.getElementsByTagName('head')[0].appendChild(style)

Rick Strahl
March 17, 2021

# re: Chromium WebView2 Control and .NET to JavaScript Interop - Part 2

@Jussi - sure that should work. Whatever you do with the DOM happens to the DOM. But there are things that might make it not work:

  • Document not loaded yet (make sure you handle the completion events properly)
  • Script errors may not be noticed

Ravi Vanzara
May 10, 2021

# re: Chromium WebView2 Control and .NET to JavaScript Interop - Part 2

I am using Webview2 to load my react.js web application that uses route to change the url but unfortunately it doesn't raise the NavigationStarting event.

Can you suggest some way to detect navigation Change.


Rick Strahl
May 11, 2021

# re: Chromium WebView2 Control and .NET to JavaScript Interop - Part 2

@Ravi - That's because you're not actually navigating the browser in a SPA app. You're only manipulating the history. Not sure if there is something on the control that can handle this, but you can always capture the routing event in the browser and forward it to the host. I think the window.onpushstate event might be what you want to look at and then use JavaScript interop to push the change notification into .NET.


Pankaj
May 25, 2021

# re: Chromium WebView2 Control and .NET to JavaScript Interop - Part 2

Hi, Any pointers to how to capture keypress or click events of ALL controls from a page loaded in webview2 control? My .Net application wants to know which button/control was clicked and possibly pass some additional data along with it to .Net host application.


Peter
May 27, 2021

# re: Chromium WebView2 Control and .NET to JavaScript Interop - Part 2

What well written and informative articles. Not sure where you find time to write any code as it seems to me all your time would go into creating this great content. Looking forward to Part 3. Keep it up. Thanks, Peter


justanotherguy
June 14, 2021

# re: Chromium WebView2 Control and .NET to JavaScript Interop - Part 2

Is it possible to use an HTML editor with webview2 that accesses the dom like webbrowser.document?


Rick Strahl
June 14, 2021

# re: Chromium WebView2 Control and .NET to JavaScript Interop - Part 2

There's is no WebBrowser.Document in WebView2. You can only interact with the browser via ExecuteScriptAsync(codeToEval).

Is it possible to interact with the DOM? Yup and I show examples in this post. Is it as easy as with the Web Browser control? Nope...

If you control the code on the client, it's better to write a front end in JavaScript and interact with that front end from .NET. That way you can do the complex code natively without dealing with string encoding issues. In part 3 I show some helpers that make Interop easier, but even with that it's still a bit of work to go back and forth.


Lorenzo Gonzalez
June 30, 2021

# re: Chromium WebView2 Control and .NET to JavaScript Interop - Part 2

Just wanted to say thank you for such an informative, thorough, and well-written article. Really helped me speed through my project considering this is still such a new bit of SDK and the Microsoft-provided documentation is pretty thin and hard to dig through.


Alex
July 06, 2021

# re: Chromium WebView2 Control and .NET to JavaScript Interop - Part 2

Thanks a lot Rick. Guessing still the one and only comprehensive and concise article about WebView2. Coming from WebrowserControl working at edge-mode actually using IE11. My main reason for heading towards EDGE/chromium based WebView2 control: User that click on links within an iframed Youtube video always starting a blank window using IE11 loosing all "edgy" - optimized disign quirks. Also animated and iframed SVG's e.g. created by inkscape focussed "Sozi" presentation tool now nicely perform within WebView2. Greetings from Munich to Maui.


Tylor Pater
July 20, 2021

# re: Chromium WebView2 Control and .NET to JavaScript Interop - Part 2

Hey, great writeup explaining the different features! I'm trying to call some C# from JavaScript and decided to use the basic message interop, but I'm wondering where the WebMessage class you reference in the code is found, or if it is just a placeholder. I looked through the sample and couldn't find any reference to it.

Thanks!


Ralph Welz
July 21, 2021

# re: Chromium WebView2 Control and .NET to JavaScript Interop - Part 2

Thanks for this great article.

I am also currently in the process of making the jump from TWebbrowser to WebView2, because it looks like MS is now serious about shutting down the old Internet Explorer.

Unfortunately there is hardly any information (on the internet) regarding a migration strategy from the 'outdated' webbrowser ActiveX Control → Webview2.

MS is also very silent, well knowing that the end of Internet Explorer - and the probably associated non-functioning of the old web browser control - will bring down the one or the other application, who are utilizing the webbrowser control.

Your article is a very good start, even if I am investigating for an older Delphi application. Embarcadero has now also taken up a TEdgeBrowser Webview2 component in Delphi 10.4 Sydney, but I will probably do the adaptation in C# because it seems this will become a major conversion (many older approaches won't work with the Webview2 component).

I would like to see some more support/clarification from Microsoft regarding this issue. Especially how it will behave with the web browser control under Windows 11 (or Windows 10 after the cut), but I think, it's will be dead and some access violations will arise. But again, your article is a good start regarding this task.

Regards from Frankfurt (have seen you live on the stage in our youth in Hanau 😃 )


Rick Strahl
July 21, 2021

# re: Chromium WebView2 Control and .NET to JavaScript Interop - Part 2

@Ralph - I don't think there's going to be a problem with existing functionality in the WebBrowser control. I think it's the IE application that's getting pulled out of the OS, but the browser engine is likely to stay for a long time as there are so many apps - including many Microsoft apps (think Outlook) depend on it. It's just not going to get updated in any way. So if it works now with what you're doing it'll continue to.

The concern of course is that you need functionality that the IE engine can't render and there's more and more of that. If you're building apps that use internal content (which is mostly what I do) this isn't much of a problem, but if you interface with external arbitrary HTML and Web sites, then there's lots of stuff that no longer works with even IE11.

I have several old (FoxPro) apps that run using the WebBrowser control and they all use local content and continue to chugg along. I don't see that breaking anytime soon - there are no easy alternatives there in ActiveX space (UI .NET interop isn't much of an option).


Ralph Welz
July 21, 2021

# re: Chromium WebView2 Control and .NET to JavaScript Interop - Part 2

Dear @Rick,

Thanks for your feedback. Shortly after writing my last post, I have seen that you have discussed this issue at your message board some years ago (discussion is still going on there 😃 ). There is still some confusion because there is hardly any information about it. But now I'll wait and see and should a change be necessary, I have the appropriate blueprints in the drawer. Yesterday I designed a first prototype, of course nothing half or whole, but I will probably be able to migrate most of the current functionality if needed (but not all and some major code adjustments will be needed).

Your second article in particular was of great help to me, as the interaction between the desktop app and the web browser (Webview2) is crucial.

In my case the webbrowser view is needed for some map based desktop apps. Using a Javascript library (leaftlet) you can realise a kind of google maps desktop (standalone) app. There isn't any modern 'HTML' functionality needed, but well proved javascript functions that the old webbrowser control can deal with (nearly perfect). I'm using this control at a smaller independent desktop app (too many users to give up on this app, but not enough to completely rebuild it from the scratch). But there are some commercial requests, cause some - so called 'Flottensoftware' (apps for forwarding agencies) - is utilzing the webbrowser control for this task too.

The Internet Explorer and the associated Active X (COM) control gave us developers headaches again and again, but when it worked, it was really great treasure box. When it comes to COM automation, hardly anyone can hold a candle to MS. Today there are of course other approaches, but I am not aware of any good working alternative in the Win32 world. And overflying some of your posts, you're always dealing with the webview2 adaption (like flickering issues for example).

However, the air for C ++ programmers is likely to get thinner and thinner, but it seems that the concepts you mentioned can also be implemented with C ++ (https://docs.microsoft.com/de-de/microsoft-edge/webview2/get- started / win32).

So a lot is happening and who knows, maybe MS will make a tough cut after all to give developers of outdated software a few new projects. It's just a new technology and Microsoft also seems to need doing a lot of fine-tuning at his time. The distribution thing still needs also some finetuning 😃

Maintaining backward compatibility has always been a big feature of MS. After I had to work on some Android projects, I really appreciate that, because in Android you keep stumbling into the so called deprecate trap, nearly everyting is deprecated in the Android world → so Android developers needs a lot of help of the google search machine. Maybe this is the secret of their business model and I'm glad that MS did not make it with Bing) 😃

Regards Ralph


Ralph Welz
August 06, 2021

# re: Chromium WebView2 Control and .NET to JavaScript Interop - Part 2

Rick, you seem to be right 😃 At least the legacy webbrowser control works with the current Windows 11 22000.120 build within a VirtualBox. But the changeover to the WebView2 Control is being tackled anyway 😃 Regards Ralph


Andreas
August 07, 2021

# re: Chromium WebView2 Control and .NET to JavaScript Interop - Part 2

Thanks, Rick, for this great article. Everytime when I'm searching because of a coding problem and West-Wind shows up in the results, it's a "Yeah" and zero hesitation to click the link. Amazing work! It was about time for me to say so.


b.pell
August 28, 2021

# re: Chromium WebView2 Control and .NET to JavaScript Interop - Part 2

First thing, thanks for your articles. They're always a good read.

I thought I'd share in case it's useful to anyone. This is probably hackery but for personal projects, whatever duct tape works in a reasonable time and doesn't crash is thumbs up for me. I'm using a WebView2 and then I have a code editor (AvalonEdit ❤️) which runs either Lua (via NLua) or JavaScript (via Jint). I pass some C# interop objects to scripting environments to allow them to interact with the WebView2 or whatever C# I want them to get to (their calls from those scripts to WebView2 are sync). What I ended up doing was creating a thread safe script result list (script result has an id, the result and if it's still running or not), so WPF would call ExecuteScriptAsync, immediately return an id that represents the script result and then the Jint/Lua can wait for that id to complete. If you do anything beyond trivial the Jint can be weird to read because it's JS executed in Jint then calling JS executed in WebView2 (a few times I forgot context I was in).

C#

public int ExecuteScript(string javaScript)
{
    var result = new ScriptResult()
    {
        Id = GetNextId(),
        IsRunning = true
    };

    ScriptResults.Add(result);

    Application.Current.Dispatcher.Invoke(DispatcherPriority.Normal, new Action(async () =>
    {
        try
        {
             result.Result = await _mainWindow.Edge.ExecuteScriptAsync(javaScript);
        }
        catch
        {
            // TODO: Pass exception info back
            result.IsRunning = false;
        }
        finally
        {
            result.IsRunning = false;
        }
    }));

    return result.Id;
}

Jint

// Call to WebView2
var id = edge.ExecuteScript("Math.abs(-5)");

// These script.LogInfo's are interop calls that write to a WPF control
// via the Dispatcher to the UI.
script.LogInfo("Waiting for id " + id);

var isRunning = true;

while (isRunning) {
        var result = edge.GetResult(id)
        isRunning = result.IsRunning;
	
        if (isRunning == false) {
            script.LogInfo("Result found!");
            buf = result.Result;
        }
	
        script.Pause(10)
}

script.LogInfo("Result is => " + buf);

My result is (noting I ate 10ms with the pause):

8/28/2021 2:12:36 PM: [Jint] [Info] Waiting for id 2
8/28/2021 2:12:36 PM: [Jint] [Info] Result found!
8/28/2021 2:12:36 PM: [Jint] [Info] Result is => 5
8/28/2021 2:12:36 PM: [MainWindow] [Info] Script complete in 34ms

Caveat, I'm by no means a WPF expert, so far I've had no dead locks with this approach (this is what I came up with after an hour of trying to wrangle the WebView2 to make sync calls).


Rene
September 29, 2021

# re: Chromium WebView2 Control and .NET to JavaScript Interop - Part 2

Hi Rick,

nice article!

Do you know if there is a way to detect a mousedown anywhere in the browser?

For example, if I have a movie playing in WebView2 and you click on it, it shows the play/stop/back/forward icons etc., but I like to receive a mousedown event in my code. In fact same applies for any element in the page.

Rene


Goran
October 13, 2021

# re: Chromium WebView2 Control and .NET to JavaScript Interop - Part 2

Hi Rick

First of all, your articles are awesome. 😃

I am currently working (company's project) on the proof of concept for mixing desktop (WPF) and web. We decided to go with CEFSharp and as far as I can conclude comparing with webview2, CEFSharp is far more flexible when it comes to Interop. There is an object binding that allows you to bind simple C# objects to the CEF Browser so they are visible as native JS objects. Same way, C# methods are visible from JS. From the other side (C# → JS), mechanism is the same as webview2 - executing the scripts.

The examples/samples on webview2 are mostly the imitation of web browser which is probably the least useful scenario in real life 😃

Regards Goran


Rick Strahl
October 14, 2021

# re: Chromium WebView2 Control and .NET to JavaScript Interop - Part 2

@Goran - yeah the WebView is not very flexible when it comes to interop, but it's not too difficult to build a reusable interop layer that works and is in the end more reliable than anything that's just 'there'. CEFSharp and WebView2 both use messaging so they're not directly using interop to execute and pass data around.

CEFSharp was just too slow in WPF - it was unusable and also fairly unstable to me, plus you had to distribute the huge runtime. I think the WinForms control is better but for WPF I had nothing but problems with it. CEFSharp also had similar UI quirks that I see with WebView2, so the issue here is partially the Windows control hosting which understandibly is difficult to implement for the controls.

At this point I've got the WebView2 to behave, but it's definitely been a long journey.


West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2021