本家動画はこちら
EP06~09はこちら
番外×2 + EP10~はこちら
#発端
Blazorを勉強するのになんかいい教材ないかなぁと探してたらYoutubeにチュートリアル動画(英語)が上がっていたので自動翻訳字幕見ながらまとめてみました。
https://www.youtube.com/watch?v=xr56fmgLl74&list=PL4WEkbdagHIR0RBe_P4bai64UDqZEbQap
★★★★注意★★★★
翻訳ではなく
「そんな感じのこと言ってるなぁ」
「全然違うこと言ってるけどつまりはそういうことか」
ということをまとめてます。
- ASP.NETでサーバーサイドで処理してたけど、サーバーのリソースいっぱい使うよね
- react angular vueとかでクライアントサイドのリソース使えばサーバーのリソース使わなくて済むよね
- でも認証コート書くわけにはいかないし、複数の言語使うの面倒だよね
- WebAssembly使えばサーバーサイドもクライアントサイドも全部C#で書けるよ
- VisualStudioでBlazorのプロジェクト作るとサンプルが勝手にできるよ
- 動かしてみるとどんな感じでリクエスト飛んでるかよくわかるね。
- 「Index.razor」「Counter.razor」「FetchData.razor」って奴らがナビゲーションをクリックしたときに表示されるWebページだよ。
- 各ファイルの頭に「
@page "(パス)”
」ってなってるね。例えば「Counter.razor」なら「@page "/counter"
」とか。 - 「Counter.razor」見てみるとHTMLと「
@code{}
」みたいなのが書かれてるね - 「Counter.razor」みたいに、HTMLの部分に「
@変数名
」「@onclick="関数名"
」、「@code{}
」の中に変数宣言と関数って書くと動くんだね。 - 「Counter.razor」でいうところの「
@currentCount
」「@onclick="IncrementCount"
」と「private int currentCount = 0;
」「private void IncrementCount(){currentCount++;}
」だね。 - 「FetchData.razor」では天気と気温をテーブルに表示してるね。
- 「FetchData.razor」のHTML部分では
forecasts
っていうオブジェクトからデータを取ってきてforeach
で回してるね。 -
forecasts
はコード部分に宣言されててWeatherForecastクラスの配列だね。 - WeatherForecastクラスは「WeatherForecast.cs」で宣言されてるね。
-
forecasts
はOnInitializedAsync関数をオーバーライドして初期化の時に非同期でForecastService
っていうオブジェクトから引数に現在の時間を指定して値を設定してるね。 -
ForecastService
はWeatherForecastService.csで定義されてるね。
(Task.FromResult
ってなんやろう?)
タスク ベースの非同期プログラミング
https://docs.microsoft.com/ja-jp/dotnet/standard/parallel-programming/task-based-asynchronous-programming
C# 並行・並列プログラミング パターン集
https://qiita.com/takutoy/items/c384fcb439d345a9a0d3
→そうか、このメソッドが非同期処理で、戻り値がTask<WeatherForecast[]>
やからこれを使うんやな。
- (「
Enumerable.Range(1, 5).Select
」…?)
[C#] 連番の作成 (Enumerable.Rangeの利用)
https://qiita.com/koara-local/items/534376eabd11c64d31af
→連番作成のメソッドとLINQの組み合わせかぁ
(アロー演算子もよう知らんねんなぁ…)
デリゲート - C# によるプログラミング入門 | ++C++; // 未確認飛行 C
https://ufcpp.net/study/csharp/sp_delegate.html
→関数ポインタの考えを発展させたみたいな感じやな。
古典的に書き換えるとこんな感じか。
LINQもアロー演算子も確かにコード量は減るけど、初学者には厳しいなぁ…
var rng = new Random();
var ret = new WeatherForecast[5];
for (int i=0;i<5;i++)
{
var tmp = new WeatherForecast();
tmp.Date = startDate.AddDays(i);
tmp.TemperatureC = rng.Next(-20, 55);
tmp.Summary = Summaries[rng.Next(Summaries.Length)];
ret[i] = tmp;
}
return Task.FromResult(ret);
-
ForecastService
が使えるのは「FetchData.razor」の上で「@inject WeatherForecastService ForecastService
」って書いてるからだよ。 - じゃ自分でページを追加してみようか
(ちょっとまて、ForecastServiceの実体はどこやねん)
→EP04で解説されてた。「Startup.cs」のConfigureServicesメソッド内で
「services.AddSingleton<WeatherForecastService>();
」
って感じでシングルトンつくって@inject
で使ってるんか。
- 「Authors.razor」っていう名前でRazorコンポーネント追加するよ
- 「Authors.razor」の頭に「
@page "/authors"
」って書くよ。「Index.razor」「Counter.razor」「FetchData.razor」と一緒だね。 - 「MainLayout.razor」と「NavMenu.razor」を見てみよう。
- 「NavMenu.razor」には「
@page
」が無いね。他のRazorコンポーネントから部品として呼ばれるようにするにはこんな風に書くよ。 - 「MainLayout.razor」には「
<NavMenu />
」って書いてあるね。こうやって「NavMenu.razor」を組み込んでるんだよ。 - 「MainLayout.razor」の「
@Body
」は「NavMenu.razor」の操作で変わるよ。
(詳しく説明はないけど「MainLayout.razor」に「@inherits LayoutComponentBase
」ってあるからLayoutComponentBaseって奴から継承した機能なんかなぁ) - 「NavMenu.razor」のメニューコピペ編集したらAuthorsってメニューが追加されてクリックしたら追加した「Authors.razor」が表示されたね。
(とりあえずEP10まで見たけど、まとめると結構時間かかる)
#EP03:ルーティング(複数のルーティング/パラメーター/ナビゲーションマネージャー))
- 「
@page
」を複数設定して別のURLでも同じページに遷移したり、「@page
」でパラメーターを指定したりできるよ。 - その前にBrazorがどこから開始してるか見てみよう
- 「Program.cs」の中にMain関数があるね。ここからStartupへ飛ぶよ。
(うあぁ、むっちゃラムダ式やん。詳しく見て言ってたらこれだけで1~2週間かかりそう)
(とりあえず定義だけ追いかけとこうか)
static IHostBuilder CreateDefaultBuilder(string[] args);
static IHostBuilder ConfigureWebHostDefaults(this IHostBuilder builder, Action<IWebHostBuilder> configure);
(thisってなんやろう?C++のthisポインタとは違うと思うけど)
→C# 3.0の拡張メソッドっていうんか。
https://ufcpp.net/study/csharp/oo_static.html
static class Extensions
{
public static int Parse(this string str)
{
return int.Parse(str);
}
}
って書くと
int x = "1".Parse();
って書ける。便利やん。
- 「Startup.cs」では一番下の方にある
endpoints.MapBlazorHub();
でリクエストの受付準備してendpoints.MapFallbackToPage("/_Host");
で「_Host.cshtml」にルーティングしてるね。 - この「_Host.cshtml」でCSSを読み込んでるよ。
- それからJavaScriptを読み込んでBlazorが動き出すよ。
- 最初は「App.razor」が呼ばれるよ。
- ここにはルーターが書かれていて、ここでブラウザから渡されたURLが有効なものかを判定しているよ。
- 有効なURLなら
<Found>
の中身が、無効なURLなら<NotFound>
の中身が返されるよ -
@routeData
でパラメーターを渡してるのも確認できるね。 - それじゃ試しに
<NotFound>
の中に<h1>404 Error</h1>
って書いてデバッグ実行してみよう。 - 試しに無効なURLを指定してみるとちゃんと404 Errorって表示されてるね。
- それではEP02で作った「Authors.razor」を改造してルーティングについてもう少し詳しく見よう。
- まずは「AuthorsDetail.razor」を追加して同ファイルの頭に
@page "/authors/authordata"
を追加しましょう。あと著者の詳細を書こう。 - 「Authors.razor」には著者の名前書いて
/authors/authordata
のリンクを張ろう。 - 動かしてみよう。「Authors.razor」の著者名をクリックしたら「AuthorsDetail.razor」へ飛ぶね。
- 別のルーティングを追加してみよう。「AuthorsDetail.razor」の
@page "/authors/authordata"
の下に@page "/author"
を追加してみよう。 - 動かして直接
/author
に飛んでみるとちゃんと「AuthorsDetail.razor」が表示されるね。 - ここで同じルーティングがあるとどうなるか見てみよう。
- さっき書いた「AuthorsDetail.razor」の
@page "/author"
を@page "/authors"
に変えて「Authors.razor」と同じにしてみよう。実行すると例外が出るのが確認できるね。 - 「AuthorsDetail.razor」の
@page "/authors"
は@page "/author"
に直しておいて、次はパラメーターについてみてみよう。 - 「AuthorsDetail.razor」の
@page "/authors/authordata"
を@page "/authors/authordata/{authorId}"
に変えよう。 - 受け取ったパラメーターを使うために
@code
に以下を追加しよう。
[Parameter]
public string AuthorId { get; set; };
- HTML部の著者の名前の前に
@AuthorId
をつけよう。
(パラメータ名は大文字/小文字の区別ないみたいやね。URLやし当然か。けど混乱しそうやから同じにするようにしたほうが良さそうやね) - とりあえずこの状態で実行して「Authors.razor」から「AuthorsDetail.razor」に飛んでみるとエラーになるよね。
- つまり
@page "/authors/authordata/{authorId}"
はパラメータが必須になるということだね。 - パラメータなしも許容したい場合は
@page "/authors/authordata/{authorId}"
だけでなく@page "/authors/authordata"
も書いてあげればいいよ。 - こうやって書くとパラメータなしでも動いて、パラメータを手で入れてやるとちゃんと表示されるのがわかるね。
-
public string AuthorId
をpublic int AuthorId
って宣言して@page "/authors/authordata/{authorId:int}"
って書いてやるとパラメーターの型指定ができるよ。詳しくはASP.NET Core Blazor のルーティングを見てね。
(bool,datetime,decimal,double,float,guid,int,long の8つが使えるみたいやね。)
- では次に「ボタンを押してページ遷移」を実装してみよう。
- とりあえず「Counter.razor」から
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
とprivate void IncrementCount()
を「Authors.razor」へコピーして関数名をNavigateに変えよう。 - 「Authors.razor」の頭に
@inject NavigationManager NavigationManager
を追加しよう。 - Navigate関数の処理に
NavigationManager.NavigateTo("/authors/authordata/22");
を追加しよう。 - あとリンクの方も
"/authors/authordata/11"
に変えておこう。 - 動かしてみよう。思った通りに動くね。
- これ以外にもいろいろやり方があるよ。
- 「App.razor」がルーティングの起点になってるって覚えておいてね。
#EP04:依存性注入
- 今回は「依存性注入(Dependency injection)」について話すよ!難しくないよ!
(依存性注入の概念はwikipediaとか見たほうが良さそう)
依存性の注入 - wikipedia)
(「依存性の注入」って言葉だといまいちピンとこないけど、依存してるクラスのオブジェクトを後から注入する、ってことか)
-
説明用にサンプル作ったよ。このサンプルでは
OnInitialized()
でAuthorService
クラスのオブジェクトを生成してるね。 -
AuthorService
クラスのコンストラクタには現在時間で初期化する変数を持たせて、Webページに表示するようにしてみた。 -
じゃ挙動を見てみよう。ページ遷移のたびに時間が更新されてるのがわかるね。
-
つまり別のコンポーネントに移動するたびにコンストラクタが呼ばれてる(=オブジェクトが生成されてる)ということだね。
-
毎回初期させたくない場合どうするかというと、依存性の注入を使うよ。
-
やり方は簡単で「Startup.cs」の
Configure
メソッドにservices.AddTransient<クラス名>();
またはservices.AddSingleton<クラス名>();
またはservices.AddScoped<クラス名>();
と書いて利用するrazorコンポーネントの頭に@inject クラス名 オブジェクト名
って書くだけ。
(動画ではservices.AddTransient<IAuthorService,AuthorService>();
みたいにインターフェースを分けて「後でバージョンアップしたのと入れ替えれ得るよ」とか言ってるけど、ビルドしなおすようじゃメリット無いよなぁ…)
(そもそも最近じゃ「バージョンアップはコンテナごと入れ替え」みたいなことが多いしなぁ…)
(まぁメリットが見えてきたらインターフェースを作るパターンを使ってみよう) -
AddTransient
とAddSingleton
とAddScoped
の違いを確認してみよう。 -
AddTransient
だとページ遷移の度に時間が更新されてるね。 -
挙動としては
OnInitialized()
でAuthorService
クラスのオブジェクトを生成してた時と同じだね。 -
AddSingleton
だとページを遷移してもリロードしても時間が変わらないね。 -
つまりアプリケーションで一回だけ生成されるようになるよ。
-
AddScoped
はAddSingleton
とほぼ同じだけど、AddScoped
はリロードしたら時間が変わるよ。 -
つまりリロードの際にオブジェクトが再生成されるよ
(インターフェースそのままで実装クラス変えてバージョンアップの話は読み飛ばしていいか)
#EP05:フォームと検証
- 今までのアプリにEmailと給与の項目を追加したよ。
- このEmailと給与を入力するときに検証を追加する方法を説明するよ。
- まずは入力フォームを作るよ。
<EditForm>
っていうrazorコンポーネントを追加するよ。 -
<EditForm>
の中に<input type="text"/>
を追加してビルドしてみるよ。 - すると例外が発生するね。これはモデルパラメーターがないからだよ。
- それじゃ「Authors.razor」の
@code
にpublic Author author { get; set; }
を追加して<EditForm Model=@author>
としてみよう。 -
author
はOnInitialized()
内で初期化するのを忘れずにね。 - 今度はビルドが通って入力フォームが表示されましたね。
- では続いてすべての項目の入力を書いてみよう。あらかじめ書いたものがあるのでそれをコピーするね。
- コピーされたコードを見てみると
<EditForm>
razorコンポーネントの中に<InputText>
っていうrazorコンポーネントがあるね。 -
<InputText>
の他にも(ASP.NETコア MVC および Razor ページでのモデル検証)[https://docs.microsoft.com/en-us/aspnet/core/mvc/models/validation?view=aspnetcore-5.0]に色々載ってるよ。
(関係ないけどMicrosoftDocの日本語ページ見るより、英語ページをEdgeかChromeでブラウザの翻訳機能で訳したほうがきれいな翻訳になるな。コードまで翻訳されるけど)
- 保存ボタンを押したときの動作を記述するには
<EditForm>
にOnValidSubmit
を追加してメソッド名を設定するよ。 - メソッドは
SaveAuthor
にしよう。メソッド内ではauthorService.SaveAuthor(author);
を呼んで入力で生成したオブジェクトをauthorService
に保持させて、autor = new Author()
で入力オブジェクトを再生成するよ。 - それじゃこれがどう動くか見てみよう。
- 情報を入力して「Save」を押すとリストに追加されるね。
- では次に入力チェックを実装してみましょう。
- 必須フィールドであることを指定するには入力に使用しているクラス(今回なら
Author
)に[Required]
を指定するよ。ここではFirstName
につけてみよう。 - あと
<EditForm>
の中に<DataAnnotationsValidator>
を追加する必要があるよ。
(この動画じゃ「Clear」ボタンも<EditForm>
に<input type="submit">
で書いてるけど、シンプルに実装するなら「Clear」は)<button>
で実装して決定時のメソッドと分けたほうが良さそう
→あかんかった。結局<button>
も<input type="submit">
になるっぽくてOnValidSubmit
で指定したメソッドが走る。
→<input type="reset">
とするのが正解か。
<input type="reset" name="btnClear" class="form-control col-1 btn btn-primary" @onclick="Clear" value="Clear" />
(あとGitHubでこの人が公開してるプロジェクト見て知ったんだけど、razorコンポーネントのコメントって@* ~~ *@
なのな。(丁度<DataAnnotationsValidator>
がコメントアウトされてた))
- それじゃまた動かしてみよう。
- 何も入力しないで「Save」をクリックすると「first name」のTextBoxが赤枠になって下のリストにも追加されてない、つまりエラーになってることがわかるね。
- じゃ次にエラーメッセージを使いするにはどうしたらいいかを見てみよう。
- これは簡単で、さっき追加した
[Required]
を[Required(ErrorMessage = "First nameは必須項目です。")]
って変えて、<EditForm>
の中に<ValidationSummary />
を追加すればいいだけだよ。 - それじゃ実行してみよう
- 何も入力せずに「Save」を押すとエラーになって上にエラーメッセージが表示されたね。
- それではほかにも追加してみよう
- 「Cityを20文字以下に制限する」という制約を書くには
[StringLength(20,ErrorMessage = "Cityは20文字以下である必要があります。")]
のように書くよ。 - データの種類を指定する場合は以下のように書くよ。
[DataType(DataType.EmailAddress)]
[EmailAddress]
public string EmailAddress { get; set; }
- 値の範囲を指定するには
[Range(1000,9999999,ErrorMessage = "Salaryは$1000以上である必要があります。")]
のように書くよ。 - また、'[Range]'を利用する場合、razorコンポーネントは
<InputText>
ではなく<InputNumber>
を使うよ。 - 他にも以下のような検証があるよ。
[EmailAddress]
: 電子メール形式であること
[Phone]
: 電話番号であること
[RegularExpression]
: 指定した正規表現に一致すること
[Url]
: URL 形式であること
- 今度はフォームの頭ではなくて各入力の横にエラーメッセージを出してみよう。First nameであれば
<ValidationMessage For="@(()=>author.FirstName)"/>
とコンポーネントを追加するだけだよ。簡単だよ。 -
<ValidationMessage>
の前に空白を入れて見栄えを良くしよう。 - First name以外にも検証メッセージを追加して動きを見てみよう。
- あとフォームの頭の
<ValidationSummary/>
は消しておこう。 - さっきと同じように入力の検証が動いて、テキストボックスの横にエラーが表示されているのがわかるね。
(長くなったんで5刻みで分けようと思います)