Blazor WebAssenblyとwin98.cssでダサいアプリをつくる

この記事は[N・S高等学校 Advent Calendar 2021](https://qiita.com/advent-calendar/2021/n-s-highschool-1)の9日目の記事です。

こんにちは！
学校法人角川ドワンゴ学園N Code Labo 秋葉原教室所属講師の土川です。

N Code Laboでは小中高校生向けに`Unity`や`Python`で`機械学習`や`Webバックエンド`、`Swift`でのiOSアプリ開発など実践的なプログラミングコンテンツがあり、2,3人の生徒に対して1人の講師と1人1人の生徒の学びたいことができる環境を整えています。

今回は、Unityでも使われるC#というプログラミング言語でWebフロントエンド開発ができるフレームワーク`Blazor WebAssenbly`とWindows98の見た目を再現できるCSSライブラリである`win98.css`を用いてダサいアプリを作っていきます。

## Blazor WebAssemblyとは？

まず、`Blazor`というのは`C#`と`HTML`でWebフロントエンドが開発出来るフレームワークで、`Blazor WebAssembly`と`Blazor Server`の2種類があります。両者ともUIを`HTML`で書けロジックを`C#`で書けるとまさにC#版`React.js`,`Vue.js`な感じなのですが、動作原理は全く違います。
`Blazor WebAssembly`は最近流行の`WebAssembly`にて`C#`のランタイムを動かし、コンパイルされたILコードをユーザーのブラウザ上にて実行します。
`Blazor Server`はフロントはHTMLにて動き、ロジックは全て`SignalR`にてサーバに送られ処理され実行されます。そのため`PWA`などには対応していません。
上記特徴を踏まえて自分は`Blazor WebAssembly(以降BlazorWASM)`の方をよく利用しています。
ここまででなぜ`Blazor WebAssembly`を使うのか不思議に思っている人もいると思います。`React.js`の方がよく使うしライブラリも多い。`C#`と同様、型が使える`typescript`も利用出来る。`Blazor WASM`は`React.js`などを置き換える物というよりは、特定の条件で使うと有用であるものです。
例えば以下の状況

- サーバー側を`ASP.NET(C#)`で実装している
- クライアント側として既に`C#`を利用しているものがあるとき（特に`Unity`やWinアプリなど)
- `C#`をこよなく愛しているとき

特に最後が重要ですねw

## Blazor WebAssemblyで開発をするには
`.NET SDK`をインストールする必要があります。
導入ですが、[こちらの記事](https://blog.digicre.net/article/104)を参考にしてください(自分がサークルのアドベントカレンダーで書いた記事です)

## プロジェクトを作成する
N高のアドベントカレンダーなのでWindows版の`VisualStudio`というよりは`dotnet`コマンドを使った方法で紹介していきます。

```bash
dotnet new blazorwasm -o Win98Dasai
```

出来た`Win98Dasai`フォルダをVSCodeなどで開きます。

開いた際に案内がでますが、拡張機能`Microsoft.AspNetCore.Razor.VSCode.BlazorWasmDebuggingExtension`をインストールすることをおすすめします。
とりあえず実行してみましょう

```bash
dotnet run
```
```
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: https://localhost:7037
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5079
```
以上のように出たので
`localhost:5079`をブラウザで開いてみましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87699/a3ae638e-b299-d682-ef92-805bd3548695.png)
このように起動出来れば成功です。

では早速挙動を見ていきます。
`/counter`を見てみましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87699/d3ab0c55-2b0e-e12f-018b-7822482cb33e.png)
`Click me`を押すことでカウンタを１ずつ増やせます。
ここのページのコードは`Pages/Counter.razor`で以下のようになっています。

```html
@page "/counter"

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p role="status">Current count: @currentCount</p>

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

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
    }
}

```

`@code`ブロックがC#でロジックを書ける部分です。
`IncrementCount()`メソッドをbuttonの`@onclick="IncrementCount"`でクリックした際に動かすメソッドとして登録しています。
基本的にはこんな感じです。

## Win98.cssの導入

[GitHubのwin98.cssのページ](https://github.com/jdan/98.css)から`style.css`の内容を全てコピーしてプロジェクトの`wwwroot/css/app.css`に追記します。
本当はrazorファイルごとにスタイルを指定することができ便利なのですが、今回は全体に適用したいので`app.css`に追記しています。`app.css`のimportは`wwwroot`フォルダ内の`index.html`に記述されています。

導入完了後、再度`dotnet run`で再実行すると適用されて既にダサさが見え始めていると思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87699/21205677-a5f1-9e51-e3ac-88954c2d7039.png)

さらにダサくするためにbuttonからclass`btn btn-primary`削除します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87699/a642fdb2-8bd2-f41a-ee7c-056675089668.png)
はい。良い感じになってきました。
このままの勢いでこの`win98.css`を用いてToDoリストアプリを作成します。

`Pages/Index.razor`を編集してアプリを作成していきます。

