「ASP.NET Core Blazor と Chart.js で入門するWebアプリ作成」の2回目。各回記事の内容は以下のようになります。
導入編:サンプルを動かす
探究編:Blazorの仕組みを理解する(当記事)
実践編:Chart.jsでグラフを描く
Tips編:BlazorやChart.jsのあれこれ
番外編:Ubuntuサーバで公開する
今回の内容は、第1回「導入編」の「作業の流れ」で提示した項目のうち「3. Blazor フレームワークの主な構成要素を知る」です。
Blazor フレームワークの主な構成要素を知る
本稿の中では、おそらく今回記事が一番ハードとなります。読んでいるうちに「あ、挫折しそう」と思ったら、迷わず次回記事に移りましょう。いろいろとプログラムをいじりながら、何か疑問が出たらこの記事に戻ってきてください。
この節の内容をより深く理解したい方は、Razor Pages についても知っておくとよいかと思います。この目的には以下のサイトが役立ちました。というか、泥縄ですが筆者は本節を書くのにこちらのサイトで勉強し直しました。
- 進化した「ASP.NET Core 2.0」新しいWeb開発手法を学ぶ (ASP.NET Core 2.0 の時代の記事。はじめの2本を読めば歴史と基本がわかる)
- Learn Razor Pages (ASP.NET Core 3.1 に対応。Google翻訳を駆使しよう)
Webアプリ初心者(本稿執筆前の筆者)は、たぶん、いきなり上記のサイトを読んでも興味が続かないかもしれません。とりあえずは本稿を読み進め、何かを作る経験をした上で、疑問点だらけの頭にしてから読むと得るものも多いと思います。
Blazor プロジェクトの全体構成
前回作成したサンプルプロジェクトで、VS のソリューションエクスプローラは下図のようになっています。
VS Code の Explorer の表示と比べると、共通するのは以下のものとなります。
- wwwroot/
- Data/
- Pages/
- Shared/
- _Imports.razor
- App.razor
- Program.cs
- Startup.cs
なので、これらが Blazor の主要構成要素であろうと想像できます。これらの要素の役割を見ていきましょう。
wwwroot
静的なコンテンツ、つまり、いつ GET されても同じ内容を返すファイルを置きます。実際、上記画像では、css と favicon が置かれています。この後、 Chart.js などの JavaScript もここに配置することになります。
wwwroot がURLのルートに対応するので、たとえばブラウザから /css/site.css を GET すると wwwroot/css/site.css の内容が表示されます。
Data
これは Blazor にとって必須というわけではありません。慣習として、データ的なものはここに置くことになっているようです。上記画像では、「Fetch data」で表示される気温データを生成するプログラムが格納されています。
Pages
ブラウザに表示されるページを構成するファイルを置きます。Blazor では Pages というフォルダを特別扱いしていて、ここが動的なページツリーのルート("/")に相当するようになっています1。
Shared
ページの「部品」となるコンポーネントなど、共用されるファイルを置きます。
.cshtml
.cshtml という拡張子の付いているファイルは、ASP.NET で以前からサポートされているもので、 HTML の中に C# のコードを混ぜることで動的にページを作るのに使われます。表示するページを構築(レンダリング)するときにそのコードが実行されるような仕組みになっています。C# のコードを混ぜるには、"@" をプレフィックスとする「Razor 構文」と呼ばれる記法に従います。詳細は「Razor ASP.NET Core の構文リファレンス」を参照していただきたいのですが、おおよそ以下のように理解していれば大丈夫かと思います。
-
@変数名
で変数の内容をそこに展開する -
@メソッド名
でメソッドを実行する -
@for
や@if
などの制御構文も使える -
@{ ... }
で完全なC#コードブロックを記述できる。上記の変数やメソッドをこの中で定義できる
.cshtml ファイルの中でも、先頭に @page
と書いてあるものは ASP.NET Core 2.0 以降で提供されている Razor Pages というフレームワークによってサポートされるファイルとなり、ページを表現することができます。MVC パターンでいうところの V(ビュー)とC(コントローラ)を一つのファイルにまとめて記述できるようになったもの、ということらしいです2。
実際 Blazor は、この Razor Pages を拡張したものです。 Startup.cs の中を見ると次のようなコードがあります。
ここの service.AddRazorPages()
により Razor Pages が有効になり、services.AddServerSideBlazor()
により Blazor が有効になるようです。
@page
にはそのページのパスを書くこともできます。たとえば、
@page "/foo"
とあれば、これは /foo
というページを表わしていることになります。
@page
にパスを書かなければ、そのファイルのパスは「Pages1 をルート(Root)とし、ディレクトリ階層をたどり、ファイル名から拡張子を除いたもの」になります。たとえば Pages/Foo/Bar.cshtml ファイルのパスは /Foo/Bar
3 になります。
この、URLで指定されているパスと実際に表示されるファイルを結びつける役割を果たしているのが「ルーティグ」(routing) と呼ばれる機構4です。ルーティングという機能は、MVC パターンだとコントローラが担っているかと思うのですが、Razor Pages の場合はフレームワークのほうでよしなに計らってくれるわけですね。
以下、 Pages に含まれる .cshtml ファイルの説明。
- _Host.cshtml
- ページの大枠を記述する Razor Pages ファイル。先頭が
@page "/"
なのでルートページとなる。中を見ると分かるが、<head> タグや <body> タグが使われている。ページ内で必要になる css や JavaScript ファイルはここで読み込んでおく。詳細については後述。 - Error.cshtml
- 必須ではないが、何かエラーが発生したときに表示する内容を記述している。このページのパスは
/Error
になるが、Startup.cs のConfigure()
の中で例外ハンドラーとしてこのページを指定している。
.razor
.razor という拡張子の付いているファイルは、再利用可能なUIコンポーネントを定義するためのファイルです。そしてこの .razor ファイルこそが Blazor フレームワークの中心的役割を果たします。
この部品は「Razor コンポーネント5」と呼ばれます。もちろん、ページ自身もコンポーネントとして定義することができます。ページ自身も含め、いろいろと使い回せるような部品をいい感じに書くことができる仕組みだと考えておけば良さそうです。
.razor ファイルの記述法は .cshtml ファイルと似ています。ただし以下の違いがあります。
-
@page
命令を記述する場合はパス指定を省略することができない
@page
命令を書く場合は、以下のように必ずパスを指定します。
@page "/counter"
パス指定を省略して(@page
だけにして)ビルドすると以下のようなエラーになります。
@page
命令自体を省略することはできます。@page
命令があるとページとして機能し、無ければ部品として機能するようです(後述の SurveyPrompt.razor など)。
-
.razor ファイルには
<script>
タグが書けない
無理やり書いてみると VS に叱られます。
ちなみに次回の「実践編」では、 Chart.js を呼び出すための <script>
を _Host.cshtml に記述しています。
- コードブロックについては
@{ ・・・ }
ではなく、@code { ・・・ }
と書く
具体例については、下記、Counter.razor を参照。
サンプルプロジェクトの .razor ファイル
サンプルプロジェクトには以下の .razor ファイルがあります。
- Pages/Counter.razor
- カウンターページの Razor コンポーネントファイル。
- Pages/FetchData.razor
- 左側サイドメニューで「Fetch data」をクリックしたときに表示されるページの Razor コンポーネントファイル。ランダムに生成される気温データを表示する。
- Pages/Index.razor
- ルート("/")ページのコンテンツとなる Razor コンポーネントファイル。
- Shared/MainLayout.razor
- ページのレイアウトを定義する Razor コンポーネントファイル。
- Shared/NavMenu.razor
- 左側に表示されるナビゲーションメニューを定義する Razor コンポーネントファイル。
- Shared/SurveyPrompt.razor
- Razor コンポーネントタグとして使う例。
- _Imports.razor
- すべての Razor ファイルでインポートされる。
- App.razor
- アプリケーションの実体となるアセンブリなどの定義や、レイアウトファイルの指定を行う。
Counter.razor
まず Counter.razor を例にして Razor コンポーネントの説明をします。
先頭に@page "/counter"
とあるので、このコンポーネントは /counter
というパスに対するページとして機能することになります。
5行目に @currentCount
という記述があります。これは典型的な Razor構文の一つで、その下の @code
部分で定義されている currentCount
という変数(フィールド)の値をこの場所に展開します。初期値が currentCount = 0
になっているので、最初にレンダリングされるHTMLは次のようになります。
<p>Current count: 0</p>
最初だけではありません。Blazor では、currentCount
の値が変化すればそれに応じてブラウザに表示されている <p>Current count: @currentCount</p>
の部分が動的に上書きされます。
7行目、button
要素のところに @onclick="IncrementCount"
という記述があります。この記述により、ボタンがクリックされた時に @code
ブロックで定義されている IncrementCount()
メソッドが呼ばれるのですが、このコード部分はサーバ側で実行されます。そして currentCount
の値が変更されると、今度はブラウザ側で @currentCount
の部分が上書きされるのです。
この一連の流れは、SignalR という双方向通信技術を用いて実現しているようです。ページ遷移を起こさず、裏でブラウザとサーバ間で通信を行い、データを送受信してページ内容を書き換えているわけです。
@code
部分は、 .razor ファイルと切り離して本来の C# コードファイルに記述することもできます。ちょっとやってみましょう。Pages フォルダ配下に新しいクラスファイルとして Counter.razor.cs6 を作成してください。
内容を以下のように置き換えます。(違いが分かるようにインクリメント幅を 2 に変更しています)
namespace MyBlazorApp.Pages
{
public partial class Counter
{
private int currentCount = 0;
private void IncrementCount()
{
currentCount += 2;
}
}
}
名前空間は、{AppName}.{FolderName} です。上記例では MyBlazorApp.Pages になっています。クラス名をファイル名(コンポーネント名)に一致させます。.razor ファイルのほうでも裏で同名の partial クラスが生成されているので、ここでもクラスに partial
を付加します。Counter.razor のほうの @code
の中は削除してください。イメージは次のようになります。
F5 を押してビルド&実行します。ブラウザが開いたら Counter 画面に移動します。ここで「Click me」ボタンをクリックすると、"Current count" が 2 になるはずです。上記修正が有効に機能しているのが分かると思います。
FetchData.razor
FetchData.razor についても C# コードの分離をやってみましょう。Counter.razor の場合と同様、 FetchData.razor.cs というファイルを作成します。内容を以下で置き換えてください7。
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using MyBlazorApp.Data;
namespace MyBlazorApp.Pages
{
public partial class FetchData : ComponentBase
{
[Inject]
private WeatherForecastService ForecastService { get; set; }
private WeatherForecast[] forecasts;
protected override async Task OnInitializedAsync()
{
forecasts = await ForecastService.GetForecastAsync(DateTime.Now);
}
}
}
FetchData.razor ファイルからは、先頭の @using MyBlazorApp.Data
および @inject WeatherForecastService ForecastService
の部分と、@code
の中を削除します。イメージは以下のようになります。
F5 を押してビルド&実行します。ここで以下の行にブレークポイントを設定してみてください。
ブラウザが開いたら Fetch data 画面に移動します。すると、先ほど仕掛けたブレークポイント8のところで実行が止まっているのが分かると思います。この OnInitializeAsync()
というメソッドは、ページがレンダリングされる前の初期化の際にフレームワーク側から呼び出されます。何か初期化処理を行いたい場合は、ここでやるようにします9。VS(または VS Code)をアクティブにし、F5 を押して処理を続行させてください。
元の .razor ファイルにあった @inject
あるいは分離した C# コードの [Inject]
属性は、その引数あるいは修飾されている変数に、フレームワークからインスタンスが「注入」されてくることを宣言しています。いわゆる「依存性の注入」(Dependency Injection)と呼ばれる機構です。
注入されてくるインスタンスは、あらかじめフレームワークによってシングルトンとして生成されている必要があります。このことをフレームワーク側に伝えるには Startup.cs の ConfigureService()
メソッドで service.AddSingleton<CLASS_NAME>()
を呼び出します。実際のコード例は下記のようになります。
上記31行目で AddSingleton()
が実行されています10。ここの WeatherForecastService
クラスは Data/WeatherForecastService.cs で定義しています。
Index.razor
Index.razor ファイルは、先頭の @page "/"
で分かるようにルートページのコンテンツを提供しています。
特徴的なのは最後の行の <SurveyPrompt ・・・ />
というタグですね。このように Razor コンポーネント名をタグとして使う11と、その場所に指定の Razor コンポーネント(この場合は SurveyPrompt
)が展開されます。さらには Title="How is Blazor working for you?"
という形でコンポーネントに値を渡すこともできます。
SurveyPrompt.razor
では SurveyPrompt.razor を見てみましょう。このファイルは Shared というフォルダに置かれており、まさに「共通部品」という扱いになっています。
まず最初の3行。ここは画面表示されるHTML部となります。
@page
命令が無いのがお分かりでしょうか。前述のように @page
のない .razor ファイルはコンポーネント(部品)として機能します。また、3行目、@Title
でプロパティ変数 Title
を参照しています。
そして末尾の C# コードブロック。
ここで Title
をプロパティとして定義し、かつ [Parameter]
属性を付与しています。この [Parameter]
属性が付与されたプロパティには、先ほどの <SurveyPrompt Title="How is Blazor working for you?" />
のような形で値を渡すことができるようになるわけです。そう考えると引数指定の関数呼び出しみたいにも見えますね。
実際にブラウザに表示されている文言を見ると、たしかに Title
の引数として記述した文字列が渡されていることを確認できます。
Razor コンポーネントがまさに「部品」として使われていることが分かるかと思います。
_Imports.razor
_Imports.razor は特殊なファイルです。このファイルが置かれたフォルダおよびそのサブフォルダに配置した .razor ファイルによって暗黙的にインポートされます。たとえば、これまでに説明した Counter.razor や SurveryPrompt.razor、また後述する App.razor など、すべての .razor ファイルによってインポートされることになります。サンプルプロジェクトでは、下記のように @using
命令を記述しておくことで個々の .razor ファイルでは @using
を書かかずに済むようにしています。
App.razor, MainLayout.razor, NavMenu.razor (and _Host.cshtml)
このあたりから鬼門12に突入していきます。
_Host -> App -> MainLayout -> NavMenu
-> Body
というような呼び出し構造になっているので、まずはサイトの入り口となる _Host.cshtml を再訪。<body>
のところだけ掲出します。
20行目、<app>
13 タグに囲まれた部分に <component type="typeof(App)" render-mode="ServerPrerendered" />
とあります。component
はフレームワーク側で用意している TagHelper14 のようです。サーバ側Blazor の render-mode
には、ServerPrerendered の他に Server と Static があります15。type
属性の値としてtypeof(App)
とあるので、おそらくここに下記 App.razor の内容が展開されるのでしょう。
ここで使われている Router
や Found
などは Microsoft.AspNetCore.Components 配下で定義されているコンポーネントのようです。
まず <Router AppAssembly="@typeof(Program).Assembly">
で当アプリの Program アセンブリを指定していると思われます。そして、ルーティングすべきデータがあれば <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
により、MainLayout.razor に記述されているレイアウトに従ってコンテンツを配置する、ということでしょう。
先頭の @inherits LayoutComponentBase
で、当ファイルから生成されるクラスが LayoutComponentBase
から派生すべきことを宣言しています。
その後に<div>
が2つあり、左側に sidebar、右側に main というレイアウトになっています。そして、sidebar の中味が NavMenu
であること、main の上部に About があって、その下に @Body
があること、が分かります。
@Body
で参照されている Body
は、下図のように派生元である LayoutComponentBase
クラスに含まれるプロパティのようです。このプロパティを呼び出すと描画するべきコンテンツが得られるということですね。
このレイアウトに従って表示すると下図のようなページが得られるというわけです。
終わりは NavMenu.razor です。3つの部分に分かれています。まずは上部の brand の部分。
アプリ名 MyBlazorApp を表示し(2行目)、それをクリックすると ToggleNavMenu
メソッドを呼び出すようになっています(3行目)。ToggleNavMenu
メソッドについては後述するコードブロックの中で定義されています。
次は各ページへのリンクです。
ここでもクリックすると ToggleNavMenu
を呼び出すようになっています(8行目)。また class 名として NavMenuCssClass
プロパティを参照しています。9行目以降、リストアイテムとして Home, Counter, Fetch data が並んでいます。それぞれ href
として ""
, counter
, fetchdata
を指定しているので、ルート(/
)からの相対パスと考えれば、これは既に見た Index.razor, Counter.razor, FetchData.razor の @page
命令で設定したパスに一致しています。
ここで使われている NavLink
ですが、ドキュメント「NavLink コンポーネント」を見ると次のように説明されています。
ナビゲーション リンクを作成するときは、HTML ハイパーリンク要素 (<a>) の代わりに NavLink コンポーネントを使用します。 NavLink コンポーネントは <a> 要素のように動作しますが、href が現在の URL と一致するかどうかに基づいて active CSS クラスを切り替える点が異なります。
この文の意味するところをじっくりと考えてみたのですが、おおよそ次のようなことかと思われます。
-
NavLink
はa
タグのような動作をする(href
やtarget
などa
タグと同じ属性が使える) - 表示されているページの URL が
href
で指定したものにマッチしていれば、当該項目に適用される css がアクティブなものに切り替わる
実際にブラウザに表示された画面を見ると、その時に表示しているページのURLにマッチしている項目の背景色がハイライトされています。つまり「href が現在の URL と一致するかどうかに基づいて active CSS クラスを切り替える」というのはこれを意味しているのではないかと思われます。
href
の値と現在表示されているページのURLとのマッチング方法を指定するのが Match
属性です。Home の NavLink を見ると Match="NavLinkMatch.All"
という属性値が指定されています。「NavLink コンポーネント」には次のような説明があります。
要素の Match 属性に割り当てられる 2 つの NavLinkMatch オプションがあります。
・NavLinkMatch.All:NavLink は、現在の URL 全体に一致する場合にアクティブになります。
・NavLinkMatch.Prefix (既定値):NavLink は、現在の URL の任意のプレフィックスに一致する場合にアクティブになります。
つまり、NavLinkMatch.All
の場合は指定のパスに完全一致した場合にアクティブに切り替わり、Match
を省略または NavLinkMatch.Prefix
に設定すると、href
値がURLの先頭部分に一致した場合にアクティブになる、ということです。
Home は href=""
なので、もしこれが NavLinkMatch.Prefix
だったら任意のURLにマッチしてしまいます。なのでここは NavLinkMatch.All
が指定されているわけですね。では Prefix
のほうの動きも確認してみましょう。
Counter.razor の @page
の部分を次のように修正します。
@page "/fetchdata/counter"
NavMenu.razor の Counter の NavLink 部分を次のように修正します。
<NavLink class="nav-link" href="fetchdata/counter">
つまり、 Counter と Fetch data が同じプレフィックス fetchdata
を持つようにしたわけです。修正後のイメージは、それぞれ次のようになります。
実行してみます。
「Counter」をクリックした画面ですが、思ったとおり、「Counter」と「Fetch data」の両方がハイライトされていますね!
最後はコードブロックです。
NavMenuCssClass
プロパティとToggleNavMenu()
メソッドが定義されています。NavMenuCssClass
は <div class="@NavMenuCssClass" >
という使われ方をしているので、どうやら class を collapse
にするか、あるいは class 属性そのものを省略する16か、ということを切り替えているようです。collapse
というのは bootstrap という CSS セットで定義されているようなのですが、自分の環境だと見た目の変化は分かりませんでした。
Program.cs と Startup.cs
最後の鬼門です。
Program.cs は Program
クラスを定義し、Startup.cs は Startup
クラスを定義しています。
Program
クラスにはプログラムのエントリポイントである Main()
が定義されています。Main
からは CreateHostBuilder
が呼ばれ、そこで Startup
クラスが使われています。
Startup
クラスには ConfigureService()
と Configure()
の2つのメソッドがあります。
まず ConfigureService()
ここでは以下のことを行っているようです。
- Razor Pages の有効化 (
service.AddRazorPages()
) - サーバサイド Blazor の有効化 (
services.AddServerSideBlazor()
) -
WeatherForecastService
クラスのインスタンスをDI可能なシングルトンとして追加する (services.AddSingleton<WeatherForecastService>()
)
サーバサイド17 Blazor の場合、はじめの2つは必ず呼ばれることになります。AddSingleton<T>()
はアプリ全体で使い回したいシングルトンなインスタンスが必要となる場合に呼び出すことになります。
次は Configure()
Configure()
には Use~
というメソッド呼び出しが並んでいます。プログラム中のコメントにも書いてありますが、これはいわゆる HTTP パイプラインフェーズで行われる処理を並べたもののようです。
add.UseRouting()
でルーティング機構を有効にしています。endpoints.MapBlazorHub()
と endpoints.MapFallbackToPage()
については「ASP.NET Core エンドポイントのルーティングの統合」に説明があります。
Blazor Server は ASP.NET Core エンドポイントのルーティングに統合されています。 ASP.NET Core アプリは、Startup.Configure で MapBlazorHub を使用して、対話型コンポーネントの着信接続を受け入れるように構成します。
[略]
最も一般的な構成は、すべての要求を Razor ページにルーティングすることです。これは、Blazor Server アプリのサーバー側部分のホストとして機能します。 通常、ホスト ページは、_Host.cshtml という名前になります。
MapBlazorHub()
の呼び出しにより、ブラウザ側からの通信を受け付けるようになるということかと思われます。MapFallbackToPage("/_Host")
の呼び出しは、フォールバックページを _Host に設定するということでしょう。
さて次回以降、このサンプルプロジェクトを換骨奪胎して、我々独自のアプリに作りかえていくことにします。
-
Startup.cs の中で Pages 以外のフォルダをルートに変更することもできます。https://www.learnrazorpages.com/razor-pages/routing#changing-the-default-razor-pages-root-folder を参照。 ↩ ↩2
-
MVVM (Model-View-ViewModel) パターンとも言うらしいです。 ↩
-
大文字・小文字は区別されないので、
/foo/bar
も同じファイルを指します。 ↩ -
ルーティング機能は、 Startup.cs の Configure() の中で
add.UseRouting();
を呼ぶことで有効になるようです。 ↩ -
まったく紛らわしいのですが、.razor という拡張子を持つファイルが Blazor アプリにおけるコンポーネントになります。そして、正式名称は「Razor コンポーネント」です。Razor Page と Blazor の勉強を始めた当初は、本当に混乱させられました。 ↩
-
.razor ファイルと同じ名前で末尾に .cs を付けます。Visual Studio のソリューションエクスプローラでは、本文の図にもあるように .razor ファイルと束ねられて表示されます。 ↩
-
実際はここで
ComponentBase
からの派生の記述は不要です(.razor ファイルから自動生成される partial クラス定義ですでに派生がなされているため)。 OnInitializedAsync() などがどのクラスで定義されているかを明示するためにあえて記述しました。 ↩ -
ここでは分離した .cs ファイルの中でブレークポイントを仕掛けましたが、.razor ファイルのコードブロックでブレークポイントを仕掛けることもできます。 ↩
-
アプリによっては、
OnInitializeAsync()
が2回呼び出されることがあります。対処方法は次回以降の記事で説明予定。 ↩ -
ここの29行目と30行目は、どんなサーバサイドBlazorアプリでも呼び出されます。31行目の
AddSingleton()
が個々のアプリごとに変わる部分となります。 ↩ -
Microsoftのドキュメント「コンポーネントを使う」に説明があります。 ↩
-
よく理解できていない要素がいっぱい出てくる、という意味です。 ↩
-
余談ですが、
<app>
タグは HTML 4.01 の時点ですでに deprecated になってるんですね。そのためか、次の ASP.NET Core 5.0 では変更されるかもしれません。 ↩ -
正直、筆者は TagHelper の仕組みをよく分かっていないのですが、とりあえずは「何らかのマクロ的なモノ」という理解で進みたいと思います。勉強したい方は、たとえば Learn Razor Pages などを参照ください。 ↩
-
render-mode
の説明は「ASP.NET Core Blazor ホスティング モデルの構成 - 表示モード」にあります。「Tips編」で説明予定。 ↩ -
Razer Pages で
<input type="checkbox" checked="@value">
のような記述をすると、value
が false や null だった場合は、属性そのものが省略される仕組みになっています。また true の場合は属性名のみが残ります。参考:Learn Razor Pages ↩ -
他に WebAssembly を用いたクライアント側の Blazor もあります。 ↩