📌

Blazor で画面作るために HTML 使いたくないよぉ… Fluent UI 編

2022/05/14に公開
1

2023/11/27 追記

この記事を書いて 1 年半ほどたって状況が結構変わっていたので新しい記事を書きました。最新では、かなり良くなっているので、そちらを参照してください。

https://zenn.dev/microsoft/articles/aspnetcore-blazor-dotnet8-fluentui

はじめに

これは知っていることや調べたことを体系的にまとめたものではなく、試してみたことの記録用のメモです。なので綺麗にまとまってないし、随所に個人的なお気持ちが散りばめられています。

本文

Blazor は結構好き!でも画面作るためには HTML/CSS が大きな比重を占めてしまうのが現実…。
HTML/CSS でそれっぽい画面を組む以前に、左半分はこれを出して右半分は、上下に分割して、それぞれ別のものを表示したい…!とかボーダーレイアウト組みたいとか、既存の UI フレームワークだと簡単に組めるようなものが結構めんどい…。

grid layout や flex layout で結構マシというか、ちゃんと覚えれば結構いい感じに出来ることはわかってるけど、個人的には今一つプリミティブすぎる機能しか提供されていないと感じてしまいます。やればできる!でも、まだちょっとメンドクサイ。

ということで、HTML/CSS を書いてる感をなるべく減らしつつ画面を作るには、そういったプリミティブな機能の上に構築されたそれなりの UI フレームワークが欲しくなってしまうのが世の常です。

Blazor 向けにもいろいろな UI フレームワークが提供されています。

Awesome Blazor を眺めてるだけでも色々ありそうなことがわかります。

https://github.com/AdrienTorris/awesome-blazor

今回は一応 Microsoft 公式で出ている Fluent UI Web Components の Blazor に対応されたものを試してみようと思います。上に書いたような個人的な不満が解消できるのかどうか…!そこらへんを見ていけたらいいなと思っています。

https://docs.microsoft.com/en-us/fluent-ui/web-components/

試してみよう

Blazor Server プロジェクトを作って以下の手順に従ってプロジェクトに追加していきます。WASM ではなく Server を使っているのは単純に各種デバッグツールの対応が Server のほうが痒い所に手が届くのでよくわからないものを使うときは、WASM でも Server でもいい場合は、Server で試したほうが便利かもという理由です。実際に便利なのかどうかは未知数ですが。

https://docs.microsoft.com/en-us/fluent-ui/web-components/integrations/blazor

Microsoft.Fast.Components.FluentUI を NuGet から追加します。
そして Web Components Script を追加します。CDN からも追加できますが、今回は npm から取得してお腹に抱え込む形でやろうと思います。

とりあえず web-components.min.js があればいいだけみたいなので、以下のようにして取り込みました。

そして、今回は Blazor Server なので _Layout.cshtml に対して web-components.min.js を追加しました。

_Layout.cshtml
@using Microsoft.AspNetCore.Components.Web
@namespace BlazorApp2.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <base href="~/" />
    <link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
    <link href="css/site.css" rel="stylesheet" />
    <link href="BlazorApp2.styles.css" rel="stylesheet" />
    <component type="typeof(HeadOutlet)" render-mode="ServerPrerendered" />
</head>
<body>
    @RenderBody()

    <div id="blazor-error-ui">
        <environment include="Staging,Production">
            An error has occurred. This application may no longer respond until reloaded.
        </environment>
        <environment include="Development">
            An unhandled exception has occurred. See browser dev tools for details.
        </environment>
        <a href="" class="reload">Reload</a>
        <a class="dismiss">🗙</a>
    </div>

    <script src="_framework/blazor.server.js"></script>
    <!-- Add!! -->
    <script type="module" src="lib/fluentui/web-components/dist/web-components.min.js"></script>
</body>
</html>

Blazor WASM の場合は index.html に追加する形になります。そして FluentUI の名前空間に対する using を _Imports.razor に追加します。

_Imports.razor
@using System.Net.Http
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using BlazorApp2
@using BlazorApp2.Shared
@using Microsoft.Fast.Components.FluentUI @* Add!! *@

そしてドキュメントにも書いてある以下の FluentCard のコード例を Index.razor に追加して動くか確認をしましょう!!

Index.razor
@page "/"

<PageTitle>Index</PageTitle>

<h1>Hello, world!</h1>

Welcome to your new app.

<SurveyPrompt Title="How is Blazor working for you?" />

<FluentCard>
  <h2>Hello World!</h2>
  <FluentButton Appearance="@Appearance.Accent">Click Me</FluentButton>
</FluentCard>

実行すると、ちゃんと動いていました。

画面作ってみよう

ということで画面を作ってみようと思います。
コンポーネントの一覧を見てみましょう…。

なんてこった…個々の UI 部品はあるけど個人的に欲しかったレイアウト系コンポーネントはなかった orz

企画倒れた。先に見ておくんだった…。

コンポーネント使ってみよう

