🍣

Blazor Desktop Apps 面白そうじゃない?

2021/02/27に公開

.NET Conf 2021 focus on Windows を見た

https://youtu.be/mZRNjixZEMg

先日上記の動画を見ていて個人的に気になったのが Blazor セッションでした。
自分の理解が間違っていなかったら Mobile Blazor Bindings が Blazor Desktop Apps になるのではないかと思います。

Blazor Desktop Apps は、Blazor (C# で SPA 作る奴) を使ってデスクトップアプリを作ってしまおうという欲張りプロジェクトです。
Mobile Blazor Bindings 自体は現時点でも試すことが出来ます。

https://docs.microsoft.com/en-us/mobile-blazor-bindings/

試してみよう

プロジェクトテンプレートを入れると現時点でも試せます。以下のコマンドを入れましょう。

dotnet new -i Microsoft.MobileBlazorBindings.Templates::0.5.50-preview

Mobile Blazor Bindings

プロジェクトもさくっと作れます。

dotnet new mobileblazorbindings -o YourProjectName

ベースとしては Xamarin.Forms が現時点では使われているので .NET 6 になると MAUI ベースになる見込みですね。これは以下のように razor ファイルで Mobile Blazor Bindings 用のコンポーネントを使って UI を定義します。

<Frame CornerRadius="10" BackgroundColor="Color.LightBlue">

    <StackLayout Orientation="StackOrientation.Horizontal" HorizontalOptions="LayoutOptions.Center">

        <Button OnClick="@IncrementCount">Increment</Button>

        <Label Text="@("The button was clicked " + count + " times")"
               FontAttributes="FontAttributes.Bold"
               VerticalTextAlignment="TextAlignment.Center" />

    </StackLayout>

</Frame>

@code
{
    int count;

    void IncrementCount()
    {
        count++;
    }
}

タグが特殊な点を覗けば見慣れた Blazor の書き方です。
最終的にネイティブの UI コントロールとしてレンダリングされるので、以下のように Blazor Server や Blazor WebAssembly のノリで button タグとかような HTML を書くとエラーになります。

<Frame CornerRadius="10" BackgroundColor="Color.LightBlue">

    <StackLayout Orientation="StackOrientation.Horizontal" HorizontalOptions="LayoutOptions.Center">

        <Button OnClick="@IncrementCount">Increment</Button>

        <Label Text="@("The button was clicked " + count + " times")"
               FontAttributes="FontAttributes.Bold"
               VerticalTextAlignment="TextAlignment.Center" />

        <button @onclick="IncrementCount">OK</button> @* これはダメ *@
    </StackLayout>

</Frame>

@code
{
    int count;

    void IncrementCount()
    {
        count++;
    }
}

実行するとこうなります。

つまり、基本的には Mobile Blazor Bindings で用意されたコンポーネント (タグ) を使ってアプリを作っていくことになります。

Web テクノロジー使いたいんじゃ…

そうはいっても Blazor が好きな人は HTML/CSS とかが好きな人や、もしくは Blazor のエコシステムが発展していくと色々なコンポーネントが使えるようになると思います。
そんな人向けに Hybrid Blazor apps というものが用意されています。

これは俗にいうガワネイティブなアプリを作るような仕組みです。ガワのほうのネイティブアプリは上でやった Mobile Blazor Bindings のコンポーネントが使えるようです。
試しにプロジェクトを作ってみましょう。以下のようなコマンドで作れます。

dotnet new blazorhybrid -o HybridApp

これが個人的にはかなりアツくて、WebView で Blazor のコンポーネントがレンダリングした HTML の表示や、ボタンクリックなどのイベントのハンドリングなどが行われます。
そして、WebView からイベントやユーザーの入力などが .NET の世界に渡されて処理されます。処理結果でコンポーネントツリーに変更があれば差分が再レンダリングされて、WebView が表示してくれます。

上でいう .NET の世界は、本当に普通の .NET の世界で WebAssembly とかじゃないです。本当にネイティブ アプリとして動いているものになるので、フルの .NET の機能を使ったり出来ますし、やろうと思えば必要に応じて OS ネイティブの API も叩けるはずです。
(複数 OS に対応することを考えると、OS ネイティブ API への依存が増える程辛いのは普通のクロスプラットフォーム開発と変わらないと思いますが。)

ということで Hybrid App の WebView は本当にただレンダリングするだけで、ほぼすべての処理はネイティブアプリとして動いている .NET の世界で行われます。なので、以下のようにネイティブ UI 部分 (画面の上側) と WebView 部分 (画面の下側) でカウンターの値を保持するクラスのインスタンスを共有することもできます。

神ってる。

ちょっと試してみよう

ネイティブ API を呼んでみようと思います。といってもとりあえず Windows 限定で。
最終成果物としてはカウンターページ表示中にカウンターの値が 10 になったら「おめでとう!!」とメッセージボックスを出すアプリで、メッセージボックスの表示に MessageBox.Show メソッドを使おうと思います。

HybridApp プロジェクトに以下のようなインターフェースを定義します。本来は非同期メソッドにすべきですが、まぁ今回は WPF の事しか考えないので同期にしました。

namespace HybridApp
{
    public interface IMessageBox
    {
        void Show(string message);
    }
}

そして、Windows 用のプロジェクトに以下のような感じで実装を定義します。

using System.Windows;

namespace HybridApp.Windows
{
    class WpfMessageBox : IMessageBox
    {
        public void Show(string message)
        {
            MessageBox.Show(message);
        }
    }
}

HybridApp プロジェクトの App クラスに外部からサービスを登録できるようにします。具体的には IServiceCollection をコンストラクタで受け取って、そこにあるサービスを services に追加します。

// 引数追加
public App(IServiceCollection additionalServices = null, IFileProvider fileProvider = null)
{
    var hostBuilder = MobileBlazorBindingsHost.CreateDefaultBuilder()
        .ConfigureServices((hostContext, services) =>
        {
            // Adds web-specific services such as NavigationManager
            services.AddBlazorHybrid();

            // Register app-specific services
            services.AddSingleton<CounterState>();
            // null じゃなかったら services に追加する
            if (additionalServices != null)
            {
                services.AddAdditionalServices(additionalServices);
            }
        })
        .UseWebRoot("wwwroot");

    if (fileProvider != null)
    {

そして、Windows プロジェクトの App.cs で WpfMessageBox を登録した ServiceCollection を App クラスに渡すようにします。

public MainWindow()
{
    Forms.Init();
    BlazorHybridWindows.Init();

    // ここ追加
    var services = new ServiceCollection();
    services.AddSingleton<IMessageBox, WpfMessageBox>();
    LoadApplication(new App(services)); // App クラスに渡す
}

そして、Counter.razor をちょっと改造して IMessageBox を使ってカウント値が 10 になったらダイアログを出すようにします。

@page "/counter"
@inject CounterState CounterState
@inject IMessageBox MessageBox
@implements IDisposable

<h1>Counter</h1>

<p>Current count: @CounterState.CurrentCount</p>

<button class="btn btn-primary" @onclick="CounterState.IncrementCount">Click me</button>

@code {
    private void CounterStateChanged()
    {
        StateHasChanged();
        if (CounterState.CurrentCount == 10)
        {
            MessageBox.Show("おめでとう!!");
        }
    }

    protected override void OnInitialized()
    {
        CounterState.StateChanged += CounterStateChanged;
    }

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

実行するとちゃんとカウンターのページを出した状態でカウント値が 10 になるとダイアログが出ます。間違いなく MessageBox.Show で表示されるやつですね。

まとめ

Blazor Desktop Apps 結構楽しみですね。WPF といい WinForms といい MAUI といい、WinUI 3.0 といい Blazor Desktop Apps といい、デスクトップ開発の選択肢がカオスなので何処かで整理しないといけないなぁと思いつつ、追いかけるものが沢山あるというのは個人的には楽しいのでとてもいいですね!

Microsoft (有志)

Discussion