.NET MAUI と Blazor を統合させる、.NET MAUI Blazor アプリを触ってみる

2022.06.25

いわさです。

先日、.NET MAUI アプリを少し触ってみたのですが、その際のプロジェクトテンプレートに「.NET MAUI Blazor アプリ」というものがありました。

Blazorをネイティブアプリ上でホスティングしてハイブリッドアプリを作成出来るような仕組みのようです。
本日はこちらを使ってみて、どういうものなのかを調べて、そしてプラットフォーム固有の機能を使った簡単なコンポーネントを実装してみました。

本記事では一部プレビュー機能について触れています。
公式ドキュメントのリンクを多めに入れておいたので、最新情報については公式ドキュメントもご確認ください。

まずは新規作成して実行してみる

新規作成すれば動くはずです。
まずはそのまま実行してみましょう。

マルチプラットフォームカテゴリで「.NET MAUI Blazor アプリ」テンプレートを選択します。

作成後、ターゲットにiOSシミュレーターを選択しデバッグ実行してみます。

実行されました。
メニューにいくつかサンプルページが用意されており動的なコンテンツの変化を確認することが出来ます。
Blazor アプリケーションをブラウザでアクセスしている感じと同じ操作感です。

ライブビジュアルツリーを見てみると、MainPageの中に存在しているコンポーネントはBlazorWebViewのみでした。
どうやら、このBlazorWebViewコンポーネント内でRazorコンポーネントやらがレンダリングされているようです。

コードを少し見てみる

ソリューション構成は .NET MAUI とほぼ変わらないのですが、MainPage.xamlからどのように Blazorアプリケーションが統合されているのか、ソースコードから確認してみたいと思います。

MainPage.xamlではメインコンポーネントとしてBlazorWebViewが配置されており、ホストページとしてwwwroot/index.htmlが指定されており、ルートコンポーネントタイプとしてMainが指定されています。

MainPage.xaml

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:hoge0625mauiblazor"
             x:Class="hoge0625mauiblazor.MainPage"
             BackgroundColor="{DynamicResource PageBackgroundColor}">

    <BlazorWebView HostPage="wwwroot/index.html">
        <BlazorWebView.RootComponents>
            <RootComponent Selector="#app" ComponentType="{x:Type local:Main}" />
        </BlazorWebView.RootComponents>
    </BlazorWebView>

</ContentPage>

次にホストページを確認してみます。

wwwroot/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
    <title>hoge0625mauiblazor</title>
    <base href="/" />
    <link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
    <link href="css/app.css" rel="stylesheet" />
    <link href="hoge0625mauiblazor.styles.css" rel="stylesheet" />
</head>

<body>

    <div class="status-bar-safe-area"></div>

    <div id="app">Loading...</div>

    <div id="blazor-error-ui">
        An unhandled error has occurred.
        <a href="" class="reload">Reload</a>
        <a class="dismiss">🗙</a>
    </div>

    <script src="_framework/blazor.webview.js" autostart="false"></script>

</body>

</html>

ここが、通常のBlazorアプリケーションと異なっている点なのかなと思いました。
通常のBlazorアプリケーションではブートストラップのために以下のようなことを行います。

blazor.server.jsあるいはblazor.webassembly.jsをロードしますが、.NET MAUI Blazorではblazor.webview.jsをロードしています。
blazor.webview.jsの中身を深堀り出来ていないのですが、どうやらこの仕組みを使って.NET MAUI Blazorアプリケーションでは WebAssembly ではなく、ホストアプリケーションの .NETランタイムを使用しているようです。

他の構成は Blazorアプリケーションと概ね変わりません。
メインのレイアウトとサイドバーが用意されており、各コンポーネントでは @page Razorディレクティブを使って各コンポーネントのルートが指定されています。

Shared/MainLayout.razor

@inherits LayoutComponentBase

<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>

    <main>
        <div class="top-row px-4">
            <a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
        </div>

        <article class="content px-4">
            @Body
        </article>
    </main>
</div>

Pages/FetchData.razor

@page "/fetchdata"

@using hoge0625mauiblazor.Data
@inject WeatherForecastService ForecastService

<h1>Weather forecast</h1>

<p>This component demonstrates fetching data from a service.</p>

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    private WeatherForecast[] forecasts;

    protected override async Task OnInitializedAsync()
    {
        forecasts = await ForecastService.GetForecastAsync(DateTime.Now);
    }
}

ネイティブ機能をBlazorから使ってみる

さて、ホストアプリケーションの .NET ランタイムを使っているということなので、プラットフォーム固有機能を Blazor から利用することが出来そうなので簡単な実験を行ってみたいと思います。

ここでは、プラットフォームのデバイス情報を取得しトーストで通知を行ってみます。
デバイス情報の取得は、プラットフォーム固有情報を取得するためのAPIが提供されているのでこちらを使います。

デバイス情報 - .NET MAUI | Microsoft Docs

トースト通知については、.NET MAUI Community ToolkitというNuGetパッケージを使うことで、共通コードでプラットフォーム個別のトースト通知が出来るのでこちらを利用します。

トースト - .NET MAUI Community Toolkit - .NET Community Toolkit | Microsoft Docs

新しく専用ページを用意したいのでナビゲーションメニューに新しい要素を作成します。

Shared/NaviMenu.razor

:

<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
    <nav class="flex-column">
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
                <span class="oi oi-home" aria-hidden="true"></span> Home
            </NavLink>
        </div>
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="counter">
                <span class="oi oi-plus" aria-hidden="true"></span> Counter
            </NavLink>
        </div>
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="fetchdata">
                <span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
            </NavLink>
        </div>
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="hoge">
                <span class="oi oi-list-rich" aria-hidden="true"></span> Hoge
            </NavLink>
        </div>
    </nav>
</div>

:

そして、Pagesディレクトリに新しいRazorコンポーネントを追加しましょう。

ページにはシンプルにボタンをひとつ配置し、クリックイベントを実装します。
その処理の中で.NET MAUIや.NET MAUI Community Toolkitを通してプラットフォーム機能へアクセスしてみます。

Pages/Hoge.razor

@page "/hoge"

<h1>Hoge</h1>

<button class="btn btn-primary" @onclick="HogeClick">Hoge Button</button>

@namespace CommunityToolkit.Maui.Alerts
@code {
    private async void HogeClick()
    {
        var toast = Toast.Make(
                DeviceInfo.Current.Platform.ToString(),
                Core.ToastDuration.Long,
                14);
        await toast.Show();
    }
}

最後にNuGetから、CommunityToolkit.Mauiパッケージを追加しておきます。

実行

iOSシミュレーターとAndroidエミュレーターでそれぞれ実行した結果は以下のようになっています。
期待どおり動作しました。簡単だなー。

さいごに

本日は .NET MAUI Blazor アプリを作成してみました。

WebAssembly は使用しておらず、.NET MAUI と統合されており、プラットフォーム固有機能の呼び出しも .NET MAUI と同様に行うことが出来ました。
ハイブリッドアプリと呼んで良いのかわかりませんが、コンテンツ自体は Razor コンポーネントを使ったページを専用の WebView でレンダリングする形ですが、別途サーバーサイドをホスティングしたり IPC を実装したりする必要なく、 WebAssembly のように ホストアプリケーションの.NETランタイムを通して、プラットフォームの機能へアクセスすることが出来ました。

Blazor アプリケーションでは PWA対応する方法が用意されていますが、より幅広いプラットフォームの機能を使いたい場合などは .NET MAUI へ既存の Blazor アプリケーションを統合させるのも戦略のひとつとなりそうです。