BlazorでSPAするぞ!(7) - DI(Dependency Injection) -正式版対応済
※最終更新日: 2020/5/24 正式リリース版に対応修正しました。
という事で、↓↓↓の続きです。
今回はDI(Dependency Injection)について。
まあ、今どきのフレームワークなので(そして.NET Coreの流れをくむBlazorなので)DIは標準でサポートしています。
DIって当たり前に使う時代になりすぎてて、改めてwikiを見てみました。
Dependency injection - Wikipedia
Inversion of control - Wikipedia
DIという言葉を最初に提唱したのマーティン・ファウラー氏だったかぁ。。
IoCの起源は1988年かぁ。。。
とか、おっさんエンジニアは歴史と共にDI/IoCに思いをめぐらせますね。。。
ということで本題。
1. DI(Dependency Injection)
DI(Dependency Injection)は「依存性の注入」ですね。
「依存性の注入って言われても意味わからん」が、DIがメジャーになった2000年初期頃からの定番な話ですが。。。
オブジェクトの具象クラスに対してプログラミング(操作)するのではなく、もっと抽象的なインターフェイスに対して操作を行おう。
「new オブジェクト() 」とかしないで、インターフェイスを実装したオブジェクトの生成をDIフレームワークにやってもらおう。
プログラムからは具象オブジェクトは意識しない。
的なものですね。
DIによるメリットの代表は、以下のようなものでしょうか。
- オブジェクト間の依存性・結合度が低くなる
- テストしやすい(モックに差し替えやすい)
- (上記にも重なるが)振る舞いの差し替えが容易
ここではDI自体の説明には注力しないので、このへんで。
という事で Blazor におけるDIの利用方法の説明に移ります。
まず、サンプルプロジェクトを作成しておきます。
dotnet new blazorwasm -o di_blazor
以下のような実装を行うこととします。
消費税計算を行うサンプルです。
税抜価格をテキストボックスに入力し、「税込価格計算」ボタンをクリックすると、税込価格が計算され画面に表示されます。
消費税計算を行う処理をDIでインジェクトするクラスとします。
2. サービスを作成
BlazorのDIはシンプルで全体の流れとしては以下のようになります。
まずはDIでインジェクトしてもらう対象のサービスクラスを用意します。
消費税計算サービスクラスを作成します。そしてこのオブジェクトを、インジェクトの対象とします。
税抜価格をインプットとして、税込価格をアウトプットしてくれるクラスを作成します。また、2019/10/1以降であれば税率10%を適用します。
2.1. インターフェイス実装
以下はIConsumptionTaxCalculatorインターフェイス実装。
# Services/IConsumptionTaxCalculator.cs # namespace di_blazor.Services { public interface IConsumptionTaxCalculator { int CalcTotalPrice(int price); } }
2.2. 具象クラス実装
以下がConsumptionTaxCalculator具象クラス実装。
# Services/ConsumptionTaxCalculator.cs # using System; namespace di_blazor.Services { public class ConsumptionTaxCalculator : IConsumptionTaxCalculator { public int CalcTotalPrice(int price) { double rate = 0; if( DateTime.Today >= new DateTime(2019, 10, 1) ) rate = 1.1; else rate = 1.08; return (int)(price * rate); } } }
3. サービスを登録
「IConsumptionTaxCalculator / ConsumptionTaxCalculator」をBlazorのDIに登録します。
ProgramクラスのMain()メソッド でオブジェクトを登録します。
以下のように「services.AddSingleton<T, O>()」で、インターフェイスとオブジェクトの組み合わせでDI対象のオブジェクトを登録します。
# Program.cs # ...省略... using di_blazor.Services; namespace di_blazor { public class Program { public static async Task Main(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.Services.AddSingleton<IConsumptionTaxCalculator, ConsumptionTaxCalculator>(); ...省略 } } }
3.1. DIオブジェクトのライフサイクル
上記サンプルでは「AddSingleton()」メソッドを使用しましたが、他に「AddTransient()」メソッドがあります。
名称から大体想像が付きますが、インジェクトされるオブジェクトの生存期間の違いがあります。
それぞれのライフサイクルは以下の通りです。
※ ASP.NET CoreのDI存在する「Scoped」は Blazor WebAssembly では未対応です。
サービスクラスの持つフィールド・プロパティの状態管理の性格に応じて Singleton / Transient を使い分けます。
4. コンポーネントにサービスをインジェクト
4.1. 【方法1】.razorにインジェクトする
Pages/Index.razorを改造して、ここに IConsumptionTaxCalculator をインジェクトします。
# Pages/Index.razor # @page "/" @using di_blazor.Services @inject IConsumptionTaxCalculator calculator 税抜価格:<input type="text" bind="@Price" /> <br /> <input type="button" @onclick="Calc" value="税込価格計算" /> <br />↓↓↓↓<br /> <div>税込価格:@TaxedPrice 円</div> @code { private int Price { get; set; } private int TaxedPrice { get; set; } public void Calc() { this.TaxedPrice = calculator.CalcTotalPrice(this.Price); } }
【@inject】
「@inject」ディレクティブを利用してオブジェクトをインジェクトします。
以下の書式になります。
@inject 【インジェクトするオブジェクトの型】 【インジェクトする変数名】
@using により IConsumptionTaxCalculator を実装した名前空間「di_blazor.Services」を参照している事にも注意してください。
【コードブロック】
@code コードブロックでは、calculator としてインジェクトされたオブジェクトを利用することができます。
ちなみに、このコードをコンパイルしたILを、逆コンパイルすると以下のようになります。
# Indexクラス # ...省略... [Route("/")] public class Index : ComponentBase { ...省略... [Inject] private IConsumptionTaxCalculator calculator { get; set; } ...省略... public void Calc() { TaxedPrice = calculator.CalcTotalPrice(Price); } }
Index.razorから生成された Indexクラス のプロパティとして「IConsumptionTaxCalculator calculator」が定義され、[Inject]属性が付与されています。
DIされるのでIndexクラス内自体にはインスタンス生成ロジックは存在しません。
4.2. 【方法2】.razorのpartialコードビハインドクラスにインジェクトする
.razorの@codeコードブロックではなく、partialとして定義したコードビハインドクラスを利用した場合の実装は以下の通りです。
@Code{ }ブロックの実装を単純にIndexクラス(Index.razor.cs)に移行するだけです。
Index.razorで定義した @inject による「calculatorプロパティ」は、partialである.cs上のIndexクラスで参照可能です。
# Index.razor # @page "/" @using di_blazor.Services @inject IConsumptionTaxCalculator calculator 税抜価格:<input type="text" @bind="Price" /> <br /> <input type="button" @onclick="Calc" value="税込価格計算" /> <br />↓↓↓↓<br /> <div>税込価格:@TaxedPrice 円</div>
# Index.razor.cs # using System; namespace di_blazor.Pages { public partial class Index { private int Price { get; set; } private int TaxedPrice { get; set; } public void Calc() { this.TaxedPrice = calculator.CalcTotalPrice(this.Price); } } }
4.3. 【方法3】.razorのComponentBase派生コードビハインドクラスにインジェクトする
次に、ComponentBase派生クラスとして実装したコンポーネントクラスにインジェクトする方法の説明になります。
ComponentBaseクラスを継承した IndexBase クラスを定義します。
Index.razorは IndexBaseクラス を継承(@inherits)する方式での実装になります。
Index.razorの定義は以下の通り。@injectはなくなり、@inherits定義を追加しています。
# Pages/Index.razor # @page "/" @inherits IndexBase 税抜価格:<input type="text" @bind="Price" /> <br /> <input type="button" @onclick="Calc" value="税込価格計算" /> <br />↓↓↓↓<br /> <div>税込価格:@TaxedPrice 円</div>
次にIndexBase.cs定義です。
IndexBaseクラスのプロパティ定義として明示的に [Inject]属性 を付与した「calculatorプロパティ」を定義します。
プロパティに[Inject]属性を付けてあげると、いわゆる プロパティ インジェクション によりオブジェクトが割り当てられます。
# Pages/IndexBase.cs(コードビハインドクラス) # using System; using Microsoft.AspNetCore.Components; using di_blazor.Services; namespace di_blazor.Pages { public class IndexBase : ComponentBase { [Inject] private IConsumptionTaxCalculator calculator { get; set; } protected int Price { get; set; } protected int TaxedPrice { get; set; } public void Calc() { this.TaxedPrice = calculator.CalcTotalPrice(this.Price); } } }
5. サービスクラスへのインジェクション
では、話を少し別のケースに進めます。
上記までで、.razor(もしくはコードビハインドクラス)へのDIは、@injectディレクティブで定義することができ、裏側ではプロパティ インジェクションが動作することが分かりました。
本投稿シリーズレベルのサンプル実装では、.razorのみ もしくは (ほぼエンティティな)モデルクラス程度しか利用していませんでした。ただし、もっと大きめのアプリケーションをBlazorで作成する場合、適切な責務を持ったモデルクラスやサービスクラスのようなクラス設計・実装が必要になります。
そのようなクラス(つまり .razor 以外)でDIを利用する方法について見てみましょう。
結論としては、これは ASP.NET CoreのDIと同じです。
サンプルでは、IConsumptionTaxCalculator / ConsumptionTaxCalculatorクラスがDateTimeに依存しています。
つまりシステム日時に依存した動作しかできない。
という事で、IDateTimeProvider / DateTimeProviderクラスを用意し、ConsumptionTaxCalculatorにはコレをインジェクトします。
5.1. IDateTimeProvider / DateTimeProviderクラスを作成
Servicesフォルダ配下に実装します。
# Services/IDateTimeProvider.cs # using System; namespace di_blazor.Services { public interface IDateTimeProvider { DateTime Today(); } }
# Services/DateTimeProvider.cs # using System; namespace di_blazor.Services { public class DateTimeProvider : IDateTimeProvider { public DateTime Today() { return DateTime.Today; } } }
上記に加え、IDateTimeProviderの実装を増やすことで、DateTimeに依存しない Mockクラス を用意する事ができます(本投稿ではそこまでの実装は行わずに進めます)。
5.2. IDateTimeProvider / DateTimeProviderを登録
Mainクラスに、IDateTimeProvider / DateTimeProviderを登録します。
これは先程のIConsumptionTaxCalculator / ConsumptionTaxCalculatorと同様です。
# Program.cs # ...省略... using di_blazor.Services; namespace di_blazor { public class Program { public static async Task Main(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.Services.AddSingleton<IDateTimeProvider, DateTimeProvider>(); builder.Services.AddSingleton<IConsumptionTaxCalculator, ConsumptionTaxCalculator>(); ...省略 } } }
5.3. ConsumptionTaxCalculatorに、IDateTimeProviderをインジェクト
ConsumptionTaxCalculatorクラスにIDateTimeProviderオブジェクトをインジェクトします。
.razorではなく、サービスクラス(コンポーネントクラス以外)にインジェクトする際は「コンストラクタ インジェクション」の形式をとります。
以下が実装になります。
# Services/ConsumptionTaxCalculator.cs # using System; namespace di_blazor.Services { public class ConsumptionTaxCalculator : IConsumptionTaxCalculator { private IDateTimeProvider _DateTimeProvider = null; public ConsumptionTaxCalculator(IDateTimeProvider dateTimeProvider) { this._DateTimeProvider = dateTimeProvider; } public int CalcTotalPrice(int price) { double rate = 0; if( this._DateTimeProvider.Today() >= new DateTime(2019, 10, 1) ) rate = 1.1; else rate = 1.08; return (int)(price * rate); } } }
【コンストラクタ引数】
「public ConsumptionTaxCalculator(IDateTimeProvider dateTimeProvider)」という形で、サービスクラスのコンストラクタの引数でDI対象の「IDateTimeProvider」オブジェクトを引数として受け取っています。
AddSingleton()やAddTransient()で登録済みのインターフェイスオブジェクトは、BlazorのDIフレームワークによってインスタンス生成時に、コンストラクタ引数としてインスタンスが渡される仕組みになっています。
ここでは、別のメソッド(CalcTotalPrice(int price))が呼び出された際に利用するため、自らのフィールド変数にオブジェクトを代入して利用しています。
6. デフォルトサービスとカスタムサービス
BalzorのDIの使い方は、すでに説明した方法になります。
インジェクトするオブジェクトには、「デフォルトサービス」と「カスタムサービス」という2つの種類があります。
すでに説明した ConsumptionTaxCalculator や DateTimeProvider をインジェクトするのは カスタムサービス という分類になります。
これとは別に「デフォルトサービス」というものがあります。
名前の通りデフォルトで用意されたサービスになり、これらをインジェクトすることができます。
以下の3つがデフォルトサービスとして用意されています。
以下の3つがデフォルトサービスとして用意されており、これらは明示的な登録(AddSingleton() / AddTransient())を行うことなく .razorコンポーネントやサービスクラスにインジェクトすることができます。
- HttpClient
Singletonで提供されます。
Http通信を行うオブジェクトです。BlazorアプリからサーバのWebAPIを呼び出し時などに利用します。 - IJSRuntime
Singletonで提供されます。JavaScriptとの相互運用に利用します(Blazor C#からのJavaScript呼び出し等)。 - IUriHelper
Singletonで提供されます。URI/Nabigation状態管理ヘルパー。
「HttpClient」に関しては dotnet new blazorwasm コマンドで作成したデフォルトプロジェクトの実装において、以下のようにInjectが初期実装されています。
// Program.csのMain()内に以下の実装がデフォルトで追加されています。 builder.Services.AddTransient(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
ここら辺を使ったサンプルは、後々 別途ブログにあげようと思います。
7. まとめ
「BlazorでSPAするぞ!」という事で、いくつかの投稿で Blazorの基本 を歩いてきました。
まだPreview版という事で今後、仕様的な変更、改善が行われるのだと思いますが、リリースに備えて自分自身で基本を理解・整理することができたと思います。
「BlazorでSPAするぞ!」というシリーズ投稿としては、これで完結し、今後の投稿ではTips的にBlazor関連情報を書いていこうかと思います。
では、Let's enjoy Blazor together!!!
BlazorでSPAするぞ!(6) - Routing -正式版対応済み
※最終更新日: 2020/5/24 正式リリース版に対応修正しました。
という事で、↓↓↓の続きです。
今回はルーティングについて。
URLに対するページ(コンポーネント)のルーティング(マッチング)ですね。
1. ルーティング(Routing)
サンプルプロジェクトからルーティングを学んでいきたいと思います。
まず、dotnet new blazorwasm コマンドでプロジェクトを作成します。
dotnet new blazorwasm -o route_blazor
自動生成コードには、ルーティング実装も ある程度組み込まれています。
dotnet run で実行し、http://localhost:5000 にアクセスします。
これは「/」(ルート)へのアクセスになります。
「/」に対するルーティング設定は、「Pages/Index.razor」となっています。
サイドメニューから「Counter」をクリックします。
ブラウザのURLが http://localhost:5000/counter になりました。
「/counter」へのアクセス、そして「/counter」へのルーティング設定は「Pages/Counter.razor」となります。
同様にサイドメニューから「Fetch data」をクリックします。
ブラウザのURLが http://localhost:5000/fetchdata になりました。
「/fetchdata」へのアクセス、そして「/fetchdata」へのルーティング設定は「Pages/FetchData.razor」となります。
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状態です。
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)が表示されました。
RouterのFallbackComponent属性が未設定の場合、無効なURLがアクセスされると、以下のように「Loading...」画面で止まってしまいます。
Chromeの「DevTools - Console」を確認すると、WASMのエラーが多数出力された状態で止まっていることが確認できます。
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>タグとしてレンダリングされていることが確認できます。
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パラメータを受け取り、画面に表示することが出来ました。
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」にアクセスした実行画面は以下です。
5. まとめ
ルーティングも従来のASP.NET Core等の仕組みと基本思想は同じくしているので、スムーズに理解することができたました。
次の投稿では、DI(Dependency Injection)を見ていこうと思います。
BlazorでSPAするぞ!(5) - レイアウト -正式版対応済
※最終更新日: 2020/5/24 正式リリース版に対応修正しました。
という事で、↓↓↓の続きです。
今回はレイアウト機能について。
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 がレンダリングされます。
3. まとめ
レイアウト機能は比較的シンプルでASP.NET Coreのレイアウトなどを知っていれば、スムーズに理解できそうです。
自動生成されるプロジェクトが、昔に比べて複雑化していますが、これを丁寧にトレースすることで、レイアウト機能の基本は理解できると思います。
BlazorでSPAするぞ!(4) - データバインド(おかわり) - 正式版対応済
※最終更新日: 2020/5/24 正式リリース版に対応修正しました。
という事で、↓↓↓の続きです。
1. データバインド
Component のデータバインディングについては BlazorでSPAするぞ!(2) - Component -正式版対応済 - ryuichi111stdの技術日記 で軽く説明し、value="@xxx"によるone-wayバインディング / @bind="xxx"によるtwo-wayバインディングについても使用しました。
この部分についてもう少し おかわり(深堀) したいと思います。
1.1. サンプル
以下のような画面のサンプルアプリをもとに説明を進めます。
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した状態。
Index.razorの「Score: @YourScore」の値も、Blazor Runtimeによりバインド変数の変化の影響を自動判定して変更が反映されます。
また、ChildComponentへの「YourScore="@YourScore"」の値も、同様に変更が反映します。
以下、引き続きこのサンプルをベースに説明を進めます。
2. value="@YourScore" によるデータバインド
上述サンプルにおいて、ChildComponentのテキストボックスの値を直接変更してみます。
「1012」を入力し、フォーカスを移動します。
テキストボックス以外の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」にも変更が反映しました。
ただし、親コンポーネント側(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値も変更されました。
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で廃止されました。
BlazorでSPAするぞ!(3) - パラメータ - 正式版対応済
※最終更新日: 2020/5/24 正式リリース版に対応修正しました。
という事で、↓↓↓の続きです。
前回 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" } ; } }
実行画面は以下の通りです。
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」属性をプロパティに付与することでパラメータを引き継ぐことができます。
実行画面は以下の通りです。
3. まとめ
ということでComponentのパラメータでした。
(ぜんぜん、まとめじゃない・・・)
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は、プロパティやイベントを持つことができる。
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#コードがクライアントサイドで動作するというキーワードは、過去にもいくつかありましたね。。。
- Silverlight(縮小版的な.NETプラグインをインストール)
- WindowsフォームをIEでホスト(フルセットの.NET Frameworkインストール+Win限定)
2020年5月現在においては、オープンな技術開発的には使えない技術達ですね(一部のtoBでは生き残ってると思いますが・・・)。
で、Blazorがクライアントサイド(ブラウザ)でC#コードの動作を実現している仕組みですが、「WebAssembly」になります。
WebAssemblyは C/C++・Rust・Go言語 なんかで作れるようになっていますが、BlazorによってC#でもWebAssemblyが作れるようになりました。
厳密には、Blazorの.NET Coreアセンブリを.NET WASMでWebAssemblyにビルドして実行するような形になります。
図にすると以下のような感じ。
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は実行されません。
実行イメージは以下のような感じ。
ボタンのクリックなど「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
作成されるファイル達↓↓↓
(2) Blazor WebAssembly App
Blazor WebAssemblyのプロジェクトテンプレートです。
dotnet new blazorwasm -o BlazorWasmApp1
作成されるファイル達↓↓↓
(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
作成されるファイル達↓↓↓
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 フォルダを開きます。
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 にアクセスすると、テンプレートで出力したサンプル実装が起動します。
Hello World以上の立派な実装がなされています。
ちなみにこのページを表示した時のHttpRequestの状況ですが、以下のようになっています。
よく見ると「blazor.webassembly.js」とか「dotnet.wasm」とか「hello_blazor.dll」とか「Microsoft.AspNetCore.Components.WebAssembly.dll」とか・・・.NETが動こうとしている事が見て取れると思います。
それから、左のサイドメニューから「+Counter」を選択すると、ボタンクリックでカウントアップが行われる画面に移動します。
「Click me」ボタンをクリックすると、Current Countが1ずつカウントアップしますが、この時サーバへのリクエストが飛んでいないことも確認できます。
(当然ながらクライアントサイドでC#コードが実行されているので、このようなロジック処理でサーバへのラウンドトリップは発生しない、という事です)
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) プロパティ変数の利用
以下のような画面に改造したいと思います。
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」ボタンを用意し、クリックされたらすぐ上にテキストで現在日時を表示します。
# 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 で逆コンパイルした結果が以下です。
// 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 を使ってみましょう。
// 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ではじめの一歩が踏み出せました!