🎉

.NET 6 Preview 3 の Windows アプリに Blazor を埋め込む BlazorWebView を試してみた

2021/04/09に公開

こちらの記事の内容を動画にしました。

https://www.youtube.com/watch?v=GE1CR4mb9hc

本文

🎉.NET 6 Preview 3 が出ましたね!🎉

https://devblogs.microsoft.com/dotnet/announcing-net-6-preview-3/

https://devblogs.microsoft.com/aspnet/asp-net-core-updates-in-net-6-preview-3/

ASP.NET Core updates の方のブログ記事の中に以下の一文を見つけました。

BlazorWebView controls for WPF & Windows Forms

ということで試してみました。

元々公開しようとは思ってなかったのでプロジェクト名が WpfApp1 ですが、実際に動くようになったコードは以下に公開しています。

https://github.com/runceel/dotnet6_blazorwebview_wpf

実際に動かすと以下のようになります。上部分が WPF アプリで下部分が Blazor で作った画面になっています。カウンターの値が WPF と Blazor の画面間で共有できていることがわかると思います。

WPF アプリの場合はプロジェクト ファイルの Sdk 属性を Microsoft.NET.Sdk.Razor に変更して Microsoft.AspNetCore.Components.WebView.Wpf パッケージを追加すれば使えるようになります。後は index.html や razor ファイルを追加して開発が出来ます。まだ razor ファイルのインテリセンスが凄く弱かったりしますが、ここらへんは追々改善されていくと思います。

プロジェクト構造的には以下のような雰囲気になります。xaml と razor ファイルが同居してるのが新鮮ですね。

ポイントとしては ServiceCollection をアプリの何処かで初期化して BlazorWebView に設定することで、ServiceCollection に設定したものを Blazor 側で使うことが出来ます。(もちろん、WPF と Blazor のアプリは同じプロセスで動いているので static 変数や他の方法でインスタンスの共有は出来ますが行儀はよくないかも)

WPF でも ServiceCollection に登録したものを使うことで Blazor 側と同じインスタンスを取得してステートなどの共有も出来ます。
作成したサンプルでは、CounterState クラスを razor ファイル内で使用しています。

Counter.razor
@implements IDisposable
@inject WpfApp1.CounterState CounterState

<h1>ここは Web !!</h1>

<p>The current count is: @CounterState.Value</p>
<button @onclick="IncrementCount">Count</button>

@code {
    protected override void OnInitialized()
    {
        this.CounterState.PropertyChanged += CounterStateChanged;
    }

    public void Dispose() 
    {
        this.CounterState.PropertyChanged -= CounterStateChanged;
    }

    private void CounterStateChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) =>
        this.StateHasChanged();

    void IncrementCount()
    {
        CounterState.Increment();
    }
}

WPF 側でも ServiceCollection に登録してある CounterStateDataContext に設定して利用しています。

MainWindow.xaml.cs
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Windows;

namespace WpfApp1
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            this.InitializeComponent();
            var provider = App.Current.Resources["services"] as IServiceProvider;
            this.DataContext = provider.GetRequiredService<CounterState>();
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            ((CounterState)this.DataContext).Increment();
        }
    }
}

ServiceCollection の初期化は App クラスの Startup イベントで行って Resources に登録することでアプリ全体で共有するようにしています。

App.xaml.cs
private void Application_Startup(object sender, StartupEventArgs e)
{
    var serviceCollection = new ServiceCollection();
    serviceCollection.AddBlazorWebView();
    serviceCollection.AddSingleton<CounterState>(); // このインスタンスを Blazor と WPF で共有している
    Resources.Add("services", serviceCollection.BuildServiceProvider());
}

Blazor の世界に渡すには BlazorWebViewServices プロパティに設定して渡しています。

MainWindow.xaml
<blazor:BlazorWebView
    Grid.Row="1"
    HostPage="wwwroot/index.html"
    Services="{StaticResource services}">
    <blazor:BlazorWebView.RootComponents>
        <blazor:RootComponent ComponentType="{x:Type local:Counter}" Selector="#app" />
    </blazor:BlazorWebView.RootComponents>
</blazor:BlazorWebView>

まとめ

とりあえず試してみた感じですが、結構気軽に Blazor を使って画面を作って WPF (と、今回は試していませんが WinForms) に埋め込むことが出来るようになっています。
Web 系の技術だと簡単に出来るのに…という表現については、これを使うとうまくハマると簡単に実現できるケースもあるかなと思いました。

あとは「XAML ??なるほど、わからん。」という感じの Web 系よりの人にとっては、もう全面 BlazorWebView にして開発してしまうのもいいと思います。そこら辺の匙加減は、自由に出来るのが素晴らしいですね。

.NET 6 は結構盛沢山で見ていて楽しいですね!

Microsoft (有志)

Discussion