Sharing object between .NET host and WebView2

 
 
  • Gérald Barré

WebView2 is a new web browser control for Windows desktop applications. It is based on the Chromium open-source project and is powered by the Microsoft Edge browser. It is available in the Microsoft.Web.WebView2 package. Using this control, you can have a web browser in your application and interact with it. You can use it to display web pages, execute JavaScript code, inject custom CSS, handle navigation events, call .NET methods from JavaScript, and so on. In this post, I describe how to share objects between .NET host and a WebView2 instance, so it can be used from .NET and from JavaScript.

You can expose an object to the WebView2 instance by using the AddHostObjectToScript method. This method takes a name and an object. The object must be public and ComVisible. The name is used to access the object from JavaScript. For example, if you use sample as the name, you can access the object from JavaScript using chrome.webview.hostObjects.sample. As the object must be ComVisible, there are some constraints on the type of the object. For example, you cannot use a Task as a return type. You need to use Task<T> instead. If you want to expose an indexer (e.g. object this[int index] => ...), you need to add the [IndexerName] attribute.

Let's create the WPF application and add a reference to the Microsoft.Web.WebView2 package:

Shell
dotnet new wpf
dotnet add package Microsoft.Web.WebView2

Then, you can add a WebView2 control to the main window:

MainWindow.xaml (XAML)
<Window x:Class="DemoWebView2.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:DemoWebView2"
        xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <wv2:WebView2 Name="webView" />
    </Grid>
</Window>

Then, you can create a class that will be shared between .NET and JavaScript. This class must be public and ComVisible:

C#
// Type must be public and ComVisible
[ComVisible(true)]
public class Sample
{
    public string Name { get; set; } = "meziantou";

    public int ClickCount { get; private set; }

    // Use [IndexerName] if you want to use an indexer from JavaScript
    [System.Runtime.CompilerServices.IndexerName("Items")]
    public int this[int index] => index;

    // Public methods are accessible from JS
    public void OnClick() => ClickCount++;

    // Task is not supported as return type. You need to use Task<T>. Otherwise, you get errors such as:
    // 'System.Threading.Tasks.VoidTaskResult' cannot be marshalled to a Variant.
    public async Task<int> DelayAsync(int milliseconds)
    {
        await Task.Delay(milliseconds);
        return 0;
    }
}

Then, you must register the object in the WebView2 control:

MainWindow.xaml.cs (C#)
public partial class MainWindow : Window
{
    // Shared object between .NET and JavaScript
    private readonly Sample _sample = new();

    public MainWindow() => InitializeComponent();

    protected override async void OnSourceInitialized(EventArgs e)
    {
        await webView.EnsureCoreWebView2Async();
        webView.CoreWebView2.AddHostObjectToScript("sample", _sample);
        webView.Source = new Uri("index.html");
    }
}

Finally, you can use the object from JavaScript:

HTML
<button id="btn" type="button">Click me</button>
<output id="result"></output>

<script>
    document.getElementById("btn").addEventListener("click", async e => {
        // Access the host object asynchronously
        const demo = chrome.webview.hostObjects.demo;
        await demo.DelayAsync(1000);  // Call async methods
        const name = await demo.Name; // Get property value

        // Access the host object synchronously
        // This is not recommended as it may block the UI thread of the webview.
        const demoSync = chrome.webview.hostObjects.sync.demo;
        demoSync.OnClick();                     // Call sync method
        const clickCount = demoSync.ClickCount; // Get property value
        const index = demoSync.Items[0];        // Get indexer value
        demoSync.Name = "new name";             // Set property value

        document.getElementById("result").textContent = `${name} ${clickCount}`;
    });
</script>

#Additional resources

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?Buy Me A Coffee💖 Sponsor on GitHub