Hosting a Blazor App in WinUI 3 with WebView2 and call a Blazor Component Method from WinUI

WinUI 3.0 is Microsoft’s upcoming UI framework to build modern, native Windows applications.

WinUI is developed open source on https://github.com/microsoft/microsoft-ui-xaml

Last week WinUI 3.0 alpha 2 came out, and Microsoft introduced a WebView2 control that is based on Microsoft Edge Chromium. That means you can run all the modern, awesome web stuff in that WebView2 control if you want.

The Goal

I thought I give WebView2 a try, and hey, why not trying to host a web app in WebView2 that is built with my favorite Single Page Application (SPA) framework: Blazor!

But just hosting would be easy. I want to call a method in the Blazor App from my WinUI app.

What I created

What I ended up is the simple WinUI 3 app that you see below.

At the top of the WinUI application is a WinUI TextBox where you can enter a firstName (it contains “Thomas” above), and a WinUI Button with the Text “Update Blazor from WinUI”.

The Blazor application is shown at the bottom of the application window in a WebView2 control. The Blazor app contains a simple component with an input field that contains the text Julia.

When you click on the WinUI Button “Update Blazor from WinUI” , the text in that Blazor component’s input field is updated with the text from the WinUI TextBox. Voila:

To implement this WinUI to Blazor communication, you could of course use some kind of server communication via SignalR. With SignalR, the WinUI app would call the server, and the server would call into the Blazor app. But instead of using SingalR, I wanted to call into the Blazor app directly, as it is already in the WinUI app.

I managed to call from WinUI into Blazor via Blazor’s JavaScript Interop. The WebView2 has an ExecuteScriptAsync method that allows you to execute JavaScript code in the hosted web app.

Show Me the Code

All the code of this post is available on GitHub: https://github.com/thomasclaudiushuber/WinUI3-WebView2-Hosting-BlazorApp

Let’s look at the most important parts, and let’s start with the Blazor app.

The Blazor App

I created a new Blazor WASM App. I removed all the page components except the Index.razor file, and in that file I created the following content:

@inject IJSRuntime JSRuntime;

@page "/"

<h1>Hello @firstName</h1>

<div class="form-group">
  <label>Firstname</label>
  <input type="text" class="form-control" @bind="firstName" />
</div>

@code{

  private string firstName = "Julia";

  protected async override Task OnAfterRenderAsync(bool firstRender)
  {
    if (firstRender)
    {
      await JSRuntime.InvokeVoidAsync("interopFunctions.registerIndexComponent",
        DotNetObjectReference.Create(this));
    }
  }

  [JSInvokable]
  public void SetFirstName(string firstName)
  {
    Console.WriteLine("Blazor SetFirstName executed");

    this.firstName = firstName;
    StateHasChanged();
  }
}

As you can see, the Index component has an HTML input element that is bound to the firstName field of the component. In the code section there’s a SetFirstName method that is decorated with the JSInvokable attribute. That means that this instance method can be invoked from JavaScript. But to invoke this instance method from JavaScript, you need an instance of the Blazor component in JavaScript. So, let’s look at the components OnAfterRenderAsync method.

In that method, the injected IJSRuntime (stored in aJSRuntime property) and its InvokeVoidAsync method are used to call the JavaScript method interopFunctions.registerIndexComponent:

await JSRuntime.InvokeVoidAsync("interopFunctions.registerIndexComponent",
  DotNetObjectReference.Create(this));

A reference for the Blazor component is passed to that JavaScript function. The reference for the component is created with the DotNetObjectReference.Create method. The this keyword points to the component instance. Now let’s look at that JavaScript method interopFunctions.registerIndexComponent.

The JavaScript Interop code

In the wwwroot folder of the Blazor app I have created a JavaScript file with the content below:

var interopFunctions = {};

interopFunctions.registerIndexComponent = (dotnetObj) => {
  interopFunctions.indexComponent = dotnetObj;
};

As you can see, an interopFunctions variable is created and an empty object is assigned to it. On that object, a registerIndexComponent method is created. It contains a dotnetObj parameter. This registerIndexComponent method is called from the Blazor’s Index component like described in the previous section. That means that the dotnetObj parameter actually contains the reference to the Index component. In the JavaScript function above that reference is stored in an indexComponent property on the interopFunctions object.

Now, that means that we have that Index component instance in JavaScript. And as we have that instance, we can write the code to invoke its SetFirstName method from JavaScript. To do this, I’ve created a setFirstName method on the interopFunctions object:

interopFunctions.setFirstName = (firstName) => {
  console.log("JavaScript setFirstName executed");

  interopFunctions.indexComponent.invokeMethodAsync('SetFirstName', firstName);
};

As you can see in the code above, the setFirstName method has a firstName parameter. Internally, it grabs the indexComponent instance, and on that instance it calls the invokeMethodAsync method to call the Blazor component’s SetFirstName method with the specified firstName.

