BlazorでSPAするぞ!(2) - Component -正式版対応済
※最終更新日: 2020/5/24 正式リリース版に対応修正しました。
という事で、↓↓↓の続きです。
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で実行した画面は以下です。
1.2. Calculatorコンポーネントの作成(.razor)
次にCalculatorコンポーネントというものを作成したいと思います。
完成画面は以下です。
入力テキストボックスを2つ配置し、その和をreadonlyのテキストボックスに表示します。
まずは 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
[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での実行画面は以下です。
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の値が反映されなかった)。
(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で逆コンパイルした画面が以下です。
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表示ページとします。
以下のイメージです。
これまでコンポーネントという表現を使っていましたがページという表現をしました。
ページも「=コンポーネント」ですが、ここでは 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は、プロパティやイベントを持つことができる。