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
Comments (5)
[…] Hosting a Blazor App in WinUI 3 with WebView2 and call a Blazor Component Method from WinUI (Thomas Claudius Huber) […]
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.
You still have two projects, and you need the WebView point to the URL of the Blazor project.
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
hi thomas. can you debug the blazor code in your example application?