【C#】WebアプリケーションのレイアウトはCSS Grid Layoutで決まり！

## はじめに
この記事ではC#+WPF開発者向けにCSS Grid Layoutを紹介します。C#開発者以外のみなさんごめんなさい。

## Webアプリケーションのレイアウト、どうするの？
今までデスクトップアプリケーション開発しかしてこなかったC#erのみなさんがある日突然Webアプリケーションを開発することになったときにぶつかる問題、それがレイアウト。

> WPFならできるんじゃ～俺にWPFを返せ！

と発狂？したくなります(なりました)。そこでおすすめしたいのがCSS Grid Layout。

## CSS Grid Layoutとは？
おおよそWPFの `Grid` です。説明終わり。
ちゃんと説明すると、割と新しめのCSSでのレイアウト手法です。比率を含むような様々な範囲で幅・高さを指定して要素を分割できます。WPF(C#のデスクトップアプリケーション用フレームワーク)に近い使用感ということで、従来のWebページというよりはSPAなどのモダンなWebアプリケーションの開発に向いていると思います。

## 使ってみよう
<details><summary>Blazor WebAssemblyでWebアプリケーションを作成中の方へ</summary>
Blazor WebAssemblyの初期のテンプレートでは自動的にFlexboxというレイアウト手法が適用されており、競合しますのでまずそちらを解除します。
SharedディレクトリのMainLayout.razorを開きます。以下の例は.NET6の場合です。.NETのバージョンにより多少違いがあります。

```html:MainLayout.razor
@inherits LayoutComponentBase

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

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

```

`Article` タグのclass属性の値、`content px-4` をBootStrapが認識し勝手にFlexboxレイアウトにされるのでclass属性ごと消します。
(ここまでBlazor WebAssemblyの方のみ)

<hr />

まず、ページの最上位に適当な要素( `div` )を配置し、IDをつけます。
次に、設定したIDに対してCSSを当てて、Grid Layoutを設定します。

```html:index.razor
@page "/"

<style>
    #primaryEditorContainer {
        display: grid;
        width: 100%;
        height: 100vh;
        max-height: 100vh;
        margin: 0px;
        padding: 6px 12px;
    }

</style>

<div id="primaryEditorContainer">

</div>
```

`display:grid` でCSS Grid Layoutになります。
一般にはWebページのレイアウトは無限に長くできる「ページ」のようなものがあり、それに向かってレイアウトするイメージです。
しかし、Webアプリケーションではそのような無限に長くなるページではなく、有限の大きさのデバイスのスクリーンに向かってレイアウトしたいでしょう。

```
width: 100%;
height: 100vh;
max-height: 100vh;
```

`vw` `vh` はデバイスのスクリーンの大きさを基準とした％単位の大きさです。`100vh`と記述することでスクリーンの大きさと同じ大きさになります。
この例では横方向は(ナビゲーションバーが入るので)厳密にスクリーンとは一致させていません。

ここまでの設定により、最上位として設定した `div` 要素はちょうど、WPFのテンプレートで最上位にある `Grid` のように振る舞います。

## 子要素を割り付ける
### レイアウトの定義
子要素の大きさの設定方法を説明します。WPFならば

```xml:MainWindows.xaml
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition />
        <RowDefinition />
    </Grid.RowDefinitions>
</Grid>
```

ですが、CSSでは

```css
#primaryEditorContainer {
        grid-template-columns: 1fr;
        grid-template-rows: 1fr 1fr;
}
```

のように、`grid-template-{なんちゃら}`を使います。
frは見慣れない単位ですが、比率を表すものです。

|WPF|CSS|
|:-:|:-:|
|1*|1fr|
|auto|auto|
|150px|150px|

と、単位系はほぼwpfのGridと互換です。

### 子要素の配置
子要素側の設定は、WPFの場合

```xml:MainWindow.xaml
<Grid Grid.Row="0">
```

ですが、

```html:Index.Razor
<div style="grid-row:1">
```

となります。今回はWPFぽくインラインで設定しましたが、適当なIDを設定して一括で当てても構いません。
インデックスの番号は1始まりです。
あくまでCSSですので、RowとColumnを同時に指定したければ`;`で区切って同時に詰め込んでOKです。

```xml:MainWindow.xaml
<Grid Grid.Row="0" Grid.Column="0">
```

```html:Index.Razor
<div style="grid-row:1;grid-column:1;">
```

## 要素のはみ出し

さて、`Grid`で指定した大きさより子要素の実際の大きさのほうが大きかった場合何が起こるでしょうか。
WPFの場合、はみ出した分が単純に描画されません。その代わり、全体のレイアウトは保たれます。
一方、CSS Grid Layoutでは親要素が勝手に大きくなります。すべてのコンテンツが見える代わりに、レイアウトが崩壊します。
`height:100vh`指定でも平気で画面以上のサイズになります。

はみ出し方を調整するには `overflow-{x or y}:{options}`を子要素に指定します。例えば`overflow-y:scroll`と指定すると縦方向にはみ出しそうになったときにスクロールバーが表示され、はみ出さなくなります。`{options}`の部分に書けるのは、およそ`hidden`(はみ出した分は描画しない,WPFの挙動に近い) `scroll`(スクロールバーを出す) `visible`(はみ出す)の3通りです。完全な説明は[ドキュメント]( https://developer.mozilla.org/ja/docs/Web/CSS/overflow ) を参照してください。

## 上付き・下付き

高さが`auto`または固定長(`150px`,`20vh`など)の要素と高さが比率(`1fr`など)の要素を混在させると上付き・下付きの要素(上下に固定して表示される要素)が作成できます。
WPFと同じ仕様ですが、実際のWebアプリケーション開発では結構役に立つと思います。

## 要素を重ねる

同じ`Row`かつ`Column`の要素を複数用意すると重なります。基本的にWPFと同じです。
子要素に`z-index`をcssで指定すると重ねる順番を変えられます。レイアウトの自由度が広がる便利な機能です。

## その他
WPFの`GridSplitter`に相当するものはないようです。

## 実例
手前味噌ですが、私が開発中のBlazor WebAssemblyを使用したC#開発環境での例をご紹介します。
ロジック部分は省略しますのでお察しください。

現時点での内容と変わらないことは保証できませんが[ここ]( https://2427dkusiro.github.io/WasmCSharpEditor/ )に実物があります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510423/d6266d80-2dd3-fc11-61dd-7ca4364d12f0.png)


```html:CodeEdit.razor
@using System;
@using System.Collections.Generic;
@using System.Linq;

@using UiLogics;
@using WorkerConnection;

@inject HttpClient Http;
@inject IJSRuntime JS;
@inject CompileQueueService CompileQueue;

@page "/CodeEdit"

<style>
    #primaryEditorContainer {
        display: grid;
        width: 100%;
        height: 100vh;
        max-height: 100vh;
        grid-template-columns: 1fr;
        grid-template-rows: 1fr repeat(3,auto) 20vh;
        margin: 0px;
        padding: 6px 12px;
    }

    .width-matchParent {
        width: 100%
    }

    .tab-active {
        background-color: white
    }

    .header-nonselected {
        color: GrayText
    }

    .tab-content {
    }

    .visible {
        visibility: visible
    }

    .hidden {
        visibility: hidden
    }
</style>

<div id="primaryEditorContainer">
    <div style="grid-row:1;overflow-y:scroll;">
        <WasmCsTest.Components.CodeEditor @bind-UserCode="userCode" @bind-UserCode:event="OnChanged" CodeEditorContext="CodeEditorContext" />
    </div>

    <div style="grid-row:2;">
        @if (IsCompiling)
        {
            <button class="width-matchParent btn btn-outline-primary">コンパイル取消(未実装)</button>
        }
        else if (IsRunning)
        {
            <button class="width-matchParent btn btn-outline-primary">プログラム強制終了(未実装)</button>
        }
        else
        {
            <button class="width-matchParent btn btn-primary" @onclick="OnRunButtonClicked">実行</button>
        }
    </div>

    <div style="grid-row:3;">
        <hr />
    </div>

    <div style="grid-row:4;">
        <ul class="nav nav-tabs">
            <li class="nav-item">
                <button class=@tab0.HeaderClassString @onclick="tab0.OnHeaderClick">実行結果</button>
            </li>
            <li class="nav-item">
                <button class=@tab1.HeaderClassString @onclick="tab1.OnHeaderClick">コンソール</button>
            </li>
        </ul>
    </div>

    <div class="@tab0.ContentClassString" style="grid-column:1; grid-row:5; padding:6px;">
        <WasmCsTest.Components.CompileResult CodeEditorContext="CodeEditorContext" />
    </div>

    <div class="@tab1.ContentClassString" style="grid-column:1; grid-row:5;">
        <WasmCsTest.Components.VirtualConsole CodeEditorContext="CodeEditorContext" />
    </div>
</div>
```
| 行 | 内容 | 大きさ |
|:-:|:-:|:-:|
|1|コードエディタ|1fr(残り全部)|
|2|実行ボタン|auto|
|3|境界線(hr)|auto|
|4|タブ切り替えボタン|auto|
|5|コンパイラメッセージ|20vh|
|5|コンソール|20vh|

と設計されています。5行目のタブ部分は、class属性の内容をC#コードとバインディングして、visibilityを書き換えることで擬似的にタブを実装しています。
同じ`row` と `column` を指定して重ねています。重ねる場合は両方明示的に指定しないと重なりません。
1行目が1fr指定なので他の要素は下付きになります。

## さいごに
WPFのグリッドが直線的なレイアウトならほぼ表現できるくらい強力なように、CSS Grid Layoutも非常に強力です。WPFと割と似ているのもC#開発者的には楽なポイントでしょう。