BlazorでSPAするぞ!(6) - Routing

※ 2019/6/15 .NET Core 3.0 Preview 6 に対応した記述に修正しました。

という事で、↓↓↓の続きです。

ryuichi111std.hatenablog.com

今回はルーティングについて。
URLに対するページ(コンポーネント)のルーティング(マッチング)ですね。

1. ルーティング(Routing)

サンプルプロジェクトからルーティングを学んでいきたいと思います。
まず、dotnet new blazor コマンドでプロジェクトを作成します。

dotnet new blazor -o route_blazor

自動生成コードには、ルーティング実装も ある程度組み込まれています。

dotnet run で実行し、http://localhost:5000 にアクセスします。
これは「/」(ルート)へのアクセスになります。
「/」に対するルーティング設定は、「Pages/Index.razor」となっています。

f:id:daigo-knowlbo:20190502144641p:plain

サイドメニューから「Counter」をクリックします。
ブラウザのURLが http://localhost:5000/counter になりました。
「/counter」へのアクセス、そして「/counter」へのルーティング設定は「Pages/Counter.razor」となります。

f:id:daigo-knowlbo:20190502144739p:plain

同様にサイドメニューから「Fetch data」をクリックします。
ブラウザのURLが http://localhost:5000/fetchdata になりました。
「/fetchdata」へのアクセス、そして「/fetchdata」へのルーティング設定は「Pages/FetchData.razor」となります。

f:id:daigo-knowlbo:20190502144720p:plain

1.1. @Page

「/」「/counter」「/fetchdata」のような各URLに対する .razor ファイルのルーティングは、「@Page」ディレクティブで設定されます。
自動生成された3つのコンポーネント(Pages配下の.razorファイル)の定義は、それぞれ以下の通りです。

# Pages/Index.razor #

@page "/"
...省略
# Pages/Counter.razor #

@page "/counter"
...省略
# Pages/FetchData.razor #

@page "/fetchdata"
...省略

1.2. ユーザ一覧画面を実装する場合

では、新規に「ユーザ一覧画面」を追加する想定の実装例を見てみましょう。
ルーティング設定するURLは「/users/list」とします。
.razorコンポーネントは「Pages/UserList.razor」ファイルで定義します。
以下がその定義になります。

# Pages/UserList.razor #

@page "/users/list"

ユーザ一覧画面です!!!
...省略...

1.3. URLが変わるけどSPA

自動生成されたプロジェクトを動作させるだけでもわかりますが、ページ(コンポーネント)を切り替えると、ブラウザのURLが切り替わります。
ただし、Blazorはクライアントサイドで動作しているので、対象のページ(コンポーネント)をサーバにHttpRequestするような事はせずSPAとして動作しています。
ChromeであればDevToolを表示し「Network」の監視を行えば確認することができます。

以下が実際に route_blazor を起動した後(ルートを表示した後)、「Counterメニュークリック」→「Fetch dataメニュークリック」という操作を行った際のNetwork状態です。

f:id:daigo-knowlbo:20190502145049p:plain

f:id:daigo-knowlbo:20190502145102p:plain

Counterメニュー クリック時:「favicon.ico」リクエストのみ
Fetch dataメニュー クリック時:「favicon.ico」リクエストおよび「weather.json」リクエストのみ。weather.jsonはアプリロジック的にサーバからjsonデータを明示的にリクエストしているため。

2. <Router>の初期化

@Pageディレクティブ定義によるルーティング設定方法を先に説明しましたが、アプリケーションでは<Router>を初期化しておく必要があります。
ルーティングを有効化する初期化処理は「/App.razor」で行います。
自動生成されたプロジェクトでも、以下のように既に実装が行われています。

# /App.razor #

<Router AppAssembly="typeof(Program).Assembly" />

<Router>を定義し、AppAssembly属性に「typeof(Program).Assembly」を設定します。
Routerクラスは、「Microsoft.AspNetCore.Components.Routing」名前空間で定義されています。

2.1. FallbackComponent

定義されたルーティングにマッチしなかった場合(フォールバックした場合)に表示するコンポーネントを、「FallbackComponent属性」で指定することができます。

# /App.razor #

<Router AppAssembly="typeof(Program).Assembly"
        FallbackComponent="typeof(Pages.InvalidRoute)" />

フォールバックコンポーネント「Pages/InvalidRoute.razor」を用意します。

# Pages/InvalidRoute.razor #

<div>
  無効なURIです。
</div>

dotnet runで実行し、http://localhost:5000/invalid にアクセスしてみます。「/invalid」はどのコンポーネントにもルーティングされていません。
以下のように、フォールバックページ(Pages/InvalidRoute.razor)が表示されました。

f:id:daigo-knowlbo:20190502145126p:plain

RouterのFallbackComponent属性が未設定の場合、無効なURLがアクセスされると、以下のように「Loading...」画面で止まってしまいます。

f:id:daigo-knowlbo:20190502145140p:plain

Chromeの「DevTools - Console」を確認すると、WASMのエラーが多数出力された状態で止まっていることが確認できます。

f:id:daigo-knowlbo:20190502145151p:plain

2.2. 複数のルート設定

1つのコンポーネントには、複数のルーティング定義を行うことができます。
つまり @Page を複数設定可能という事です。
先程の「UserList.razor」に対して、2つのURLルーティング設定を行った例が以下です。

# Pages/UserList.razor #

@page "/users/list"
@page "/employees/list"

ユーザ一覧画面です!!!

3. NavLinkコンポーネント

ルーティング設定された各コンポーネント(ページ)の表示切替は <a href="/xxx"> タグで可能です。
<a>タグをラップしたコンポーネントとして「NavLinkコンポーネント(Microsoft.AspNetCore.Components.Routing名前空間)」が用意されています。
サンプルプロジェクトでは、左サイドのメニューを「Shared/NavMenu.razor」で実装しており、ここでNavLinkコンポーネントを利用しています。

# Shared/NavMenu.razor #

<div class="top-row pl-4 navbar navbar-dark">
  <a class="navbar-brand" href="">route_blazor</a>
  <button class="navbar-toggler" onclick="@ToggleNavMenu">
    <span class="navbar-toggler-icon"></span>
  </button>
</div>

<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>
      
    </li>
  </ul>
</div>

@code {
  bool collapseNavMenu = true;

  string NavMenuCssClass => collapseNavMenu ? "collapse" : null;

  void ToggleNavMenu()
  {
    collapseNavMenu = !collapseNavMenu;
  }
}

3.1. href属性

<a>タグと同様に「href属性」でURLを設定します。この「URL」と「.razprで定義された@Page値」によってルーティング先コンポーネントが決定されます。

3.2. 「active」CSSクラス

NavLinkコンポーネントの特徴(機能の1つ)として、「href属性値」と「現在のURL」がマッチした場合、「active」CSSクラスが付与される というものがあります。

以下は「現在のURLが /counter」なので「Counterメニューにactive cssクラス」が付いています。
加えて、NavLinkは<a>タグとしてレンダリングされていることが確認できます。

f:id:daigo-knowlbo:20190502145212p:plain

f:id:daigo-knowlbo:20190502145225p:plain

Match属性

NavLinkコンポーネントには「Match属性(Microsoft.AspNetCore.Components.Routing名前空間のNavLinkMatch列挙型)」というものがあります。
href値とURLのマッチングルールを設定します。
以下の2つの列挙値があります。

  • NavLinkMatch.All
    URLが完全一致した場合に active CSSクラスが適用されます。
    ※ href=/usersに対しては、URL=/users はマッチするが、URL=/users/list はマッチしない
  • NavLinkMatch.Prefix
    URLのプレフィクスが一致した場合に active CSSクラスが適用されます。
    ※ href=/usersに対しては、URL=/users も URL=/users/list もマッチとして扱われる

4. ルートパラメータ

ルーティングのURLにパラメータを含めることができます。
それらのパラメータ値は、ルーティング先のコンポーネントで受け取ることができます。

Pages/Index.razorコンポーネントでルート パラメータを受け取るようにしたいと思います。

4.1. @page ディレクティブにパラメータ定義を追加

ルートパラメータは @Page ディレクティブで設定を行います。
Index.razorは「/」(ルート)にルーティングされています。
「/【Message】」といった形式でパラメータを受け取るように設定します。

# Pages/Index.razor #

@page "/"
@page "/{Message}"

<h1>Hello, world!</h1>

your message is '@Message'
<br />

@code {
  private string _Message = "None";
  [Parameter]
  protected string Message {
    get {
      return this._Message;
    }
    set { 
      this._Message = System.Net.WebUtility.UrlDecode(value);
    }
  }
}

上記のように、ルートパラメータ定義は「{【パラメータ名】} 」の形式をとります。
Messageは、コードブロックで[Parameter]属性を付与したプロパティとして定義します。
ここでは「%20(スペース)」などのURLエンコードされた文字列パラメータを、URLデコードしてMessageプロパティに保持するために、getter / setterを定義し setterではUrlDecode()処理を行っています。

dotnet run コマンドで実行し、以下のURLをブラウザでアクセスします。

http://localhost:5000/Blazor%20is%20new%20SPA%20framework

URLパラメータを受け取り、画面に表示することが出来ました。

f:id:daigo-knowlbo:20190502145250p:plain

4.2. ルートパラメータの型指定

