2020年11月24日火曜日

Blazor WebAssemblyをvscodeで触ってみた

どうも、もりもりです。

今月ついに.NET 5.0がリリースされましたね。 今回は.NETでSPA(SinglePageApplication)を実現できるWebフレームワークの BlazorWebAssemblyを触ってみました。 SPAとは、ページ遷移を行わない単一Webページで動作するWebアプリです。 一般的なWebアプリではJavaScriptを用いてSPAを実現しますが、 C#だけでできてしまうってC#erからしたら嬉しくないですか?

はじめに

Blazorには2種類のモデルがあり、サーバーサイドで動作するBlazor Serverと、 ブラウザ上で動作するBlazor WebAssemblyがあります。

Blazor Serverは、サーバー上の.NET Core環境で主な処理が行われ、サーバー側がブラウザのDOMを変更します。 Blazor WebAssemblyは、最初に.NETアセンブリとランタイムがブラウザにダウンロードされ ブラウザ上で.NETコードを実行し、別途プラグインなどは不要です。

vscodeでBlazor WebAssemblyプロジェクトを作成して、 さらにTodoMVC のようなTodoページを追加してみました。

プロジェクト作成

まずは任意の場所で下記コマンドを叩いてプロジェクトを作成します。

$ dotnet new blazorwasm -o BlazorWasmSample

プロジェクト構成

確認すると初めからテンプレート的な感じで3つほどページが用意されています。

BlazorWasmSample
  ├── App.razor
  ├── BlazorWasmSample.csprojs
  ├── Pages
  │   ├── Counter.razor
  │   ├── FetchData.razor
  │   └── Index.razor
  ├── Program.cs
  ├── Properties
  │   └── launchSettings.json
  ├── Shared
  │   ├── MainLayout.razor
  │   ├── MainLayout.razor.css
  │   ├── NavMenu.razor
  │   ├── NavMenu.razor.css
  │   └── SurveyPrompt.razor
  ├── _Imports.razor
  └── wwwroot
      ├── css
      │   ├── app.css
      │   ├── bootstrap
      │   │   ├── bootstrap.min.css
      │   │   └── bootstrap.min.css.map
      │   └── open-iconic
      │       ├── FONT-LICENSE
      │       ├── ICON-LICENSE
      │       ├── README.md
      │       └── font
      │           ├── css
      │           │   └── open-iconic-bootstrap.min.css
      │           └── fonts
      │               ├── open-iconic.eot
      │               ├── open-iconic.otf
      │               ├── open-iconic.svg
      │               ├── open-iconic.ttf
      │               └── open-iconic.woff
      ├── favicon.ico
      ├── index.html
      └── sample-data
          └── weather.json

ソースコードは下記に置いてます。

アプリ起動

ということで早速実行してみましょう。 BlazorWasmSampleまで移動して下記コマンドで実行します。

$ dotnet run

https://localhost:5001 にアクセスします。 シンプルで今風な感じの画面が表示されました。 Homeメニューには、おなじみの「Hello world」ですね。

Counterメニューをみてみましょう。 ボタンをポチポチするとカウントアップされていきます。

最後はFetch dataページです。 こちらはGrid的な表の画面です。 標準のtableなので編集やソートなどフィルタリングなどはできません。 このあたりはいい感じのパッケージがないかまた探してみます。

ソースを確認

Counterページのソースを確認してみましょう。

Counter.razor

@page "/counter"

<h1>Counter</h1>

<p>Current count: @currentCount</p>

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