というわけでいい感じのレイアウト組みたかったら諦めて CSS を使うとして、個々のコンポーネントを見ていこうと思います。コンポーネントを使う上で個人的にはパット見でわかる使い方とリファレンスがあると使いやすいので、その 2 つを見ていこうと思います。例えば業務システムを組む人なら最初に欲しいなぁと思う DataGrid あたりをピックアップしてみてみようと思います。

以下のページが、ぱっと見使い方を確認できるページのようです。

https://docs.microsoft.com/en-us/fluent-ui/web-components/components/data-grid

Blazor の例がないので最初にくじけそうになりますが、とりあえずこういうのは読み替えが効くようになっていることが多いので、そのまま個々のコンポーネント名やプロパティ名を C# の流儀に従って先頭大文字、単語の区切りは大文字の pascal case にして使ってみようと思います。

ドキュメントに埋め込まれている codepen を見ると fluent-data-grid というタグを使っていて rowsData プロパティにオブジェクトの配列をセットしているので、それを元に Blazor でも設定してみましょう。
FetchData.razor ページが、ちょうど WeatherForecast のデータを表形式で出しているので、以下のような感じで置き換えてみました。

FetchData.razor
@page "/fetchdata"

<PageTitle>Weather forecast</PageTitle>

@using BlazorApp2.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
{
    <FluentDataGrid RowsData="forecasts">

    </FluentDataGrid>
@*    
    オリジナルコードはとりあえず避難
    <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 {
    // List を求められているので配列から List に変更
    private List<WeatherForecast>? forecasts;

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

実行すると残念ながら実行時例外 orz

デバッガーが表示してくれたものを見ると foreach (ColumnDefinition<TItem> column in ColumnDefinitions!) のように力強く ColumnDefinitions は null じゃない!という宣言を行っていますがマウスカーソルを持っていくと null でした。列の自動生成はしてくれないんですね?JavaScript で使うときは自動生成してくれる風なことが書いてあるけど Blazor はダメみたいです。まぁ JS の世界と Blazor の .NET の世界でやり取りしないといけないのでデフォルトではやらないほうが安全っぽいですよね。

ということで Blazor で使う上では ColumnDefinitions プロパティが必須みたいなので設定してみました。ColumnDefinition プロパティの型情報とかからそれっぽく組み立てました。こういうとき型情報があるのはありがたい(でもドキュメントください)。

FetchData.razor
@page "/fetchdata"

<PageTitle>Weather forecast</PageTitle>

@using BlazorApp2.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
{
    <FluentDataGrid RowsData="forecasts" ColumnDefinitions="_columnDefinitions">
    </FluentDataGrid>
@*    
    オリジナルコードはとりあえず避難
    <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 {
    // List を求められているので配列から List に変更
    private List<WeatherForecast>? forecasts;

    // 無ければ作ればいいじゃない?プロパティとかの型情報をもとにそれっぽく作ってみた。
    private ColumnDefinition<WeatherForecast>[] _columnDefinitions = new ColumnDefinition<WeatherForecast>[]
    {
        new("Date", x => x.Date.ToShortDateString()),
        new("Temp. (C)", x => x.TemperatureC),
        new("Temp. (F)", x => x.TemperatureF),
        new("Summary", x => x.Summary!),
    };

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

実行すると今度は表示されました!

なんでずれてるんですか…。

ということで調べてみると、リファレンスとして一番役立つのは microsoft/fast リポジトリ にある各コンポーネントの spec と実際にコンポーネントを試しながら見ることが出来る FAST Component Explorer という感じかなということがわかりました。

https://explore.fast.design/components/fast-accordion

そして、結局 css の grid layout で幅が設定されていて、その幅は GridTemplateColumns プロパティで設定できるということみたいなので明示的に指定することで表示がそろうみたいです。でもデフォルトで 1fr が設定されるという風に書いてあるように見えるのにデフォルトでずれてしまうのが解せぬ…。Blazor だと JS で使っているときは自動で設定される系のものは設定されないことが多いという期待で使ったほうが良さそうな空気を感じます。

ということで以下のように設定すると

<FluentDataGrid RowsData="forecasts" ColumnDefinitions="_columnDefinitions" GridTemplateColumns="1fr 1fr 1fr 2fr">
</FluentDataGrid>

ちゃんと表示されました!

まとめ

fluent ui の react 向けにはレイアウト系コントロールがあったので期待していたのですが、Web Components 版には特に無さそうな感じでした。とりあえず Fluent UI Web Components を使うためには、使い方を調べたり Blazor 用に読み替えたり、表示をカスタマイズしたりといった所までちゃんとやろうと思うと CSS/Web Components/Blazor についてある程度の理解は求められるなという感じだったので、使うときには諦めて覚えていこうと思います…。

HTML/CSS をほぼ意識しなくて済む UI フレームワーク無いかなぁ。他も当たってみよう。

その2 AntDesign 編へ続く

Microsoft (有志)

Discussion

くさばくさば

Version 3でレイアウト系のコンポーネントが追加になっているのでもう一度やってみるのお勧めです。