ルートパラメータは厳密な型を指定することができます。
Pages/Counter.razorコンポーネントに、CurrentCountの初期値をint型ルートパラメータとして受け取るような実装を追加したいと思います。
以下のように「{【パラメータ名】:【型名】}」の形式でルーティング設定を記述します。
コードブロック側も、[Parameter]属性を適用するために、CurrentCountを 変数 から setterを持つプロパティ に修正しています。

# Pages/Counter.razor #

@page "/counter"
@page "/counter/{currentCount:int}"

<h1>Counter</h1>

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

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

@functions {
  [Parameter]
  private int CurrentCount {get; set; } = 0;

  void IncrementCount()
  {
    this.CurrentCount++;
  }
}

http://localhost:5000/counter/100」にアクセスした実行画面は以下です。

f:id:daigo-knowlbo:20190502145309p:plain

5. まとめ

ルーティングも従来のASP.NET Core等の仕組みと基本思想は同じくしているので、スムーズに理解することができたました。
次の投稿では、DI(Dependency Injection)を見ていこうと思います。

次は ryuichi111std.hatenablog.com

BlazorでSPAするぞ!(5) - レイアウト

※ 2019/6/15 .NET Core 3.0 Preview 6 に対応した記述に修正しました。

という事で、↓↓↓の続きです。

ryuichi111std.hatenablog.com

今回はレイアウト機能について。

1. レイアウト機能とは

これはよくあるやつですね。
ASP.NET Coreでの「_Layout.cshtml」と大体同じことです。
ヘッダやフッター、メニューなど、アプリケーション内の複数のページで共通する画面項目(UI)がある場合に使われます。
すべての画面定義に「ヘッダ、フッター、メニュー」をペタペタとコピペしても画面実装は可能ですが、煩雑ですしメンテナンス性も失われます。
そこで、複数画面で共通して利用するUIを「レイアウト」として定義します。そして、画面毎に異なるコンテンツ部分を個別に実装し、それらを共通レイアウトに差し込む事ができます。

2. 自動生成コードから学ぶ

dotnet new コマンドで自動生成したプロジェクトは、きっちりとレイアウト機能が使われた形になっています。
自動生成コードを見ながらレイアウト機能を理解していきたいと思います。

では、dotnet new blazorコマンドでプロジェクトを生成します。

dotnet new blazor -o layout_blazor

2.1. _Imports.razor / @layout

まず「_Imports.razor」という名前のファイルがBlazorにおいて特殊なファイルとして扱われます。
コンパイラは「
_Imports.razor」という名前のファイルを見つけると、このファイルの記述内容を同一フォルダおよび配下のフォルダに再帰的に適用します。

(1) /_Imports.razor

layout_blazorプロジェクトでは、まず、プロジェクトルートに「__Imports.razor」ファイルがあります。

# /_Imports.razor #

@using System.Net.Http
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Layouts
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.JSInterop
@using layout_blazor
@using layout_blazor.Shared

「@using」宣言がいくつか並んでいます。
ルートの_Imports.razorで定義されているという事で、つまり、このプロジェクトではルートフォルダおよび配下フォルダで定義されるすべての .razor において、これらのusingが有効になります。

(2) Pages/_Imports.razor

次にPagesフォルダ配下にも_Imports.razorファイルがあります。

# Pages/_Imports.razor #

@layout MainLayout

1行のみ「@layout」ディレクティブが記述されています。
@layoutは重要でレイアウトファイル(クラス)の指定になります。
つまり、Pagesおよびその配下のフォルダで定義された .razor ファイルには MainLayout で定義されたレイアウトが適用されることを意味します。

(3) MainLayout

MainLayoutは、どこで定義されているでしょうか?
Pagesフォルダ配下には存在しません。Sharedフォルダ配下で定義されています。
Pages/_Imports.razorにおいて「MainLayout」と記述できる理由は、/_Imports.razorにおいて「@using layout_blazor.Shared」という宣言が行われていたためです。

以下を見てわかるように、この「Shared/MainLayout.razor」がレイアウト定義を行っているファイルになります。

# Shared/MainLayout.razor #

@inherits LayoutComponentBase

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

<div class="main">
  <div class="top-row px-4">
    <a href="http://blazor.net" target="_blank" class="ml-md-auto">About</a>
  </div>

  <div class="content px-4">
    @Body
  </div>
</div>
【@inherits LayoutComponentBase】

@inheritsディレクティブは、コードビハインドクラスを指定するときにも使用しましたが、ここでは MainLayout の継承元として LayoutComponentBase を指定しています。
LayoutComponentBaseクラスは、Blazorライブラリで「Microsoft.AspNetCore.Components.Layouts」名前空間に定義されたクラスです。
レイアウトを定義するクラスは「LayoutComponentBase」を継承する必要があります。

【NavMenu】

サイドバーとして「NavMenu」コンポーネントを配置しています。
NavMenu.razorは、同じSharedフォルダに定義されており、以下のような実装になります。
実行したサンプルの 左のサイドバーメニュー そのままの定義ですね。

# Shared/NavMenu.razor #

<div class="top-row pl-4 navbar navbar-dark">
    <a class="navbar-brand" href="">layout_blazor</a>
    <button class="navbar-toggler" onclick="@ToggleNavMenu">
        <span class="navbar-toggler-icon"></span>
    </button>
</div>

<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>
    </ul>
</div>

@code {
    bool collapseNavMenu = true;

    string NavMenuCssClass => collapseNavMenu ? "collapse" : null;

    void ToggleNavMenu()
    {
        collapseNavMenu = !collapseNavMenu;
    }
}
【@Body】

「@Body」ディレクティブが、各ページ(各コンポーネント)固有のコンテンツが配置される部分になります。
例えば「/」(ルート)にマップされるファイルは Pages/Index.razor になりますが、Shared/MainLayout.razor の @Body 箇所に Pages/Index.razor がレンダリングされます。

f:id:daigo-knowlbo:20190502022002p:plain

3. まとめ

レイアウト機能は比較的シンプルでASP.NET Coreのレイアウトなどを知っていれば、スムーズに理解できそうです。
自動生成されるプロジェクトが、昔に比べて複雑化していますが、これを丁寧にトレースすることで、レイアウト機能の基本は理解できると思います。

次は ryuichi111std.hatenablog.com

BlazorでSPAするぞ!(4) - データバインド(おかわり)

※ 2019/6/15 .NET Core 3.0 Preview 6 に対応した記述に修正しました。

という事で、↓↓↓の続きです。

ryuichi111std.hatenablog.com

1. データバインド

Component のデータバインディングについては BlazorでSPAするぞ!(2) - Component - ryuichi111stdの技術日記 で軽く説明し、value="@xxx"によるone-wayバインディング / bind="@xxx"によるtwo-wayバインディングについても使用しました。
この部分についてもう少し おかわり(深堀) したいと思います。

1.1. サンプル

以下のような画面のサンプルアプリをもとに説明を進めます。

f:id:daigo-knowlbo:20190501225947p:plain

donet newコマンドでプロジェクトを作成します。

dotnet new blazor -o bind_ex_blazor

Components/ChildComponent.razorコンポーネントを作成します。

# Components/ChildComponent.razor #

<h2>Child Component</h2>

<p>YourScore: @YourScore</p>
<input type="text" value="@YourScore" />

@code {
    [Parameter]
    private int YourScore { get; set; }
}

YourScoreというint型の値をパラメータとして受け取り、テキストおよびinput要素として表示します。

次に、ChildComponentコンポーネントをPages/Index.razorに配置します。

# Pages/Index.razor #

@page "/"
@using bind_ex_blazor.Components

<h2>Index Component</h2>
Score: @YourScore
<br />
<button onclick="@InclementScore">+</button>
<button onclick="@DeclementScore">-</button>
<br />
<ChildComponent YourScore="@YourScore" />

@code {
  private int YourScore = 100;

  private void InclementScore() {
    this.YourScore++;
  }

  private void DeclementScore() {
    this.YourScore--;
  }
}

Index.razor自体にも いくつかUIを追加しています。
YourScoreは初期値100で用意しています。
+ボタン / -ボタンを配置し、クリックされたらイベントハンドラ「InclementScore() / DeclementScore()」によりYourScoreの値を増減させます。

1.2. 動作確認

では、動作を確認していきます。

+ボタンをクリックすると、Index.razorの YourScore 値が +1 され 101 になります。
-ボタンをクリックすると、Index.razorの YourScore 値が -1 され 99 になります。

f:id:daigo-knowlbo:20190501230014p:plain

Index.razorの「Score: @YourScore」の値も、Blazor Runtimeによりバインド変数の変化の影響を自動判定して変更が反映されます。
また、ChildComponentへの「YourScore="@YourScore"」の値も、同様に変更が反映します。

以下、引き続きこのサンプルをベースに説明を進めます。

2. value="@YourScore" によるデータバインド

上述サンプルにおいて、ChildComponentのテキストボックスの値を直接変更してみます。
「1012」を入力し、フォーカスを移動します。

f:id:daigo-knowlbo:20190501230029p:plain

テキストボックス以外のYourScore値は変更されませんした。
これはテキストボックスに対して「value="@YourScore"」としてデータバインド定義が行われており、つまりone-wayバインディングになっているためです。
(データ→UIの一方通行バインディング)

3. @bind="@YourScore" によるデータバインド

ChildComponent.razorを以下のように「@bind="@YourScore"」で書き換えます。
これでtwo-wayバインディングとなり、データ→UI / UI→データの両方向のデータバインディングが有効になります。

# Components/ChildComponent.razor #

<h2>Child Component</h2>

