BlazorでSPAするぞ!(6) - Routing -正式版対応済み

※最終更新日: 2020/5/24 正式リリース版に対応修正しました。

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

ryuichi111std.hatenablog.com

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

1. ルーティング(Routing)

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

dotnet new blazorwasm -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. 無効ルーティング時の表示

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

# /App.razor #

...
<NotFound>
  <LayoutView Layout="@typeof(MainLayout)">
    <p>Sorry, there's nothing at this address.</p>
  </LayoutView>
</NotFound>
...

「<p>Sorry, there's nothing at this address.</p>」部分は任意のコンポーネントに置き換えることも可能です。
以下のように「Shared/FallbackPage.razor」ページを用意して、App.razorのNotFountコンテンツとして指定します。

# Shared/FallbackPage.razor #

@DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")
<p>無効なパスが要求されました;;</p>
# App.razor #

<Router AppAssembly="@typeof(Program).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
    </Found>
    <NotFound>
        <LayoutView Layout="@typeof(MainLayout)">
            <FallbackPage />
        </LayoutView>
    </NotFound>
</Router>

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

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

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

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

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

f:id:daigo-knowlbo:20200524160110p: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>

@code {
  [Parameter]
  public 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) - レイアウト -正式版対応済

※最終更新日: 2020/5/24 正式リリース版に対応修正しました。

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

ryuichi111std.hatenablog.com

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

1. レイアウト機能とは

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

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

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

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

dotnet new blazorwasm -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 System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
@using layout_blazor
@using layout_blazor.Shared

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

(2) DefaultLayout

App.razorを見ると、Routerが定義されていますが、その中で「DefaultLayout」値が設定されています。
以下のように「DefaultLayout="@typeof(MainLayout)」と定義されています。
つまり、このアプリケーション内では「MainLayout」がデフォルトのレイアウトとして適用されます。

# App.razor #

<Router AppAssembly="@typeof(Program).Assembly">
    <Found Context="routeData">
        <RouteView
          RouteData="@routeData"
          DefaultLayout="@typeof(MainLayout)" />
    </Found>
    <NotFound>
        <LayoutView Layout="@typeof(MainLayout)">
            <p>Sorry, there's nothing at this address.</p>
        </LayoutView>
    </NotFound>
</Router>

(3) MainLayout

MainLayoutは、どこで定義されているでしょうか?
これは、Sharedフォルダ配下で定義されています。
App.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」名前空間に定義されたクラスです。
レイアウトを定義するクラスは「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 {
  private bool collapseNavMenu = true;

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

  private 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) - データバインド(おかわり) - 正式版対応済

※最終更新日: 2020/5/24 正式リリース版に対応修正しました。

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

ryuichi111std.hatenablog.com

1. データバインド

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

1.1. サンプル

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

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

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

dotnet new blazorwasm -o bind_ex_blazor

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

# Components/ChildComponent.razor #

<div class="container bg-secondary">
  <h2>Child Component</h2>

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

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

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

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

# Pages/Index.razor #

@page "/"
@using bind_ex_blazor.Components

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

@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 になります。

以下は 110 までincrementした状態。

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

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

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

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

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

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

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

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

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

# Components/ChildComponent.razor #

<div class="container bg-secondary">
  <h2>Child Component</h2>

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

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

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

f:id:daigo-knowlbo:20200524151741p: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

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

@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 #

<div class="container bg-secondary">
  <h2>Child Component</h2>

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

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

    private int _yourScore = 0;

    [Parameter]
    public 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:20200524151812p: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
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;

public class ChildComponent : ComponentBase
{
    private int _yourScore = 0;

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