```html
@page "/"

<PageTitle>ToDoｱﾌﾟﾘ</PageTitle>

<div class="window">
  <div class="title-bar">
    <div class="title-bar-text">ToDoﾘｽﾄｱﾌﾟﾘ</div>
    <div class="title-bar-controls">
      <button aria-label="Minimize">_</button>
      <button aria-label="Close">×</button>
    </div>
  </div>
  <div class="window-body container">
    <div class="row">
      <p>新規ﾀｽｸ登録</p>
    </div>
    <div class="row">
      <input class="col-11" type="text" @bind-value="newTaskTextInput" @bind-value:event="oninput" style="width: 490px;"/>
      <button class="col-1" @onclick="AddNewTask">登録</button>
    </div>
    <div class="row">
      <ul>
        @foreach (var todo in toDos)
      {
        <li>@todo.content</li>
      }
      </ul>
    </div>
  </div>
</div>
@code{
  private string newTaskTextInput;
  private List<ToDo> toDos = new();
  class ToDo
  {
    public string uuid{get;set;}
    public string content{get;set;}
  }

  private void AddNewTask()
  {
    toDos.Add(new(){
      uuid=Guid.NewGuid().ToString(),
      content=newTaskTextInput});
  }
}
```
こんな感じで一応ToDoｱﾌﾟﾘは作れますね。ただ、これではブラウザをリロードした際に情報が消えてしまいます。そこで今回はあまり良い方法ではありませんが`localStrage`を使います。
以下のコマンドでLocalStorageを扱うためのライブラリを取得します。

```bash
dotnet add package Blazored.LocalStorage
```

出来たら、以下を`Index.razor`最初に追記します。

```csharp
@inject Blazored.LocalStorage.ILocalStorageService localStorage
```

次に`AddNewTask`を次のように書き換えます。

```csharp
  private async void AddNewTask()
  {
    toDos.Add(new(){
      uuid=Guid.NewGuid().ToString(),
      content=newTaskTextInput});
    string jsonText = System.Text.Json.JsonSerializer.Serialize(toDos);
    await localStorage.SetItemAsync("todos",jsonText);
  }
```

これで保存がされるようになりました。
次にサイト読み込み時に`LocalStorage`から`json`を読み出してListに変換する処理を入れます。
以下の2つのメソッドを`@code`ブロック内に追記します。

```csharp
  protected override void OnInitialized()
  {
    LoadData();
  }
  private async void LoadData()
  {
    string jsonText = await localStorage.GetItemAsync<string>("todos");
    toDos = System.Text.Json.JsonSerializer.Deserialize<List<ToDo>>(jsonText)!;
  }
```

更に、`Program.cs`内に`using Blazored.LocalStorage;`と`builder.Services.AddBlazoredLocalStorage();`を追記します。

```csharp
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Win98Dasai;
using Blazored.LocalStorage;

var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");

builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddBlazoredLocalStorage();
await builder.Build().RunAsync();

```

よし！これで動作するはずですね！ビルドして確認してみます。
おかしい...上手く動きませんね。localStorageには確実に含まれているのに。
こういうときはBlazor側が見た目にも影響する変数が変更されたことを検知していないのが原因なので
`StateHasChanged();`を`LoadData()`の最後にいれ、更新されるようにします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87699/77f5af59-5d73-9486-9c2e-86d8ab3aa0a7.png)

はい。完成です。

## おわり
今回は既存のデスクトップアプリやサーバーサイドのコードを流用しなかったので特にうま味ZEROでしたが、これが複数プラットフォームへ展開する際ならうま味が出てくるはずです。

## おまけ

最後に今回書いたコードを記載しておきます。

```csharp
@page "/"
@inject Blazored.LocalStorage.ILocalStorageService localStorage

<PageTitle>ToDoｱﾌﾟﾘ</PageTitle>

<div class="window">
  <div class="title-bar">
    <div class="title-bar-text">ToDoﾘｽﾄｱﾌﾟﾘ</div>
    <div class="title-bar-controls">
      <button aria-label="Minimize">_</button>
      <button aria-label="Close">×</button>
    </div>
  </div>
  <div class="window-body container">
    <div class="row">
      <p>新規ﾀｽｸ登録</p>
    </div>
    <div class="row">
      <input class="col-11" type="text" @bind-value="newTaskTextInput" @bind-value:event="oninput" style="width: 490px;"/>
      <button class="col-1" @onclick="AddNewTask">登録</button>
    </div>
    <div class="row">
      <ul>
        @foreach (var todo in toDos)
      {
        <li>@todo.content</li>
      }
      </ul>
    </div>
  </div>
</div>
@code{
  private string newTaskTextInput;
  private List<ToDo> toDos = new();
  class ToDo
  {
    public string uuid{get;set;}
    public string content{get;set;}
  }

  private async void AddNewTask()
  {
    toDos.Add(new(){
      uuid=Guid.NewGuid().ToString(),
      content=newTaskTextInput});
    string jsonText = System.Text.Json.JsonSerializer.Serialize(toDos);
    await localStorage.SetItemAsync("todos",jsonText);
  }
  protected override void OnInitialized()
  {
    LoadData();
  }
  private async void LoadData()
  {
    if(!await localStorage.ContainKeyAsync("todos"))return;
    string jsonText = await localStorage.GetItemAsync<string>("todos");
    toDos = System.Text.Json.JsonSerializer.Deserialize<List<ToDo>>(jsonText)!;
    StateHasChanged();
  }
}
```