@code {
    private int currentCount = 0;

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

これだけで書けてしまうんですね。 すごい。シンプル。

ボタンをクリックすると @code 内の変数 currentCount がインクリメントされ 画面上の @currentCount に反映されるようです。

Todoを追加してみる

◆ 追加 / 編集ファイル

では、TodoMVCのような ページを追加してみましょう。 追加、編集するファイルは下記の4つだけです。

BlazorWasmSample
  ├── Pages
  │   ├── [★] Todo.cs
  │   └── [★] Todo.razor
  ├── Shared
  │   ├── [★] NavMenu.razor
  ├── [★] TodoItem.cs
◆ TodoItemクラス追加

まずはTodoアイテムのクラスを追加します。

TodoItem.cs

public class TodoItem
{
    public string Content {get; set;}
    public bool IsDone {get; set;}
}
◆ Viewと処理を分けてファイルを追加

次に画面と処理ですが、コードが長くなってしまうのでrazorファイルにはViewのみとし、処理はpartialクラスにしてTodo.csと、ファイルを2つに分けて追加します。

Todo.razor

@page "/todo"

<h1>Todo</h1>

・・・

Todo.cs

namespace BlazorWasmSample.Pages
{
    public partial class Todo
    {
        ・・・
    }
}
◆ 入力したTodoアイテムを追加

Todoを入力してEnterでどんどん追加していくようにします。
※現状、Enterを2回押さないとリストに追加されないのでまた時間ある時にデバッグして調べてみます。。。

Todo.razor

<input style="width: 100%" placeholder="What needs to be done?" @bind="newItem" @onkeydown="@Enter" />

Todo.cs

// Todoアイテム全て
private List<TodoItem> allTodos = new List<TodoItem>();
// 追加するアイテム
private string newItem;
// 表示用のTodoアイテム
private List<TodoItem> todosForDisplay = new List<TodoItem>();
// 現在の表示状態
private State currentState;
// 表示状態
private enum State
{
    All,
    Active,
    Completed,
}

/// <summary>
/// アイテムを追加
/// </summary>
private void AddItem() {
    if (!string.IsNullOrWhiteSpace(newItem))
    {
        allTodos.Add(new TodoItem() { Content = newItem, });
        this.ShowTodo();
        newItem = string.Empty;
    }
}

/// <summary>
/// Todoアイテムを表示
/// </summary>
private void ShowTodo()
{
    switch (this.currentState)
    {
        case State.All:
            todosForDisplay = allTodos;
            break;
        case State.Active:
            todosForDisplay = allTodos.Where(x => !x.IsDone).ToList();
            break;
        case State.Completed:
            todosForDisplay = allTodos.Where(x => x.IsDone).ToList();
            break;
    }
}

/// <summary>
/// Enterキーイベント
/// </summary>
/// <param name="e">イベントデータ</param>
private void Enter(KeyboardEventArgs e)
{
    Console.WriteLine($"KeyboardEventArgs : Code[{e.Code}] Key[{e.Key}]");
    if (e.Key == "Enter")
    {
        this.AddItem();
    }
}
◆ Todoリスト表示

追加したアイテムは下記のように表示します。 TodoItem.IsDoneをCheckBoxに、TodoItem.ContentをTextBoxにバインドしてます。

Todo.razor

<ul>
    @foreach (var item in todosForDisplay)
    {
        <li>
            <input type="checkbox" @bind="item.IsDone" />
            <input @bind="item.Content"/>
        </li>
    }
</ul>
◆ Todoアイテムのチェック変更

追加したアイテムのCheckをONにすると取消線を入れ文字色をグレーに、OFFにすると黒に戻すようスタイルと処理を追加します。

Todo.razor

<ul>
    @foreach (var item in todosForDisplay)
    {
        <li>
            <input type="checkbox" @bind="item.IsDone" />
            <input @bind="item.Content" style="color: @GetTextStyles(item).foreColor; text-decoration: @GetTextStyles(item).decoration"/>
        </li>
    }
</ul>

色と装飾をTupleで返します。

Todo.cs

/// <summary>
/// Todoアイテムのテキストスタイルを取得
/// </summary>
/// <param name="item">Todoアイテム</param>
/// <returns>文字色と取消線を付けるかどうかのセット</returns>
private (string foreColor, string decoration) GetTextStyles(TodoItem item)
{
    return item.IsDone ? ("lightgray", "line-through") : ("black", "none");
}
◆ 未完了アイテムの残数表示

未完了のActiveなTodoItemのカウントを左下に表示します。View側に直接LINQでバインドしちゃいます。

Todo.razor

@allTodos.Count(x => !x.IsDone) items left
◆ 表示内容の切り替え

全て / 未完了 / 完了と表示を切り替えるボタンを追加し、メソッドをView側でバインドする。

Todo.razor

<button @onclick="ShowAll">All</button>
<button @onclick="ShowActive">Active</button>
<button @onclick="ShowCompleted">Completed</button>

Todo.cs

/// <summary>
/// Todoアイテム全て表示
/// </summary>
private void ShowAll() {
    currentState = State.All;
    this.ShowTodo();
}

/// <summary>
/// Todo未完了アイテム表示
/// </summary>
private void ShowActive() {
    currentState = State.Active;
    this.ShowTodo();
}

/// <summary>
/// Todo完了アイテム表示
/// </summary>
private void ShowCompleted() {
    currentState = State.Completed;
    this.ShowTodo();>
}
◆ 完了アイテムのクリア

完了アイテムが一つ以上ある場合はClear completedボタンを表示するようにし、 クリックで完了アイテムをメモリから削除する。

Todo.razor

<button @onclick="ClearCompleted" style="display: @GetDisplay()">Clear completed</button>

Todo.cs

/// <summary>
/// Todo完了アイテムをクリア
/// </summary>
private void ClearCompleted()
{
    var completedTodos = allTodos.Where(x => x.IsDone);
    allTodos = allTodos.Except(completedTodos).ToList();
    this.ShowTodo();
}

/// <summary>
/// ClearCompletedボタン表示スタイルを取得
/// </summary>
/// <returns>表示スタイル [inline-block / none]</returns>
private string GetDisplay()
{
    return allTodos.Any(x => x.IsDone) ? "inline-block" : "none";
}
◆ メニューへTodoページを追加

最後に、Todo.razorで指定した @page "/todo" へのリンクをメニューに追加します。

NavMenu.razor

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

さいごに

記事の内容は長くなってしまいましたが、思ってたよりもスッキリとコードが書けるのでサクっと作れてしまいました。 ただブラウザにランタイムや.NETアセンブリをダウンロードして実行するので機能の多いアプリにしてしまうとダウンロードに時間がかかってしまうようです。 それでも手軽にSPAをC#で作れちゃうってなかなかイイですね。

今回追加したTodoはスタイルとか特にセットしてないので味気ない感じになってますが MatBlazorのようなマテリアルデザインのUIフレームワークもあり 面白そうなので近々使ってみようと思います。

以上、もりもりでした。

0 件のコメント:

コメントを投稿