<p>YourScore: @YourScore</p>
<input type="text" @bind="@YourScore" />

@code {
    [Parameter]
    private int YourScore { get; set; }
}

dotnet runし直し、再び、ChildComponentのテキストボックスの値を直接変更してみます。
two-wayバインディングになったため、UIでの入力内容 10012 が裏側でChildComponentの YourScoreプロパティ値 に反映し、「YourScore: @YourScore」にも変更が反映しました。

f:id:daigo-knowlbo:20190501230046p:plain

ただし、親コンポーネント側(Index.razor)にはこの変更は伝達されていません(Index.razorのScore表示は100のまま)。

4. @bind-YourScore="@YourScore" によるデータバインド

ChildComponentでのYourScoreの変更が、親コンポーネントIndex.razorにも伝達されるようにしてみたいと思います。

4.1. Index.razorのバインド定義を修正

コンポーネント側(Index.razor)で、ChildComponent定義時のYourScoreプロパティへのパラメータ定義を「YourScore="@YourScore"」から「@bind-YourScore="@YourScore"」に変更します。
つまり「@bind-」プレフィックスを追加しています。

@page "/"
@using bind_ex_blazor.Components

<h2>Index Component</h2>
Score: @YourScore
<br />
<button onclick="@InclementScore">+</button>
<button onclick="@DeclementScore">-</button>
<br />
<ChildComponent @bind-YourScore="@YourScore" />

@code {
  private int YourScore = 100;

  private void InclementScore() {
    this.YourScore++;
  }

  private void DeclementScore() {
    this.YourScore--;
  }
}

4.2. ChildComponent.razorにEventCallbackを実装

ChildComponent.razor側にも手を加えます。

# Components/ChildComponent.razor #

<h2>Child Component</h2>

<p>YourScore: @YourScore</p>
<input type="text" @bind="@YourScore" />

@code {
    [Parameter]
    public EventCallback<int>YourScoreChanged { get; set; }

    private int _yourScore = 0;

    [Parameter]
    private int YourScore {
        get {
            return this._yourScore;
        }
        set {
            if(this._yourScore != value) {
                this._yourScore = value;
                YourScoreChanged.InvokeAsync(this._yourScore);
            }
        }
    }
}

(1) EventCallbackパラメータ定義

EventCallbackパラメータを追加しています。
YourScoreChangedイベントを定義していますが、これにより当該イベント発生時にサブスクライバに対してイベントコールバックを呼び出すことができます。
int型の値をコールバック引数として渡すので「EventCallback<int>」と定義しています。

(2) YourScoreプロパティのgetter / settter定義

YourScoreプロパティにgetter/setterを定義しました。
setterでは、値が変更されていた場合、EventCallbackを呼び出します(YourScoreChanged.InvokeAsync(this._yourScore);)。

4.3. 実行

dotnet runで実行してみます。
ChildComponentのテキストボックス値を変更しフォーカスを移動すると、親コンポーネント(Index.razor)のYourScore値も変更されました。

f:id:daigo-knowlbo:20190501230118p:plain

4.4. ILを確認

Index.razor / ChildComponent.razorにいくつかの変更を加えましたがコンパイル後のILを確認すると、動作の繋がり・フローがよく分かります。
「bind_ex_blazor\bin\Debug\netstandard2.0\bind_ex_blazor.dll」をIlSpyでオープンします。

(1) ChildComponent.razor

まず、ChildComponentですが「YourScoreプロパティの定義」「BuildRenderTree()メソッドの定義」は以下のようになっています。

// bind_ex_blazor.Components.ChildComponent

[Parameter]
private int YourScore
{
    get
    {
        return _yourScore;
    }
    set
    {
        if (_yourScore != value)
        {
            _yourScore = value;
            YourScoreChanged.InvokeAsync(_yourScore);
        }
    }
}

protected override void BuildRenderTree(RenderTreeBuilder builder)
{
    builder.OpenElement(0, "div");
    builder.AddAttribute(1, "class", "container bg-secondary");
    builder.AddMarkupContent(2, "\r\n    ");
    builder.AddMarkupContent(3, "<h2>Child Component</h2>\r\n\r\n    ");
    builder.OpenElement(4, "p");
    builder.AddContent(5, "YourScore: ");
    builder.AddContent(6, YourScore);
    builder.CloseElement();
    builder.AddMarkupContent(7, "\r\n    ");
    builder.OpenElement(8, "input");
    builder.AddAttribute(9, "type", "text");
    builder.AddAttribute(10, "value", BindMethods.GetValue(YourScore));
    builder.AddAttribute(11, "onchange", EventCallback.Factory.CreateBinder(this, delegate(int __value)
    {
        YourScore = __value;
    }, YourScore));
    builder.CloseElement();
    builder.AddMarkupContent(12, "\r\n");
    builder.CloseElement();
}

YourScoreプロパティの定義の方は、ほぼ元の実装の通りです。繰り返しですが、setterで値が変更されたらYourScoreChangedがInvokeされます。
BuildRenderTree()メソッドは、テキストボックスに対してonchangeがハンドルされYourScoreプロパティに代入が行われています。
「テキストボックス入力→onchange→YourScore setter呼び出し→YourScoreChangedのinvoke」という流れになります。

(2) 親コンポーネント(Index.razor)

次に親コンポーネントIndex.razorのBuildRenderTree()メソッドを見ていきます。

// bind_ex_blazor.Pages.Index

protected override void BuildRenderTree(RenderTreeBuilder builder)
{
    builder.OpenElement(0, "div");
    builder.AddAttribute(1, "class", "container bg-primary");
    builder.AddMarkupContent(2, "\r\n  ");
    builder.AddMarkupContent(3, "<h2>Index Component</h2>\r\n  Score: ");
    builder.AddContent(4, YourScore);
    builder.AddMarkupContent(5, "\r\n  <br>\r\n  ");
    builder.OpenElement(6, "button");
    builder.AddAttribute(7, "onclick", EventCallback.Factory.Create<UIMouseEventArgs>((object)this, (Action)InclementScore));
    builder.AddContent(8, "+");
    builder.CloseElement();
    builder.AddMarkupContent(9, "\r\n  ");
    builder.OpenElement(10, "button");
    builder.AddAttribute(11, "onclick", EventCallback.Factory.Create<UIMouseEventArgs>((object)this, (Action)DeclementScore));
    builder.AddContent(12, "-");
    builder.CloseElement();
    builder.AddMarkupContent(13, "\r\n  <br>\r\n  ");
    builder.OpenComponent<ChildComponent>(14);
    builder.AddAttribute(15, "YourScore", RuntimeHelpers.TypeCheck(BindMethods.GetValue(YourScore)));
    builder.AddAttribute(16, "YourScoreChanged", RuntimeHelpers.TypeCheck(EventCallback.Factory.Create(this, EventCallback.Factory.CreateInferred(this, delegate(int __value)
    {
        YourScore = __value;
    }, YourScore))));
    builder.CloseComponent();
    builder.AddMarkupContent(17, "\r\n");
    builder.CloseElement();
}

ChildComponentに対して「YourScoreChanged」イベントハンドラが定義されEventCallbackで受け取ったint値を自分のYourScoreに代入しています。
この「YourScoreChanged」実装は、Index.razorを以下の実装にした場合には出力されません。

<ChildComponent YourScore="@YourScore" />

今回の修正で以下のように実装したために追加された処理になります。

<ChildComponent @bind-YourScore="@YourScore" />

Blazorでは、「bind-【プロパティ名】」と定義することで、「【プロパティ名】Changed」というEventCallbackを受け取る仕組みになっています。

という事で、整理すると以下のフローになります。

子コンポーネント側:
①テキストボックス入力
②onchange
③YourScore setter呼び出し
④YourScoreChangedのinvoke
↓↓↓  
親コンポーネント側:
⑤YourScoreChangedをハンドルして自らのYourScoreに変更値を反映
⑥UIにも変更が反映  

※以下の「bind-YourScore-YourScoreChanged」に関してはPreview6で廃止されました。

5. bind-YourScore-YourScoreChanged="@YourScore" によるデータバインド

前のサンプルで「bind-YourScore="@YourScore"」という定義を行いましたが、これは「bind-YourScore-YourScoreChanged="@YourScore"」と同意になります。
では、以下のように実装したらどうなるか?

# Index.razor #

<ChildComponent bind-YourScore-YourScoreChangeeeeeeeed="@YourScore" />

コンパイル後のILは以下のようになります。
定義した「YourScoreChangeeeeeeeed」がそのままEventCallback名としてコンパイルされました。