Great, now the Blazor app and the JavaScript interop code are done. From the WinUI app, we just need to call the interopFunctions.setFirstName JavaScript method. But before we do that, you can try if the Blazor app and the JavaScript interop code are working.

Just run the Blazor app, open the browser’s console via F12. In the console, you can call the interopFunctions.setFirstName function like this:

interopFunctions.setFirstName('from the Console');

In the screenshot below I entered the function call in the browser’s console.

Now when I pressEnter to execute the interopFunctions.setFirstName function, you can see the updated text in the Blazor app:

Great, it works. Now let’s call the interopFunctions.setFirstName function from the WinUI app.

The WinUI app

To use the WebView2 in your WinUI 3 app, you need to install the NuGet package Microsoft.Web.WebView2. Note that I’m using here also a WinUI 3 alpha 2 app. This should not be used in production. You find more information about WinUI 3 here: https://docs.microsoft.com/en-us/uwp/toolkits/winui3/

In the MainPage.xaml file of the app, I have the content below:

<Grid>
  <Grid.RowDefinitions>
    <RowDefinition Height="Auto"/>
    <RowDefinition Height="Auto"/>
    <RowDefinition/>
  </Grid.RowDefinitions>

  <StackPanel>
    <TextBlock FontSize="28" Text="WinUI Controls" Margin="10"/>
    <TextBox Header="FirstName from WinUI" Text="Thomas" x:Name="txtFirstName" Margin="10"/>
    <Button Content="Update Blazor from WinUI" Click="ButtonSetFirstName_Click" Margin="10 0 10 10"/>
  </StackPanel>

  <TextBlock Grid.Row="1" FontSize="28" Text="WinUI Web View 2 with Blazor App" Margin="10 20 10 0"/>

  <WebView2 Grid.Row="2" x:Name="webView2" Margin="10"></WebView2>
</Grid>

As you can see above, the TextBox has the name txtFirstName to access it from codebehind, the WebView2 has the name webView2 to access it from codebehind, and there’s an Event Handler for the Button‘s Click event. Now let’s look at the codebehind file MainPage.xaml.cs.

In the MainPage constructor, I set the UriSource property of the WebView2 to the Uri where the Blazor app is running (We could also set this property in XAML, of course).

public MainPage()
{
  this.InitializeComponent();
  webView2.UriSource = new Uri("https://localhost:44305/");
}

Then there’s the Click event handler. It uses the ExecuteScriptAsync method of the WebView2. As you can see below, the interopFunctions.setFirstName JavaScript method is executed, and as an argument, the text of the txtFirstName TextBox is passed in.

private async void ButtonSetFirstName_Click(object sender, RoutedEventArgs e)
{
  await webView2.ExecuteScriptAsync($"interopFunctions.setFirstName('{txtFirstName.Text}');");
}

That’s it. Now the WinUI 3 app can actually set the FirstName property of the Blazor component via Blazor’s JavaScript Interop functionality.

That’s awesome, because this scenario means that you can easily integrate any Blazor component in your WinUI app that needs just data, like for example Chart controls.

Developer Tip

When the WebView2 control in your WinUI app has the focus, you can actually hit F12 to open up the developer tools. The developer tools will open up in a new window. That’s very usable to debug and to see errors.

Trying the app

As mentioned, you can download the app from this post from https://github.com/thomasclaudiushuber/WinUI3-WebView2-Hosting-BlazorApp. Ensure that you start the Blazor app before you start the WinUI app, so that the WinUI app can load the Blazor app into the WebView2 control.

Summary

This post showed that it’s possible to integrate Blazor Apps and Components via WebView2 in your WinUI 3 app. The communication was from WinUI to Blazor. In the next post, I will show you how to communicate in the other direction: From Blazor to WinUI.

Happy coding,
Thomas

Share this post

Comments (4)

  • Dew Drop – February 18, 2020 (#3135) | Morning Dew Reply

    […] Hosting a Blazor App in WinUI 3 with WebView2 and call a Blazor Component Method from WinUI (Thomas Claudius Huber) […]

    February 18, 2020 at 2:14 pm
  • Brett Reply

    How do you merge them into a single project? I’m trying to do this with WPF: I have a WPF WebView 2 project and a Blazor Server project. I can’t figure out how to make them a single program that could host Blazor and also display it all in one.

    November 9, 2020 at 10:48 pm
    • Thomas Claudius Huber Reply

      You still have two projects, and you need the WebView point to the URL of the Blazor project.

      December 21, 2020 at 3:23 pm
  • John Reply

    Hi Thomas, I ran two projects simultaneously, and the WinUIHostingChromiumWebview one showed a suitable version of Microsoft edge was not detected although I’ve already installed one

    December 17, 2020 at 12:51 am

Leave a Reply to Brett Cancel reply

Your email address will not be published. Required fields are marked *

*

This site uses Akismet to reduce spam. Learn how your comment data is processed.