ラベル matblazor の投稿を表示しています。 すべての投稿を表示
ラベル matblazor の投稿を表示しています。 すべての投稿を表示

2021年1月11日月曜日

Blazor向けUIコンポーネントMatBlazorでマテリアルデザイン

どうも、もりもりです。

前回は .NETでSPA(SinglePageApplication)を実現できるWebフレームワークの BlazorWebAssemblyでTodo画面を作成しました。 今回はMatBlazorを使って、そのTodo画面をマテリアルデザインにしてみました。

MatBlazor使ったらどうなるの?

こんな感じの画面が

こんな感じの画面になっちゃいます。

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

前準備

まずは前準備としてMatBlazorのパッケージを追加し、使用できるように設定していきます。

◆ パッケージの追加

下記のコマンドでパッケージを追加します。念のためcsprojに追加されていることを確認しておきましょう。

$ dotnet add package MatBlazor
◆ _imports.razorを編集

下記を追加します。

_imports.razor

@using MatBlazor
◆ wwwroot/index.htmlを編集

下記を追加します。

index.html

<body>
    <div id="app">Loading...</div>

    <div id="blazor-error-ui">
        An unhandled error has occurred.
        <a href="" class="reload">Reload</a>
        <a class="dismiss">🗙</a>
    </div>
    <script src="_framework/blazor.webassembly.js"></script>
    <!-- 下記を追加 -->
    <script src="_content/MatBlazor/dist/matBlazor.js"></script>
</body>

これで準備は整いました。

Todoアイテム入力欄

ここではチェックボックス、入力欄、ボタンにMatBlazorを使用しています。

TodoMat.razor

@page "/todomat"

<style>
    .new-item-text-field > label.mat-text-field > span {
        margin-left: 30px;
    }
</style>

<h1>Todo</h1>

<div class="new-item-text-field" style="display: flex; position: relative; align-items: center;">
    <MatCheckbox Style="z-index: 1; position: absolute; left: 0;" 
                 @bind-Value="allChecked" Disabled="@(!TodoItemExists())"></MatCheckbox>
    <MatTextField @bind-Value="newItem" Label="What needs to be done?"
                  OnKeyDown="@Enter" OnInput="@OnInput"
                  Style="z-index: 0; width: 500px; padding-left: 50px;"></MatTextField>
    <MatButton OnClick="@AddItem" Outlined="true">add</MatButton>
</div>

MatBlazorのドキュメントに使用例がたくさん載ってるのでさらっと説明を。

◆ MatCheckbox

チェックボックスのコンポーネント。前回作成したTodo画面では入力欄のとこにチェックボックスを付けてなかったのですが追加しました。

Name Description
@bind-Value バインドする値。true / false
Disabled 無効にするかどうか。true / false

@bind-Valueには、プロパティallCheckedをバインド。 コメントの通りです。

Disabledには、メソッドTodoItemExists()の戻り値をバインド。

TodoMat.cs

// newItemのチェック状態
private bool allChecked
{
    get
    {
        // 未完了アイテムがない場合にチェックON、一つでもあればチェックOFF
        return !allTodos.Any(x => !x.IsDone);
    }
    set
    {
        // チェック状態に合わせてTodoアイテムすべてのチェック状態を変更
        allTodos.ForEach(x => x.IsDone = value);
    }
}
        
/// <summary>
/// Todoアイテムが存在するかどうか
/// </summary>
/// <returns></returns>
private bool TodoItemExists()
{
    return allTodos.Any();
}
◆ MatTextField

テキストフィールドのコンポーネント。

Name Description
@bind-Value バインドする値。入力した値を保持。
Label 入力項目名。今回はPlaceholder的な使い方してます。
HelperTextというのが別に用意されているので本来はこちらの方がいいのかな?
OnKeyDown キーダウンイベント。
OnInput 入力イベント。

ただ1個だけ面倒だったのがあります。

MatTextFieldのLabel「What needs to be done?」の文字の位置。 CheckBoxとTextFieldを重ねてるので入力内容はStyle属性で paddingを指定してやればズラせたのですが Labelの文字位置は用意されてる属性では触れそうになかったので Styleを強引にイジってます。(上記ソースの4-6行目) これが正解なのかどうかわかりませんがまたどっかで時間作って確認してみます。

◆ MatButton

ボタンのコンポーネント。

Name Description
OnClick クリックイベント。
Outlined ボタンの枠を表示するかどうか。

Todoアイテムリスト

リスト用コンポーネント。MatListとMatListItemを使用してます。今回はStyleのみ指定。

TodoMat.razor

<style>
    .completed-todo-item {
        text-decoration: line-through;
        color: lightgray !important;
    }
</style>
・・・
・・・
<MatList Style="width: 500px">
    @foreach (var item in todosForDisplay)
    {
        <MatListItem Style="padding: 0;">
            <MatCheckbox Style="z-index: 1;" @bind-Value="item.IsDone"></MatCheckbox>
            <MatStringField Style="z-index: 0; width: 100%; position: absolute; padding-left: 50px;" 
                            InputClass="@GetCompletedTodoItemCssClass(item.IsDone)" 
                            @bind-Value="item.Content"></MatStringField>
        </MatListItem>
    }
</MatList>

MatListItem内ではMatCheckboxとMatStringFieldを使用しています。 MatStringFieldはMatTextFieldと違ってString型専用のコンポーネントっぽいです。 MatTextFieldはintやDateTimeなどの型がバインドできます。

◆ MatStringField

String専用の入力用コンポーネント。

Name Description
InputClass input要素のCSSクラス。

Styleに「completed-todo-item」を用意し、InputClassに チェック状態によって異なるStyle名を返すメソッド「GetCompletedTodoItemCssClass()」をバインドしています。

TodoMat.cs

/// <summary>
/// Todo完了アイテムCSSクラス名を取得
/// </summary>
/// <param name="isDone">完了フラグ</param>
/// <returns>クラス名</returns>
private string GetCompletedTodoItemCssClass(bool isDone)
{
    return isDone ? "completed-todo-item" : "";
}

リスト下のボタン群

ボタン用のコンポーネント。

TodoMat.razor

@allTodos.Count(x => !x.IsDone) items left
<MatButton OnClick="@ShowAll" Outlined="true" Style="margin-left: 20px">All</MatButton>
<MatButton OnClick="@ShowActive" Outlined="true">Active</MatButton>
<MatButton OnClick="@ShowCompleted" Outlined="true" Style="margin-right: 20px">Completed</MatButton>
<MatButton OnClick="@ClearCompleted" Outlined="true" 
           Disabled="@(!CompletedTodoItemExists())">Clear completed</MatButton>

TodoMat.cs

/// <summary>
/// 完了済みのTodoアイテムが存在するかどうか
/// </summary>
/// <returns></returns>
private bool CompletedTodoItemExists()
{
    return allTodos.Any(x => x.IsDone);
}

さいごに

使ってみた感じ、CSSの知識がない人にとっては嬉しいとは思いますが Webの知識がある人からすると物足りなさそうな感じがします。 ちょっと細かいことに拘るとCSSの知識も必要になってきます。 ドキュメントはわかりやすかったのですんなり使えました。 他にもBlazor向けのコンポーネントで良さそうなのがあったらまた紹介したいと思います。

以上、もりもりでした。

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フレームワークもあり 面白そうなので近々使ってみようと思います。

以上、もりもりでした。