protected override void BuildRenderTree(RenderTreeBuilder builder)
{
  ...省略
  builder.OpenComponent<ChildComponent>(14);
    builder.AddAttribute(15, "YourScore", RuntimeHelpers.TypeCheck(BindMethods.GetValue(YourScore)));
    builder.AddAttribute(16, "YourScoreChangeeeeeeeed", EventCallback.Factory.CreateBinder(this, delegate(int __value)
  ...省略
}

これを正常に動作させる場合、ChildComponent側のEventCallback定義名も合わせて変更する必要があります。

# ChildComponent.razor #
  [Parameter]
  public EventCallback<int>YourScoreChangeeeeeeeed { get; set; }

3. まとめ

データバインディングの動作の仕組みをILレベルで確認してみました。
データバインド構文的には以下のような感じで、構文とその動きを理解すればいいわけですが、ILを見てみるとロジックとして動作が繋がってすっきりしました。

one-way: [property name]="@Value"
two-way: @bind-[property name]="@value"
bind-[property name]-[event callback] ※Preview6で廃止されました。

次は ryuichi111std.hatenablog.com

BlazorでSPAするぞ!(3) - パラメータ

※ 2019/6/15 .NET Core 3.0 Preview 6 に対応した記述に修正しました。

という事で、↓↓↓の続きです。

ryuichi111std.hatenablog.com

前回 Component の基本的な作り方・使い方を見てみましたが、今回はComponentのパラメータ機能を見ていきます。

1. パラメータ(Parameter)の利用

コンポーネントは、コンポーネント(ページ等)内にネストして配置することができました。
そして以下のように タグ で配置しました。
以下は、PersonViewerコンポーネントを .razor に配置しています(PersonViewerはPerson情報を表示するコンポーネントを想定)。

<PersonViewer />

コンポーネントではタグ属性を利用して、親(コンテナとなっているコンポーネント)から子(配置されているコンポーネント)へパラメータを渡すことができます。
以下ではPersonViewerコンポーネントに対して「ID / FirstName / LastName」の3つのパラメータを渡しています。

<PersonViewer1
  ID="@CurrentPerson.ID"
  FirstName="@CurrentPerson.FirstName"
  LastName="@CurrentPerson.LastName" />

1.1. サンプル実装

では具体的なサンプル実装を。
プロジェクトを作成します。

dotnet new blazor -o component_param_blazor

(1) モデルクラスを作成

Personモデルクラスを作成します(Models/Person.cs)。

# Models/Person.cs #

using System;

namespace component_param_blazor.Models {
  public class Person {
    public int ID { get; set; }
    public string FirstName{ get; set; }
    public string LastName{ get; set; }
  }
}

(2) コンポーネントを作成

PersonViewerコンポーネントを作成します(Components/PersonViewer1.razor)。

# Components/PersonViewer1.razor #

@using component_param_blazor.Models

<div class="container">
  <div class="row">
    <div class="col-sm">ID:</div>
    <div class="col-sm">@ID</div>
  </div>
  <div class="row">
    <div class="col-sm">FirstName:</div>
    <div class="col-sm">@FirstName</div>
  </div>
  <div class="row">
    <div class="col-sm">LastName:</div>
    <div class="col-sm">@LastName</div>
  </div>
</div>

@code {
  [Parameter]
  private int ID { get; set; }
  
  [Parameter]
  private string FirstName{ get; set; }

  [Parameter]
  private string LastName{ get; set; }
}
[Parameter]

コンポーネントがらパラメータとして受け取るプロパティには [Parameter] 属性を付けます。
コンポーネントからの受取ですが、アクセス修飾子はprivateでもOKです。

(3) PersonViewerコンポーネントをページに配置

Pages/Index.razorにPersonViewer1コンポーネントを配置します。
Index.razorのコードブロックで Personオブジェクト を作成して、PersonViewer1コンポーネントにパラメータとして各価を引き渡します。

# Pages/Index.razor #

@page "/"
@using component_param_blazor.Components
@using component_param_blazor.Models

<PersonViewer1
  ID="@CurrentPerson.ID"
  FirstName="@CurrentPerson.FirstName"
  LastName="@CurrentPerson.LastName" />

@code {
  private Person CurrentPerson { get; set; }

  protected override void OnInit() {
    this.CurrentPerson = new Person() {
      ID = 0,
      FirstName = "takashi",
      LastName = "sakata" } ;
  }
}

実行画面は以下の通りです。

f:id:daigo-knowlbo:20190501224826p:plain

1.2. サンプル実装(オブジェクトとしてパラメータを渡す)

[Parameter]属性で引き渡すパラメータは、リテラル型だけでなく任意のオブジェクト型も指定可能です。
先程の PersonViewer1.razor を Personオブジェクト を受け取るように修正した版(PersonViewer2.razor)が以下です。

# Components/PersonViewer2.razor #

@using component_param_blazor.Models

<div class="container">
  <div class="row">
    <div class="col-sm">ID:</div>
    <div class="col-sm">@Person.ID</div>
  </div>
  <div class="row">
    <div class="col-sm">FirstName:</div>
    <div class="col-sm">@Person.FirstName</div>
  </div>
  <div class="row">
    <div class="col-sm">LastName:</div>
    <div class="col-sm">@Person.LastName</div>
  </div>
</div>

@code {
  [Parameter]
  private Person Person { get; set; }
}
# Pages/Index.razor #

@page "/"
@using component_param_blazor.Components
@using component_param_blazor.Models

<PersonViewer2 Person="@CurrentPerson" />

@code {
  private Person CurrentPerson { get; set; }

  protected override void OnInit() {
    this.CurrentPerson = new Person() {
      ID = 0,
      FirstName = "takashi",
      LastName = "sakata" } ;
  }
}

2. Cascading Parameter

2.1. 単純なパラメータの引き回し

親から子にパラメータを引き渡す方法は前述の通りでした。
子の配下に孫コンポーネントがあり、パラメータオブジェクトをそのまま引き継ぐ必要がある場合、↓↓↓のように実装すれば可能です。

# Pages/Index.razor #

@page "/"
@using cascading_parameter_blazor.Components

<ChildComponent YourName="@YourName" />

@code {
  private string YourName = "ryuichi111std";
}
# Components/ChildComponent.razor #

this is Child.
<br />
@YourName
<br />
<GrandChildComponent YourName="@YourName" />

@code {
  [Parameter]
  private string YourName { get; set; }
}
# Components/GrandChildComponent.razor #

this is GrandChild.
<br />
@YourName

@code {
  [Parameter]
  private string YourName { get; set; }
}

2.2. Cascadingパラメータによる引き回し

が、面倒なので、Cascading Parameter機能を使えば、子孫までパラメータを一括で引き回せます。
以下が実装サンプル。

(1) 親コンポーネントの実装

# Pages/Index.razor #

@page "/"
@using cascading_parameter_blazor.Components

<div class="container bg-primary">
  <CascadingValue Value="@YourName" Name="YourNameParam">
    <ChildComponent />
  </CascadingValue>
</div>

@code {
  private string YourName = "ryuichi111std";
}
【CascadingValue】

パラメータを提供するコンポーネントで「<CascadingValue>要素」を定義し、その子要素としてパラメータを引き継ぎたいコンポーネントを配置します。
Value属性値はパラメータの値。@code {} コードブロックで定義した YourName 値を渡しています。
Name属性値はパラメータ名。子孫コンポーネントValue属性で指定した@YourNameの値を YourNameParam というパラメータとして参照することができます。

(2) 子コンポーネントの実装

# Components/ChildComponent.razor #

<div class="container bg-secondary">
  this is Child.
  <br />
  @YourName
  <br />
  <GrandChildComponent />
</div>

@code {
  [CascadingParameter (Name = "YourNameParam")]
  private string YourName { get; set; }
}

【CascadingParameter属性】

プロパティ定義に対して「CascadingParameter属性」を付与します。
親要素で定義されたCascadingValueの中からYourNameParamという名前のパラメータを、自コンポーネントのYourNameプロパティの値として取得する、という意味になります。

(3) 孫コンポーネントの実装

# Components/GrandChildComponent.razor #

<div class="container bg-success">
  this is GrandChild.
  <br />
  @YourName
</div>

@code {
  [CascadingParameter (Name = "YourNameParam")]
  private string YourName { get; set; }
}

こちらも同様に「CascadingParameter」属性をプロパティに付与することでパラメータを引き継ぐことができます。

実行画面は以下の通りです。

blazor_20.pngf:id:daigo-knowlbo:20190501225149p:plain

3. まとめ

ということでComponentのパラメータでした。
(ぜんぜん、まとめじゃない・・・)

次は ryuichi111std.hatenablog.com

BlazorでSPAするぞ!(2) - Component

※ 2019/6/15 .NET Core 3.0 Preview 6 に対応した記述に修正しました。

という事で、↓↓↓の続きです。

ryuichi111std.hatenablog.com

BlazorでのUI要素であるComponent(コンポーネント)について見ていこうと思います。

1. コンポーネント(Component)

前回 Hello Blazor ページを実装しましたが、これ自体がコンポーネントです。
ページやダイアログ、ページ内の要素などのUI要素(オブジェクト)がコンポーネントです。
HTML/CSS と イベント処理などを行うロジックコード で構成されます。
コンポーネントは、ネストしてコンポーネント内に配置することも可能です。

まあ、ASP.NETとかWPFとかでも同様な一般的な UIコンポーネント とか コントロール とか言われるようなものですね。

1.1. SimpleHelloコンポーネントの作成(.razor)

コンポーネントは .razor 拡張子で作成します(Previewリリースより前は .cshtml 拡張子でした)。
では超シンプルなコンポーネントを作成し、そのコンポーネントをページに配置したいと思います。

まずは dotnet new でプロジェクトを作成します。
名前はsimple_component_blazorとしました。

C:\workspace\for_blog>dotnet new blazor -o simple_component_blazor
The template "Blazor (client-side)" was created successfully.

Processing post-creation actions...
Running 'dotnet restore' on simple_component_blazor\simple_component_blazor.csproj...
  C:\workspace\for_blog\simple_component_blazor\simple_component_blazor.csproj の復元が 219.72 ms で完了しました。

Restore succeeded.

(1) SimpleHello.razorを作成

simple_component_blazorフォルダ直下に Componentsフォルダ を作成します。
そして、Components/SimpleHello.razor ファイルを作成します。

# Components/SimpleHello.razor #

@Message

@code {
  private string Message = "Hello シンプル過ぎるコンポーネント";
}
@Message と Message定義

「@Message」を記述することにより、@codeブロックで定義した変数Messageが画面上に表示されいます。

(2) SimpleHelloコンポーネントをページに配置

次にページ(Pages/Index.razorn)に、SimpleHelloコンポーネントを配置します。

# Pages/Index.razor #

@page "/"
@using simple_component_blazor.Components

<SimpleHello />
@using

まず「@using」。
Blazorの.razorでは、プロジェクト名+フォルダ階層が名前空間になります(※1)。
Index.simple_component_blazr.Pages名前空間のIndexクラスになります。
SimpleHello.razorは、simple_component_blazor.Components名前空間のSimpleHelloクラスになります。
Pages/Index.razorからは、別の名前空間コンポーネントを使うことになるので「@using simple_component_blazor.Components」として名前空間への参照を指定しておきます。

※1 namespaceを明示的に指定することも可能です。

# Components/SimpleHello.razor (名前空間を明示的に指定)#

@namespace hogehoge.ugougo

@Message

@code {
  private string Message = "Hello シンプル過ぎるコンポーネント";
}
<SimpleHello />

@usingでsimple_component_blazorへの参照を定義済みなので、コンポーネントコンポーネントクラス名(= 拡張子.razorを除いたファイル名 = SimpleHello)をタグで記述することで配置可能です。

dotnet runで実行した画面は以下です。

f:id:daigo-knowlbo:20190501221813p:plain

1.2. Calculatorコンポーネントの作成(.razor)

次にCalculatorコンポーネントというものを作成したいと思います。
完成画面は以下です。
入力テキストボックスを2つ配置し、その和をreadonlyのテキストボックスに表示します。

f:id:daigo-knowlbo:20190501221917p:plain

まずは dotnet new でプロジェクトを作成します。
名前はcalc_blazorとしました。

C:\workspace\for_blog>dotnet new blazor -o calc_blazor
The template "Blazor (client-side)" was created successfully.

Processing post-creation actions...
Running 'dotnet restore' on calc_blazor\calc_blazor.csproj...
  C:\workspace\for_blog\calc_blazor\calc_blazor.csproj の復元が 219.72 ms で完了しました。

Restore succeeded.

(1) Calculationコンポーネントを作成

calc_blazorフォルダ直下に Componentsフォルダ を作成します。
Components/Calculator.razor / Components/CalculatorBase.cs ファイルを作成します。
(UI定義の .razor と コードビハインドの .cs です)

# Components/Calculator.razor #

@inherits calc_blazor.Components.CalculatorBase

<input type="text" @bind="@Value1" />
+
<input type="text" @bind="@Value2" />
=
<input type="text" readonly="true" @value="@Result" />
<br />
<input type="button" @onclick="@Calc" value="計算" />
# Components/CalculatorBase.cs #

using System;
using Microsoft.AspNetCore.Components;

namespace calc_blazor.Components
{
  public class CalculatorBase : ComponentBase
  {
    public int Value1 { get; set; } = 0;
    
    public int Value2 { get; set; } = 0;

    public int Result { get; set; } = 0;

    public void Calc() {
      this.Result = this.Value1 + this.Value2;
    }
  }
}
CalculatorBaseクラス

コードビハインドクラスの実装です。
Value1 / Value2 は2つの入力項目、Result は計算結果を表すプロパティです。
また Calc()メソッド は、Value1 と Value2 の和を Result に設定する計算メソッドです。

Calculator.razor

主にUIを定義したrazorファイルです。
「@inherits calc_blazor.Components.CalculatorBase」でコードビハインドクラスを指定しています。
Value1 と Value2 は <input> タグの @bind属性 に対してバインディング設定されています。
これはTwo-Wayバインディングになります。ロジック上でValue1/Value2の値が変更されるとHTML上でのinput-valueの値が変更されますし、逆に、HTML UI上からユーザによって値の入力が行われるとValue1/Value2の値が変更されます。
Result は <input> タグの value属性 に対してバインディング設定されています。
これはOne-Wayバインディングになります。ロジック上でResultの値が変更されるとHTML上でのinput-valueの値が変更されますが、逆に、HTML UI上からユーザによって行われた値の変更はResultには反映されません(ここではReadOnlyとすることで入力自体出来なくしていますが)。
「<input type="button" @onclick="@Calc" value="計算" />」により、ボタンクリック時に計算メソッド Calc() が呼び出されるようにしています。
Two-WayバインディングでHTML上の入力からプロパティ this.Value1 / this.Value2 に反映した値を合計し、this.Result を更新しています。this.Resultの変更はOne-WayバインディングによりUIに反映されます。

(2) Calculationコンポーネントをページに配置

作成した Components/Calculator コンポーネントを、Pages/Index.razor に配置します。

# Pages/Index.razor #

@page "/"
@using calc_blazor.Components

<Calculator />

dotnet run コマンドを実行するとCalculatorコンポーネントが画面に表示されます。

(さらに)Calculator.razorにイベントを追加

もう1つ。Calculatorコンポーネントにイベントを追加してみたいと思います。
計算が完了した際に計算完了イベント(計算完了コールバック)を実装します。

Calculator.razorにイベント(コールバック)を追加

まず、CalculatorBase.csコードビハインドクラスにイベントを追加します。

# Components/CalculatorBase.cs #

using System;
using Microsoft.AspNetCore.Components;

namespace calc_blazor.Components
{
  public class CalculatorBase : ComponentBase
  {
    public int Value1 { get; set; } = 0;
    
    public int Value2 { get; set; } = 0;

    public int Result { get; set; } = 0;

    [Parameter]
    public EventCallback<int> ChangeCalcResult { get; set; }

    public void Calc() {
      this.Result = this.Value1 + this.Value2;
      ChangeCalcResult.InvokeAsync(this.Result);
    }
  }
}
【ChangeCalcResult】

「public EventCallback ChangeCalcResult { get; set; }」がイベントハンドラの受け口になります。
[parameter]属性は今後(別投稿)の説明で出てきますが、ここでは「呼び出し元(親)から受け取る値について付ける属性」と思っておいてください。

Index.razorでイベント(コールバック)を受け取る

次にIndex.razor側でイベント(コールバック)を受け取ります。
こちらは、とりあえずコードビハインドを使わずに実装してしまいます。

# Pages/Index.razor #

@page "/"
@using calc_blazor.Components

<Calculator ChangeCalcResult="@OnChangeCalcResult" />
<br />
計算結果:@ReceivedResult

@code {
  private int ReceivedResult = 0;

  private void OnChangeCalcResult(int result) {
    this.ReceivedResult = result;
  }
}
【ChangeCalcResult="@OnChangeCalcResult"】

Calculatorタグの属性ChangeCalcResultにイベント(コールバック)を受け取るメソッド名を記述します。
OnChangeCalcResult()メソッドは@codeブロック内で定義しています。

dotnet runでの実行画面は以下です。

f:id:daigo-knowlbo:20190501222820p:plain

1.3. one-wayバインディングとtwo-wayバインディング

引き続き、作成したcalc_blazorを使ってDataBindingに関するちょっとした実験をしてみます。

<input value={@xxx}>によるOne-Wayバインディングについて、razorコードを不適切にいじって動作を確認してみます。
以下のように、Value2に対するバインディングを 「@bind → value」 に変更します。

# Components/Calculator.razor(Value2をOne-wayバインディングに変更) #

@inherits calc_blazor.Components.CalculatorBase

<input type="text" @bind="@Value1" />
+
<input type="text" value="@Value2" />
=
<input type="text" readonly="true" value="@Result" />
<br />
<input type="button" onclick="@Calc" value="計算" />

つまり、Value2 は「HTML UIからの変更がコードのValue2変数に反映されない」状態になりました。
実行画面は以下です。
「10 + 20 = 10」という事で、Value2の値は計算コード上では 0 となってしまいました(HTML inputの値が反映されなかった)。

f:id:daigo-knowlbo:20190501222936p:plain

(1) valueと@bindのIL(Intermediate Language)

Blazor(というか.NET)は、.razor/.csをコンパイルしてDLL(アセンブリ)に変換しています。つまりC#をILに変換しています。
valueと@bindの違いは、ILを覗くとしっくり感じ取ることができます。
Value2への「bind→valueの変更を元に戻して」ビルドしておきます(dotnet buildコマンド実行)。
「calc_blazor\bin\Debug\netstandard2.0\calc_blazor.dll」をIlSpyで逆コンパイルした画面が以下です。

f:id:daigo-knowlbo:20190501223053p:plain

calc_blazor.Components.Calculator#BuildRenderTree(RenderTreeBuilder)メソッドがポイントです。
.razorで記述されたHTMLタグおよびRazor構文が解釈され、コードに落とし込まれています。
calc_blazor.Components.Calculator.BuildRenderTree()メソッドのコードは以下です。

protected override void BuildRenderTree(RenderTreeBuilder builder)
{
    builder.OpenElement(0, "input");
    builder.AddAttribute(1, "type", "text");
    builder.AddAttribute(2, "value", BindMethods.GetValue(base.Value1));
    builder.AddAttribute(3, "onchange", EventCallback.Factory.CreateBinder(this, delegate(int __value)
    {
        base.Value1 = __value;
    }, base.Value1));
    builder.CloseElement();
    builder.AddMarkupContent(4, "\r\n+\r\n");
    builder.OpenElement(5, "input");
    builder.AddAttribute(6, "type", "text");
    builder.AddAttribute(7, "value", BindMethods.GetValue(base.Value2));
    builder.AddAttribute(8, "onchange", EventCallback.Factory.CreateBinder(this, delegate(int __value)
    {
        base.Value2 = __value;
    }, base.Value2));
    builder.CloseElement();
    builder.AddMarkupContent(9, "\r\n=\r\n");
    builder.OpenElement(10, "input");
    builder.AddAttribute(11, "type", "text");
    builder.AddAttribute(12, "readonly", value: true);
    builder.AddAttribute(13, "value", base.Result);
    builder.CloseElement();
    builder.AddMarkupContent(14, "\r\n<br>\r\n");
    builder.OpenElement(15, "input");
    builder.AddAttribute(16, "type", "button");
    builder.AddAttribute(17, "onclick", EventCallback.Factory.Create<UIMouseEventArgs>((object)this, (Action)base.Calc));
    builder.AddAttribute(18, "value", "計算");
    builder.CloseElement();
}

眺めれば、大体 Value1を構築している箇所 / Value2を構築している箇所 / Resultを構築している箇所 が分かります。
@bind属性を利用した Value1 / Value2 に対しては「onchangeに対してEventCallBackが作成され、デリゲートメソッド内で変更された値(__value)を base.Value1(コードビハインドのValue1プロパティ)に設定する」処理が実装されています。
一方value属性を利用した Result に対しては「onchangeに対する実装」がありません。
このように、.razorにおける value="@xxx" と @bind="@xxx" の定義の違いは、対象要素に対して UIからの変更をバインドプロパティに反映するonchangeイベントハンドラ を生成するかどうかの違いになってきます。

1.4. リストデータを表示するページ(コンポーネント)を作成

次はリストデータを表示するページを作成してみます。
TodoList表示ページとします。
以下のイメージです。

f:id:daigo-knowlbo:20190501223553p:plain

これまでコンポーネントという表現を使っていましたがページという表現をしました。
ページも「=コンポーネント」ですが、ここでは Pages/TodoList.razor というページ(コンポーネント)を追加して実装してみます。

以下のコマンドで todo_blazor プロジェクトを作成しておきます。

dotnet new blazor -o todo_blazor

(1) モデルクラスを作成

Todoを表すモデルクラスを作成します。
todo_blazorフォルダ直下に Modelsフォルダ を作成します。
Models/Todo.csを追加します。

# Models/Todo.cs #
using System;

namespace todo_blazor.Models {
  public class Todo {
    public int ID { get; set; }

    public string Contents { get; set; }

    public bool IsCompleted { get; set; }
  }
}

普通のC#のPOCOなモデルクラスです。

(2) ページ(コンポーネント)を作成

UIを定義した Pages/TodoList.razor とそのコードビハインド Pages/TodoListBase.cs を追加します。

まず、Pages/TodoList.razor。

# Pages/TodoList.razor #

@page "/Todos/list"
@inherits todo_blazor.Pages.TodoListBase
@using todo_blazor.Models

@foreach( Todo todo in this.TodoList) {
  @if(todo.IsCompleted) {
    <s>@todo.Contents</s>
  }
  else {
    @todo.Contents
  }
  <br />
}
【@page】

@page は、Blazorのルーティング設定です。
「【アプリルート】/Todos/list」が、このページを表すURIになります。

【@inherits】

TodoList.razorのコードビハインドクラスを設定しています。
todo_blazor.Pages.TodoListBaseクラスがコードビハインドクラスになります。

【@using】

.razor内でのレンダリングにモデルクラスを利用する為、@using todo_blazor.Models を設定しています(C#のusingと同じです)。

レンダリングロジック】

@foreach / @if でTodoをレンダリングしています。
これは従来からのASP.NET / ASP.NET Core の Razorエンジンによるテンプレート記述と同じ方式で記述可能です。

次に、コードビハインドクラス Pages/TodoListBase.cs。

# Pages/TodoListBase.cs #

using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Components;
using calc_blazor.Models;

namespace todo_blazor.Pages
{
  public class TodoListBase : ComponentBase
  {
    public List<Todo> TodoList { get; } = new List<Todo>();
    
    protected override void OnInit() {
      this.TodoList.Add( new Todo() { ID = 0, Contents = "C#やる", IsCompleted = true } );
      this.TodoList.Add( new Todo() { ID = 1, Contents = "TypeScriptやる", IsCompleted = false } );
      this.TodoList.Add( new Todo() { ID = 2, Contents = "Babelやる", IsCompleted = true } );
      this.TodoList.Add( new Todo() { ID = 3, Contents = "Blazorやる", IsCompleted = false } );
      this.TodoList.Add( new Todo() { ID = 4, Contents = "Goやる", IsCompleted = false } );
    }
  }
}
【TodoListプロパティ】

TodoListプロパティにTodoのリストを保持し、プロパティとして公開しています。

【OnInit()メソッド】

OnInit()メソッドでTodoListを初期化しています。
OnInit()メソッドは、コンポーネントのライフサイクルメソッドで、コンポーネントの初期化時に呼び出されます。
通常のアプリではWebAPI呼び出しなどでTodoリストを取得すると思いますが、ここではローカルで固定の5つのTodoを作成します。
TodoListプロパティの値が TodoList.razor に適用されて画面にレンダリングされます。

(3) メニューにTodoListを追加

自動生成されたプロジェクトテンプレートでは、サイドメニューに「Home / Counter / Fetch data」の3つが並んでいます。
TodoList.razor へのメニューリンクを追加します。
Shared/NavMenu.razor を開き、<li>要素の並びに以下を追加します。

# Shared/NavMenu.razor #
...省略...

<li class="nav-item px-3">
  <NavLink class="nav-link" href="Todos/list">
    <span class="oi oi-list-rich" aria-hidden="true"></span> Todo List
  </NavLink>
</li>

...省略...

NavLink要素の href属性 がページのURIを表します。ページ側で @page で指定したルーティングとマッチしたページにリンクするようになります。

以上で完成です。
コマンドプロンプトで(プロジェクトフォルダで) donet run コマンドにより実行することができます。

2. まとめ

という事でComponent(コンポーネント)についてのまとめ。
BlazorのUI要素は .razor拡張子 で定義し、これらがComponentと呼ばれるものである。
ページやコントロール的なもの含めてComponent(コンポーネント) と呼ぶ。
Component内には、Componentをネストして配置することができる。
Componentは、プロパティやイベントを持つことができる。

次は ryuichi111std.hatenablog.com

BlazorでSPAするぞ!(1)

※ 2019/6/15 .NET Core 3.0 Preview 6 に対応した記述に修正しました。
以前からBlazorに興味を持ちつつもExperimentalだしなぁ、SPAは まぁ React+Redux で安定的な感じでいいんじゃね? などと思っていたら 2019/4/18 にExperimentalから正式Previewになったので、ちょろちょろ勉強してみました。

https://devblogs.microsoft.com/aspnet/blazor-now-in-official-preview/

個人的には当面、仕事で使うわけでもなく、忘れてしまいそうなので得た知識をBlogという形で書き記しておきます。
間違ってるか所あったら指摘よろ。

1. Blazorとは

公式Docsでは以下のように書かれています。

Blazor is a framework for building interactive client-side web UI with .NET

「Blazorは、.NETでインタラクティブなクライアントサイドWebユーザーインターフェイスを構築するためのフレームワークです。」ってことですね。
エンジニア的には、抽象的なモヤモヤ感のある文章ですね。
具体的に出来ることを簡単に言うと、今までJavaScript(+各種JSフレームワーク)でやってた事を、.NET Core(つまりC#)で作ることができるよ、ってものです。
JavaScript(or TypeScript) + React」とか「JavaScript(or TypeScript) + Vue」とかで作ってたようなSPAなアプリケーションをBlazorで作ることができます。
UI要素はHTML・CSSで構築し、ロジック部分をC#で記述するようなイメージになります。
メリットとしては「サーバーサイドもクライアントサイドもC#で開発できる」ってことです。
そもそもサーバーサイドにおいて非.NETなエンジニアにとっては、メリットとはならないものですが、.NETディベロッパーにとってはクライアントサイドもC#で書けるというのは生産性とかメンテナンス性とかでメリットがあります。
従来のWeb開発の React/Redux/TypeScript/Flow/Vue/Angular/Babel... の技術たちと比べるとキワ物感がありますが(Blazorは.NETに絞られる技術になるので)、ASP.NET Core+BlazorでのSPA開発は、かなり生産性高そうという印象を持っています(まだPreviewだし、実開発してない段階での感想ですが)。

1.1. WebAssembly

C#コードがクライアントサイドで動作するというキーワードは、過去にもいくつかありましたね。。。

2019年5月現在においては、オープンな技術開発的には使えない技術達ですね(一部のtoBでは生き残ってると思いますが・・・)。

で、Blazorがクライアントサイド(ブラウザ)でC#コードの動作を実現している仕組みですが、「WebAssembly」になります。
WebAssemblyは C/C++・Rust・Go言語 なんかで作れるようになっていますが、BlazorによってC#でもWebAssemblyが作れるようになりました。
厳密には、Blazorの.NET CoreアセンブリをMono WASMでWebAssemblyにビルドして実行するような形になります。

図にすると以下のような感じ。

f:id:daigo-knowlbo:20190501214517p:plain

BlazorとDOMとの間に矢印がありますが、Blazorの中でプログラムによって画面要素に変更があるとそれがDOMに伝えられて画面の更新が行われます。
Reactの仮想DOMと同様に、Blazorでは「render tree」と呼ばれるUI要素構造をメモリに展開し、変更の必要がある部分のみDOMに反映させる仕組みを持っています。

1.2. Client-Side BlazorとServer-Side Blazor

Blazorは上述のようにクライアントサイドでC#が動作する技術(Client-Side Blazor)ですが、これ以外に同じ実装をサーバーサイドで動作させるServer-Side Blazorというものがあります。
Blazorコンポーネントをサーバーサイドで実行する仕組みのものになります。クライアントサイドはプレーンなHTML+JSでWebAssemblyは実行されません。
実行イメージは以下のような感じ。

f:id:daigo-knowlbo:20190501214537p:plain

ボタンのクリックなど「Blazor的に影響のある事象が発生する」と、Signal Rでサーバにリクエストが行われ、サーバサイド(ASP.NET Core)でBlazorが動作し、結果をクライアントに送り返す。結果に応じたDOMの更新がクライアントで行われます(Web Formのポストバックみたいな処理が裏側で自動的に行われます)。

※ この投稿では特別に記述がない限り Client-Side Blazor を前提とした説明を行います。

2. 開発環境構築

開発環境はWindowsでもMacでも行けますが、ここではWindowsを前提とします。

2.1. .net Core 3インストール

以下から.NET Core 3.0 Preview SDKをダウンロードしてインストールします。

https://dotnet.microsoft.com/download/dotnet-core/3.0

コマンドプロンプトでバージョンを確認。

C:>dotnet --version 3.0.100-preview6-012264

※2019/5/1現在の最新は 3.0.100-preview4-011223
※2019/6/15現在の最新は 3.0.100-preview6-012264

2.2. Blazorテンプレートのインストール

コマンドプロンプトで以下のコマンドを実行します。
(dotnet newコマンドに、Blazorのプロジェクトテンプレートがインストールされます)

dotnet new -i Microsoft.AspNetCore.Blazor.Templates::3.0.0-preview6.19307.2

2.3. C# for Visual Studio Codeのインストール(Visual Studio Code利用の場合)

Visual Studio Codeを使う場合は、「C# for Visual Studio Code」をインストールしておきます。

https://marketplace.visualstudio.com/items?itemName=ms-vscode.csharp

2.4. Visual Studio Extensionインストール(Visual Studio 2019利用の場合)

Visual Studio 2019を使う場合は、「BlazorのExtension」をインストールしておきます。
(Visual Studio 2019に、Blazorのプロジェクトテンプレートがインストールされます)

https://marketplace.visualstudio.com/items?itemName=aspnet.blazor

※本記事では Visual Studio Code を開発に利用します。(VS2019だと.razorのファイルの追加がUIから出来ない等Extensionが十分にPreview版に追い付いていない感があったので)

3. Blazorプロジェクト作成

では早速Blazorプロジェクトを作成します。

3.1. dotnet コマンド / 4つのプロジェクトテンプレート

VS Codeベースで進めるのでdotnetコマンドを使っていきます。

まずプロジェクトテンプレートの確認を。
dotnet new」でテンプレート一覧を確認すると以下のようにBlazor関連のテンプレートが4種類表示されます。

C:\>dotnet new
使用法: new [オプション]
...省略

Templates                                         Short Name            Language          Tags
-------------------------------------------------------------------------------------------------------------------------------
...省略
Blazor (server-side)                              blazorserverside      [C#]              Web/Blazor
Blazor (ASP.NET Core hosted)                      blazorhosted          [C#]              Web/Blazor/Hosted
Blazor Library                                    blazorlib             [C#]              Web/Blazor/Library
Blazor (client-side)                              blazor                [C#]              Web/Blazor/Standalone
...省略

それぞれ以下の通りです。

(1) Blazor (server-side)

Server side BlazorのASP.NET Coreプロジェクトテンプレートです。

dotnet new blazorserverside -o blazor_serverside

作成されるファイル達↓↓↓

f:id:daigo-knowlbo:20190501214557p:plain

(2) Blazor (ASP.NET Core hosted)

Client side Blazor + BlazorにWebAPIを提供するASP.NET Core + Sharedライブラリ(Client side BlazorとASP.NET Coreサーバとの間でモデルクラス等を共通するライブラリ)のテンプレートです。

dotnet new blazorhosted -o blazor_hosted

作成されるファイル達↓↓↓
f:id:daigo-knowlbo:20190501214618p:plain

(3) Blazor Library

Blazorコンポーネントのライブラリプロジェクトのテンプレートです。

dotnet new blazorlib -o blazor_lib

作成されるファイル達↓↓↓
f:id:daigo-knowlbo:20190501214636p:plain

(4) Blazor (client-side)

Client side Blazorのプロジェクトテンプレートです。

dotnet new blazor -o blazor

作成されるファイル達↓↓↓
f:id:daigo-knowlbo:20190501214648p:plain

4. Hello Blazor

では、純粋にBlazorを試すために「Blazor (client-side)」でプロジェクトを作成します。

ますプロジェクトを作成。hello_blazorという名前にします。

dotnet new blazor -o hello_blazor

4.1. Visual Studio Codeで開く

VS Codeの「File - Open Folder」メニューで 作成した hello_blazor フォルダを開きます。

f:id:daigo-knowlbo:20190501214702p:plain

4.2. ビルド + 実行する

ある程度の動作する実装がテンプレートで出力されているので、ひとまずそのままビルドします。
コマンドプロンプトでプロジェクトフォルダに移動し、「dotnet build」コマンドを実行します。

C:\workspace\for_blog\hello_blazor>dotnet build
.NET Core 向け Microsoft (R) Build Engine バージョン 16.2.0-preview-19278-01+d635043bd
Copyright (C) Microsoft Corporation.All rights reserved.

  C:\cbt\github\hello_blazor\hello_blazor.csproj の復元が 21.57 ms で完了しました。
C:\Program Files\dotnet\sdk\3.0.100-preview6-012264\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.RuntimeIdentifierInference.targets(158,5): message NETSDK1057: プレビュー版の .NET Core を使用しています。https://aka.ms/dotnet-core-preview をご覧ください [C:\cbt\github\hello_blazor\hello_blazor.csproj]
  hello_blazor -> C:\cbt\github\hello_blazor\bin\Debug\netstandard2.0\hello_blazor.dll
  Processing embedded resource linker descriptor: mscorlib.xml
  Duplicate preserve in resource mscorlib.xml in mscorlib, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e of System.Threading.WasmRuntime (All).  Duplicate uses (All)
  Type System.Reflection.Assembly has no fields to preserve
  Type Mono.ValueTuple has no fields to preserve
  Output action:     Link assembly: hello_blazor, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
  Output action:     Save assembly: System.Threading.Tasks.Extensions, Version=4.2.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
  Output action:     Save assembly: System.Text.Json, Version=4.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
  Output action:     Save assembly: System.Runtime.CompilerServices.Unsafe, Version=4.0.5.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
  Output action:     Save assembly: System.Numerics.Vectors, Version=4.1.4.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
  Output action:     Save assembly: System.Memory, Version=4.0.1.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
  Output action:     Save assembly: System.ComponentModel.Annotations, Version=4.2.2.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
  Output action:     Save assembly: System.Buffers, Version=4.0.3.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
  Output action:     Save assembly: Mono.WebAssembly.Interop, Version=3.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
  Output action:     Save assembly: Microsoft.JSInterop, Version=3.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
  Output action:     Save assembly: Microsoft.Extensions.Primitives, Version=3.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
  Output action:     Save assembly: Microsoft.Extensions.Options, Version=3.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
  Output action:     Save assembly: Microsoft.Extensions.Logging.Abstractions, Version=3.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
  Output action:     Save assembly: Microsoft.Extensions.DependencyInjection.Abstractions, Version=3.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
  Output action:     Save assembly: Microsoft.Extensions.DependencyInjection, Version=3.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
  Output action:     Save assembly: Microsoft.AspNetCore.Metadata, Version=3.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
  Output action:     Save assembly: Microsoft.AspNetCore.Components.Browser, Version=3.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
  Output action:     Save assembly: Microsoft.AspNetCore.Components, Version=3.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
  Output action:     Save assembly: Microsoft.AspNetCore.Blazor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
  Output action:     Save assembly: Microsoft.AspNetCore.Authorization, Version=3.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
  Output action:   Delete assembly: netstandard, Version=2.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
  Output action:     Link assembly: mscorlib, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e
  Output action:     Link assembly: System, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e
  Output action:     Link assembly: Mono.Security, Version=2.0.5.0, Culture=neutral, PublicKeyToken=0738eb9f132ed756
  Output action:   Delete assembly: System.Xml, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e
  Output action:   Delete assembly: System.Numerics, Version=2.0.5.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
  Output action:     Link assembly: System.Core, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e
  Output action:   Delete assembly: System.Data, Version=2.0.5.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
  Output action:   Delete assembly: System.Drawing.Common, Version=4.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
  Output action:   Delete assembly: System.IO.Compression, Version=2.0.5.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
  Output action:   Delete assembly: System.IO.Compression.FileSystem, Version=2.0.5.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
  Output action:   Delete assembly: System.ComponentModel.Composition, Version=2.0.5.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
  Output action:     Link assembly: System.Net.Http, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
  Output action:   Delete assembly: System.Runtime.Serialization, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e
  Output action:   Delete assembly: System.Transactions, Version=2.0.5.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
  Output action:   Delete assembly: System.Web.Services, Version=2.0.5.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
  Output action:   Delete assembly: System.Xml.Linq, Version=2.0.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35
  Output action:   Delete assembly: System.ServiceModel.Internals, Version=0.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35
  Output action:   Delete assembly: System.Runtime, Version=4.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
  Writing boot data to: C:\cbt\github\hello_blazor\obj\Debug\netstandard2.0\blazor\blazor.boot.json
  Blazor Build result -> 31 files in C:\cbt\github\hello_blazor\bin\Debug\netstandard2.0\dist

ビルドに成功しました。
    0 個の警告
    0 エラー

経過時間 00:00:09.00

obj / binフォルダが作成され、その下にアセンブリ(.dll)が出力されました。

では以下のように dotnet run コマンドを実行します。

C:\workspace\for_blog\hello_blazor>dotnet run
Hosting environment: Production
Content root path: C:\workspace\for_blog\hello_blazor
Now listening on: http://localhost:5000
Now listening on: https://localhost:5001
Application started. Press Ctrl+C to shut down.

Blazor自体はClient sideのWebAssembly(の上で動く.NETアセンブリ)ですが、http://localhost:5000 / https://localhost:5001 としてhello_blazorをホストするHttpサーバが立ち上がります。
ブラウザで http://localhost:5000 にアクセスすると、テンプレートで出力したサンプル実装が起動します。

f:id:daigo-knowlbo:20190501214729p:plain

Hello World以上の立派な実装がなされています。

ちなみにこのページを表示した時のHttpRequestの状況ですが、以下のようになっています。

f:id:daigo-knowlbo:20190501214743p:plain

よく見ると「mono.js」とか「mono.wasm」とか「hello_blazor.dll」とか「Microsoft.AspNetCore.Blazor.dll」とか・・・.NETが動こうとしている事が見て取れると思います。

それから、左のサイドメニューから「+Counter」を選択すると、ボタンクリックでカウントアップが行われる画面に移動します。
「Click me」ボタンをクリックすると、Current Countが1ずつカウントアップしますが、この時サーバへのリクエストが飛んでいないことも確認できます。
(当然ながらクライアントサイドでC#コードが実行されているので、このようなロジック処理でサーバへのラウンドトリップは発生しない、という事です)

f:id:daigo-knowlbo:20190501214802p:plain

4.3. Hello Blazorに着手!

何もしないで http://localhost:5000 にアクセスした画面がすでに「Hello, World!」になっています。
これを「Hello Blazor」に改造していこうと思います。
Blazorが動作するベースの仕組みは色々ありますが、、、とりあえず このページは「Pages/index.razor」で実装されています。

# Pages/Index.razor #

@page "/"

<h1>Hello, world!</h1>

Welcome to your new app.

<SurveyPrompt Title="How is Blazor working for you?" />

実行画面と見比べると 大体想像できる感じ ですよね(HTMLタグが書かれていて、ASP.NET CoreのRazorを知っていれば @xx なRazor構文が書かれているな・・と)。
画面上にヘッダーやサイドメニューが付いてるのはレイアウト設定が効いているからです(ASP.NET Coreの _Layout.cshtml とかと同じような仕組みがBlazorにもあり、それが適用されています )。
先頭の「@page "/"」は後(後続の投稿)で詳細を説明しますが、ルーティング設定です。"/"なのでルートとしてこのIndex.razorがルーティング適用されています。

(1) プロパティ変数の利用

以下のような画面に改造したいと思います。

f:id:daigo-knowlbo:20190501214817p:plain

HTMLをべた書きすれば実現できるのですが、それではあまり意味がないので、無理やりC#なコードを使っていきます。

# Pages/Index.razor(Hello Blazor版 その1) #

@page "/"

<h1>@Message</h1>

@code {
  private string Message = "Hello Blazor";
}

「@Message」は、C#で定義した変数(プロパティ)をレンダリングします。ASP.NETのRazorと完全に一緒です。
「@code」は、このコンポーネント(Index.razor)のコードブロックを表します。コードブロックには、C#で.NETコードを記述することができます。
string型のMessage変数を定義し、"Hello Blazor"で初期化しているのは見れば分かるでしょう。

こんな感じでクライアントサイド実装をC#で行うことができます。

(2) イベントの使用

では、次に以下のような画面に改造します。
「get date and time」ボタンを用意し、クリックされたらすぐ上にテキストで現在日時を表示します。

f:id:daigo-knowlbo:20190501214833p:plain

# Pages/index.razor(Hello Blazor版 その2) #

@page "/"

<h1>@Message</h1>
<br />
CurrentDateTime:@CurrentDateTime
<br />
<input type="button" @onclick="@GetDateTime" value="get date and time" />

@code {
  private string Message = "Hello Blazor";
  private string CurrentDateTime = "???";
  
  private void GetDateTime() {
    CurrentDateTime = DateTime.Now.ToString("yyyy/MM/dd H:m:s");
  }
}
@CurrentDateTime

CurrentDateTime変数を用意し(初期値は ??? )、「CurrentDateTime:@CurrentDateTime」として画面に表示しています。

ボタン

「<input type="button" @onclick="@GetDateTime" value="get date and timee" />」でボタンを配置しています。
「@onclick="@GetDateTime"」がポイントで、onclickはHTML/JavaScriptにおけるinputタグのイベントと同様です。
これに対して GetDateTime() メソッドをバインディングしています(onclickに対してJavaScriptのFunctionをバインドするのと同じ感覚ですね)。
補足としては、GetDateTime()は @code{} コードブロックで定義したメソッドであり、C#メソッドの呼び出しなのでRazor構文の「@」付けて呼び出しを記述します。
GetDateTime()では DateTime.Now.ToString() という.NETのC#コードにより CurrentDateTime 変数を変更しています。
CurrentDateTimeの変更を受けBlazor Runtimeは、対象箇所の再レンダリングを行います。

良い感じにプロパティとイベントをC#で実装し、HTML要素に反映することが出来ましたね。

※ 「@onclick="@GetDateTime"」の@GetDateTime部分は将来的に「@プレフィックス」を不要とする方針のようです。

4.4. CodeBehindを使う

上述のIndex.razorは、.razorファイルに UI要素 も コード も混ざった状態になってしまいました。
よくある話ですが、Blazorでも UIとコード は分離可能です。

# Pages/Index.razor(code behind版) #

@page "/"
@inherits hello_blazor.Pages.IndexBase

<h1>@Message</h1>
<br />
CurrentDateTime:@CurrentDateTime
<br />
<input type="button" @onclick="@GetDateTime" value="get date and time" />
# Pages/IndexBase.cs(code behind版) #

using System;
using Microsoft.AspNetCore.Components;

namespace hello_blazor.Pages
{
  public class IndexBase : ComponentBase
  {
    public string Message { get; set; } = "Hello Blazor";

    public string CurrentDateTime { get; set; }= "???";

    public void GetDateTime() {
      CurrentDateTime = DateTime.Now.ToString("yyyy/MM/dd H:m:s");
    }
  }
}

(1) @inherits

.razorファイルに「@inherits 【クラス名】」とすることでCodeBehindファイルを指定することができます。
で、CodeBehindクラス側は、ここでは「Pages/IndexBase.cs」としましたが、どこに置いてもどんな名前でも問題はありません。
大事なのは、コードビハインドクラスは「Microsoft.AspNetCore.Components.ComponentBase」を継承するという事です。

(2) プロパティとメソッド

Message / CurrentDateTimeが、フィールドとして定義していて不格好だったのでgetter / setterを持つプロパティとしました。
また、2つのプロパティおよびGetDateTime()メソッドはアクセス修飾子を private → public に変更しています。
private だとビルドエラーになります。最低限必要なのは protected です。
.razor上の @code{}内では private で動作したものが、CodeBehind版ではprotected以上のアクセス権限が必要になった理由は、ビルド後の hello_blazor.dll をIlSpy等で確認すると意味が理解できます。

f:id:daigo-knowlbo:20190501214853p:plain

Index.razorはコンパイルされ「Indexクラス」となりました。
Indexクラスは「IndexBaseクラス」を継承しています。IndexBaseクラスは「Pages/IndexBase.cs」で定義したCodeBehindクラスです。
つまり、Index <- IndexBase という継承関係ですからIndexBaseに定義するプロパティ・メソッドはprotectedもしくはpublicでなければIndexからは参照できなくなっています。

アーキテクチャ的には MVVM で実装するとかの選択肢もあるので、コードを code{} に書くとか コードビハインドに書くとか がベストプラクティスどうかは、プロジェクトごとの選択になります。

5. まとめ

という感じで、Blazorではじめの一歩が踏み出せました!

次は ryuichi111std.hatenablog.com

BlazorでSPAするぞ! - 目次

※ 2019/6/15 .NET Core 3.0 Preview 6 に対応した記述に修正しました。

2019年GWを使って Blazor についての基本をブログにまとめました。

本投稿は「目次」になります。

ryuichi111std.hatenablog.com

ryuichi111std.hatenablog.com

ryuichi111std.hatenablog.com

ryuichi111std.hatenablog.com

ryuichi111std.hatenablog.com

ryuichi111std.hatenablog.com

ryuichi111std.hatenablog.com