    [Parameter]
    public 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", BindConverter.FormatValue(YourScore));
        __builder.AddAttribute(11, "onchange", EventCallback.Factory.CreateBinder(this, delegate(int __value)
        {
            YourScore = __value;
        }, YourScore));
        __builder.SetUpdatesAttributeName("value");
        __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<MouseEventArgs>((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<MouseEventArgs>((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(YourScore));
    __builder.AddAttribute(16, "YourScoreChanged", RuntimeHelpers.TypeCheck(EventCallback.Factory.Create(this, RuntimeHelpers.CreateInferredEventCallback(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) - パラメータ - 正式版対応済

※最終更新日: 2020/5/24 正式リリース版に対応修正しました。

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

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 blazorwasm -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]
  public int ID { get; set; }
  
  [Parameter]
  public string FirstName{ get; set; }

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

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

(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 OnInitialized() {
    this.CurrentPerson = new Person() {
      ID = 0,
      FirstName = "takashi",
      LastName = "sakata" } ;
  }
}

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

f:id:daigo-knowlbo:20200524144749p: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]
  public 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 OnInitialized() {
    this.CurrentPerson = new Person() {
      ID = 0,
      FirstName = "takashi",
      LastName = "sakata" } ;
  }
}

2. Cascading Parameter

親子間のプロパティの受け渡しは、前述の通りコンポーネント定義にプロパティ設定を行えば可能です。
親子関係の階層が深くなった場合に、数珠繋ぎが煩雑になることに対する解決策の1つが 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]
  public string YourName { get; set; }
}
# Components/GrandChildComponent.razor (個コンポーネントに配置される 孫コンポーネント)#

this is GrandChild.
<br />
@YourName

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

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

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

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

# Pages/Index.razor #

@page "/"
@using component_param_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」属性をプロパティに付与することでパラメータを引き継ぐことができます。

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

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

3. まとめ

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

次は ryuichi111std.hatenablog.com

BlazorでSPAするぞ!(2) - Component -正式版対応済

※最終更新日: 2020/5/24 正式リリース版に対応修正しました。

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

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>dotnet new blazorwasm -o simple_component_blazor
The template "Blazor WebAssembly App" was created successfully.

Processing post-creation actions...
Running 'dotnet restore' on C:\workspace\simple_component_blazor\simple_component_blazor.csproj...
  復元対象のプロジェクトを決定しています...
  C:\workspace\simple_component_blazor\simple_component_blazor.csproj を復元しました (192 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.razorは、simple_component_blazr.Pages名前空間のIndexクラスになります。
SimpleHello.razorは、simple_component_blazor.Components名前空間のSimpleHelloクラスになります。
Pages/Index.razorからは、別の名前空間コンポーネントを使うことになるので「@using simple_component_blazor.Components」として名前空間への参照を指定しておきます。

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

@namespace hogehoge.ugougo

@Message

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

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

※ using指定せず名前空間コンポーネント名をフルに記述してコンポーネントを配置することも可能

@page "/"

<simple_component_blazor.Components.SimpleHello />
「実行画面」

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

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

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

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

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

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

C:\workspace>dotnet new blazorwasm -o calc_blazor
The template "Blazor WebAssembly App" was created successfully.

Processing post-creation actions...
Running 'dotnet restore' on C:\workspace\calc_blazor\calc_blazor.csproj...
  復元対象のプロジェクトを決定しています...
  C:\workspace\calc_blazor\calc_blazor.csproj を復元しました (192 ms)。

Restore succeeded.

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

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

# Components/Calculator.razor #

<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/Calculator.razor.cs #

using System;

namespace calc_blazor.Components
{
  public partial class Calculator
  {
    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;
    }
  }
}
「Calculatorクラス」

コンポーネントロジッククラスの実装です。(partial形式で実装しています。https://ryuichi111std.hatenablog.com/entry/2019/05/01/220242
Value1 / Value2 は2つの入力項目、Result は計算結果を表すプロパティです。
また Calc()メソッド は、Value1 と Value2 の和を Result に設定する計算メソッドです。

「Calculator.razor」

主にUIを定義したrazorファイルです。
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にイベント(コールバック)を追加

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

# Components/Calculator.razor.cs #

using System;
using Microsoft.AspNetCore.Components;

namespace calc_blazor.Components
{
  public partial class Calculator
  {
    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バインディングに変更) #

<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:20200524140156p: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:20200524140217p: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", BindConverter.FormatValue(Value1));
  __builder.AddAttribute(3, "onchange", EventCallback.Factory.CreateBinder(this, delegate(int __value)
  {
    Value1 = __value;
  }, Value1));
  __builder.SetUpdatesAttributeName("value");
  __builder.CloseElement();
  __builder.AddMarkupContent(4, "\r\n+\r\n");
  __builder.OpenElement(5, "input");
  __builder.AddAttribute(6, "type", "text");
  __builder.AddAttribute(7, "value", BindConverter.FormatValue(Value2));
  __builder.AddAttribute(8, "onchange", EventCallback.Factory.CreateBinder(this, delegate(int __value)
  {
    Value2 = __value;
  }, Value2));
  __builder.SetUpdatesAttributeName("value");
  __builder.CloseElement();
  __builder.AddMarkupContent(9, "\r\n=\r\n");
  __builder.OpenElement(10, "input");
  __builder.AddAttribute(11, "type", "text");
  __builder.AddAttribute(12, "readonly", "true");
  __builder.AddAttribute(13, "value", 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<MouseEventArgs>((object)this, (Action)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:20200524140323p:plain

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

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

dotnet new blazorwasm -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.Todos) {
  @if(todo.IsCompleted) {
    <s>@todo.Contents</s>
  }
  else {
    @todo.Contents
  }
  <br />
}
【@page】

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

【@inherits】

TodoList.razorのコードビハインドクラスを設定しています。
todo_blazor.Pages.TodoListBaseクラスがコードビハインドクラスになります(ここでは partial形式 ではなく、ComponentBaseクラスを継承した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 todo_blazor.Models;

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

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

【OnInitialized()メソッド】

OnInitialized()メソッドでTodoListを初期化しています。
OnInitialized()メソッドは、Blazorコンポーネントのライフサイクルメソッドで、コンポーネントの初期化時に呼び出されます。
通常のアプリではWebAPI呼び出しなどでTodoリストを取得すると思いますが、ここではローカルで固定の5つのTodoを作成します。
Todosプロパティの値が 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) - 正式版対応済

※最終更新日: 2020/5/24 正式リリース版に対応修正しました。

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

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

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

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

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

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

1.2. Blazor WebAssembly(Client-Side Blazor) と Blazor Server(Server-Side Blazor)

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

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

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

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

2. 開発環境構築

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

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

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

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

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

C:>dotnet --version 3.1.300

※2020/5/24現在の最新は 3.1.300

### 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 App                                 blazorserver             [C#]              Web/Blazor
Blazor WebAssembly App                            blazorwasm               [C#]              Web/Blazor/WebAssembly
...省略

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

(1) Blazor Server App

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

dotnet new blazorserver -o BlazorServerApp1

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

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

(2) Blazor WebAssembly App

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

dotnet new blazorwasm -o BlazorWasmApp1

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

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

(3) Blazor WebAssembly App with hosted app

blazorwasmテンプレートに --hosted オプションを追加すると、Blazor WebAssembly + ASP.NET Core WebAPI構成のソリューションが作成されます。
Blazor WebAssemblyの通信先WebAPIサービスとしてのASP.NET Coreプロジェクトもセットで作成されるイメージです。

dotnet new blazorwasm --hosted -o BlazorWasmAppWithHosted1

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

4. Hello Blazor WebAssembly

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

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

dotnet new blazorwasm -o hello_blazor

4.1. Visual Studio Codeで開く

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

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

4.2. ビルド + 実行する

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

daigo@ardentred MINGW64 /c/workspace/hello_blazor
$ dotnet build
.NET Core 向け Microsoft (R) Build Engine バージョン 16.6.0+5ff7b0c9e
Copyright (C) Microsoft Corporation.All rights reserved.

  復元対象のプロジェクトを決定しています...
  復元対象のすべてのプロジェクトは最新です。
  hello_blazor -> C:\workspace\hello_blazor\bin\Debug\netstandard2.1\hello_blazor.dll
  hello_blazor (Blazor output) -> C:\workspace\hello_blazor\bin\Debug\netstandard2.1\wwwroot

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

経過時間 00:00:03.30

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

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

daigo@ardentred MINGW64 /c/workspace/hello_blazor
$ dotnet run
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: C:\workspace\hello_blazor

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

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

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

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

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

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

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

f:id:daigo-knowlbo:20200524125154p: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:20200524125221p: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:20200524125242p: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()では DateTime.Now.ToString() という.NETのC#コードにより CurrentDateTime 変数を変更しています。
CurrentDateTimeの変更を受けBlazor Runtimeは、対象箇所の再レンダリングを行います。

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

※ 「@onclick="GetDateTime"」の@GetDateTime部分は将来的に「@プレフィックス」を不要とする方針のようです。 ← 正式版出で不要になりました。

4.4. CodeBehindを使う

上述のIndex.razorは、.razorファイルに UI要素 も コード も混ざった状態になってしまいました。
よくある話ですが、Blazorでも UIとコード は分離可能です。
「UI(razor定義)」と「コード(C#ページロジック)」を分離する方法は2つあります。
1つは「razor定義のpartialクラスを実装する方法」、もう1つは「定義したページクラスをrazorで継承する方法」です。

(1) razor定義のpartialクラスを実装する方法

以下がrazor定義です。
@codeセクションを定義せず、UI定義のみを行っています。

# Pages/Index.razor (razor定義のpartialクラスを実装する方法) #

@page "/"

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

次に、ページロジッククラスを定義するIndex.csファイルを用意します。
定義するクラス名はrazor定義のファイル名「Index」と同様にします。また、partialクラスとして定義することが重要です。
razor定義側で要求される仕様に合わせ、Message / CurrentDateTimeプロパティ、GetDateTime()メソッドを実装します。

# Pages/Index.cs (razor定義のpartialクラスを実装する方法) #

using System;

namespace hello_blazor.Pages
{
  public partial class Index
  {
    private string Message { get; set; } = "Hello Blazor";

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

    private void GetDateTime() {
      CurrentDateTime = DateTime.Now.ToString("yyyy/MM/dd H:m:s");
    }
  }
}
何故、クラス名をIndexにして、partialクラスで定義し、privateメンバーを定義したのか?

「razor定義のpartialクラスを実装する方法」は上記に説明した通りですが、「何故~」について少し深堀したいと思います。
答えは、コーディングしたhello_blazorを「dotnet build」により生成した「hello_blazor.dll」を見ることで明確になります。
hello_blazor.dllを ILSpy で逆コンパイルした結果が以下です。

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

// hello_blazor.Pages.Index
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Web;
using System;

[Route("/")]
public class Index : ComponentBase
{
    private string Message
    {
        get;
        set;
    } = "Hello Blazor";


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


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

    protected override void BuildRenderTree(RenderTreeBuilder __builder)
    {
        __builder.OpenElement(0, "h1");
        __builder.AddContent(1, Message);
        __builder.CloseElement();
        __builder.AddMarkupContent(2, "\r\n<br>\r\nCurrentDateTime:");
        __builder.AddContent(3, CurrentDateTime);
        __builder.AddMarkupContent(4, "\r\n<br>\r\n");
        __builder.OpenElement(5, "input");
        __builder.AddAttribute(6, "type", "button");
        __builder.AddAttribute(7, "onclick", EventCallback.Factory.Create<MouseEventArgs>((object)this, (Action)GetDateTime));
        __builder.AddAttribute(8, "value", "get date and time");
        __builder.CloseElement();
    }
}

Index.razor / Index.razor.cs がコンパイルされることで、「Microsoft.AspNetCore.Components.ComponentBase」クラスを継承した「Index」クラスとなりました(名前空間は hello_blazor.Pages)。

まず、前提として .razor 定義は「Microsoft.AspNetCore.Components.ComponentBaseを継承したクラス(名前空間・クラス名は、プロジェクト・フォルダ名・razorファイル名から決定)」にコンパイルされます。そして、HTMLタグ や @プレフィックス 等による定義は「BuildRenderTree()」メソッド内でC#コードによる要素構築に変換されます。
つまり、Index.razor.cs で上記名前空間・クラス名に合わせたクラスを partial で定義すれば、razorから自動生成されるクラスのパーシャルクラス定義とすることができます。
これが「class partial Index { }」がIndex.razorの分離コード定義となる仕組みです。
そして、「Message / CurrentDateTime / GetDateTime()」が private のスコープ定義で事足りた理由も同じところにあります。Index.razorから自動生成されたIndexクラスと、Index.razor.csで定義したIndexクラスとは、partialによりコード上分離した位置に定義しただけであり、コンパイル後の実体は同一だからです。

(2) 定義したページクラスをrazorで継承する方法

以下がrazor定義です。

# Pages/Index.razor (定義したページクラスをrazorで継承する方法) #

@page "/"
@inherits IndexBase

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

「@inherits IndexBase」により、このrazor定義はIndexBaseクラスを継承することを明示しています。

次に、ページロジッククラスを定義するIndexBase.csファイルを用意します。
定義するクラス名は、razorファイル定義内の@inheritsでも指定した通り「IndexBase」です。
ページクラス(厳密にはBlazorのコンポーネントクラス)は「Microsoft.AspNetCore.Components.ComponentBase」を継承して作成します。
razor定義側で要求される仕様に合わせ、Message / CurrentDateTimeプロパティ、GetDateTime()メソッドを実装します。

# Pages/Index.cs (定義したページクラスをrazorで継承する方法) #

using System;
using Microsoft.AspNetCore.Components;

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

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

    protected void GetDateTime() {
      CurrentDateTime = DateTime.Now.ToString("yyyy/MM/dd H:m:s");
    }
  }
}
@inherits、partialでなくなったページクラス定義、プロパティ・メソッドはprotectedになった

partialクラスによるUIとロジックの分離実装からいくつかの要素に変更が加わっています。
再度、コンパイルにより得られた「hello_blazor.dll」を見ると根柢の仕組みが見えてきます。
先ほどと同様に ILSpy を使ってみましょう。

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

// hello_blazor.Pages.Index
using hello_blazor.Pages;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Web;
using System;

[Route("/")]
public class Index : IndexBase
{
    protected override void BuildRenderTree(RenderTreeBuilder __builder)
    {
        __builder.OpenElement(0, "h1");
        __builder.AddContent(1, base.Message);
        __builder.CloseElement();
        __builder.AddMarkupContent(2, "\r\n<br>\r\nCurrentDateTime:");
        __builder.AddContent(3, base.CurrentDateTime);
        __builder.AddMarkupContent(4, "\r\n<br>\r\n");
        __builder.OpenElement(5, "input");
        __builder.AddAttribute(6, "type", "button");
        __builder.AddAttribute(7, "onclick", EventCallback.Factory.Create<MouseEventArgs>((object)this, (Action)base.GetDateTime));
        __builder.AddAttribute(8, "value", "get date and time");
        __builder.CloseElement();
    }
}

// hello_blazor.Pages.IndexBase
using Microsoft.AspNetCore.Components;
using System;

public class IndexBase : ComponentBase
{
    protected string Message
    {
        get;
        set;
    } = "Hello Blazor";


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


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

IndexクラスとIndexBaseクラスが別々に定義されています。そして、IndexはIndexBaseを継承しています。
これは実装した通りですが、IndexBaseクラスはpartialではなくComponentBaseクラスを継承した独立定義したクラスとして実装しました(IndexBase.cs)。
そして Index.razor では、「@inherits IndexBase」と明示的にIndexBaseクラスを継承する定義を行いました。

「.razorによりUI定義を行った結果生成されたIndexクラス」と「UIロジック実装として用意したIndexBaseクラス」は継承関係にあることから、「Message / CurrentDateTimeプロパティ、GetDateTime()メソッド」は privateスコープでは不足、「protected」以上のスコープが必要となります。

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

5. まとめ

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

次は ryuichi111std.hatenablog.com

BlazorでSPAするぞ! - 目次 -正式版対応済

※最終更新日: 2020/5/24 正式リリース版に対応修正しました。

Blazor(主にWebAssembly)についての基本をブログにまとめました。

2019年のGWにBlazorに興奮して初校を書きましたが、
2020年5月、遂に Blazor WebAssembly が正式リリースとなったので、改めて正式リリース版に合わせた記述に改訂しました。

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

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

ryuichi111std.hatenablog.com

ryuichi111std.hatenablog.com

ryuichi111std.hatenablog.com

ryuichi111std.hatenablog.com

ryuichi111std.hatenablog.com

ryuichi111std.hatenablog.com

ryuichi111std.hatenablog.com

ryuichi111std.hatenablog.com