この記事はN・S高等学校 Advent Calendar 2021の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
をインストールする必要があります。
導入ですが、こちらの記事を参考にしてください(自分がサークルのアドベントカレンダーで書いた記事です)
プロジェクトを作成する
N高のアドベントカレンダーなのでWindows版のVisualStudio
というよりはdotnet
コマンドを使った方法で紹介していきます。
dotnet new blazorwasm -o Win98Dasai
出来たWin98Dasai
フォルダをVSCodeなどで開きます。
開いた際に案内がでますが、拡張機能Microsoft.AspNetCore.Razor.VSCode.BlazorWasmDebuggingExtension
をインストールすることをおすすめします。
とりあえず実行してみましょう
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
をブラウザで開いてみましょう。
このように起動出来れば成功です。
では早速挙動を見ていきます。
/counter
を見てみましょう。
Click me
を押すことでカウンタを1ずつ増やせます。
ここのページのコードはPages/Counter.razor
で以下のようになっています。
@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のページからstyle.css
の内容を全てコピーしてプロジェクトのwwwroot/css/app.css
に追記します。
本当はrazorファイルごとにスタイルを指定することができ便利なのですが、今回は全体に適用したいのでapp.css
に追記しています。app.css
のimportはwwwroot
フォルダ内のindex.html
に記述されています。
導入完了後、再度dotnet run
で再実行すると適用されて既にダサさが見え始めていると思います。
さらにダサくするためにbuttonからclassbtn btn-primary
削除します。
はい。良い感じになってきました。
このままの勢いでこのwin98.css
を用いてToDoリストアプリを作成します。
Pages/Index.razor
を編集してアプリを作成していきます。
@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を扱うためのライブラリを取得します。
dotnet add package Blazored.LocalStorage
出来たら、以下をIndex.razor
最初に追記します。
@inject Blazored.LocalStorage.ILocalStorageService localStorage
次にAddNewTask
を次のように書き換えます。
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
ブロック内に追記します。
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();
を追記します。
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()
の最後にいれ、更新されるようにします。
はい。完成です。
おわり
今回は既存のデスクトップアプリやサーバーサイドのコードを流用しなかったので特にうま味ZEROでしたが、これが複数プラットフォームへ展開する際ならうま味が出てくるはずです。
おまけ
最後に今回書いたコードを記載しておきます。
@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();
}
}