12
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ASP.NET Core Blazor と Chart.js で入門する Web アプリ作成 - 探究編:Blazorの仕組みを理解する

Last updated at Posted at 2020-10-18

「ASP.NET Core Blazor と Chart.js で入門するWebアプリ作成」の2回目。各回記事の内容は以下のようになります。

導入編:サンプルを動かす
探究編:Blazorの仕組みを理解する(当記事)
実践編:Chart.jsでグラフを描く
Tips編:BlazorやChart.jsのあれこれ
番外編:Ubuntuサーバで公開する

今回の内容は、第1回「導入編」の「作業の流れ」で提示した項目のうち「3. Blazor フレームワークの主な構成要素を知る」です。

Blazor フレームワークの主な構成要素を知る

本稿の中では、おそらく今回記事が一番ハードとなります。読んでいるうちに「あ、挫折しそう」と思ったら、迷わず次回記事に移りましょう。いろいろとプログラムをいじりながら、何か疑問が出たらこの記事に戻ってきてください。

この節の内容をより深く理解したい方は、Razor Pages についても知っておくとよいかと思います。この目的には以下のサイトが役立ちました。というか、泥縄ですが筆者は本節を書くのにこちらのサイトで勉強し直しました。

Webアプリ初心者(本稿執筆前の筆者)は、たぶん、いきなり上記のサイトを読んでも興味が続かないかもしれません。とりあえずは本稿を読み進め、何かを作る経験をした上で、疑問点だらけの頭にしてから読むと得るものも多いと思います。

Blazor プロジェクトの全体構成

前回作成したサンプルプロジェクトで、VS のソリューションエクスプローラは下図のようになっています。
image.png

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 の中を見ると次のようなコードがあります。
image.png
ここの service.AddRazorPages() により Razor Pages が有効になり、services.AddServerSideBlazor() により Blazor が有効になるようです。

@page にはそのページのパスを書くこともできます。たとえば、

@page "/foo"

とあれば、これは /foo というページを表わしていることになります。

@page にパスを書かなければ、そのファイルのパスは「Pages1 をルート(Root)とし、ディレクトリ階層をたどり、ファイル名から拡張子を除いたもの」になります。たとえば Pages/Foo/Bar.cshtml ファイルのパスは /Foo/Bar3 になります。

この、URLで指定されているパスと実際に表示されるファイルを結びつける役割を果たしているのが「ルーティグ」(routing) と呼ばれる機構4です。ルーティングという機能は、MVC パターンだとコントローラが担っているかと思うのですが、Razor Pages の場合はフレームワークのほうでよしなに計らってくれるわけですね。

以下、 Pages に含まれる .cshtml ファイルの説明。

_Host.cshtml
ページの大枠を記述する Razor Pages ファイル。先頭が@page "/"なのでルートページとなる。中を見ると分かるが、<head> タグや <body> タグが使われている。ページ内で必要になる css や JavaScript ファイルはここで読み込んでおく。詳細については後述。
Error.cshtml
必須ではないが、何かエラーが発生したときに表示する内容を記述している。このページのパスは /Error になるが、Startup.csConfigure() の中で例外ハンドラーとしてこのページを指定している。

.razor

.razor という拡張子の付いているファイルは、再利用可能なUIコンポーネントを定義するためのファイルです。そしてこの .razor ファイルこそが Blazor フレームワークの中心的役割を果たします。

この部品は「Razor コンポーネント5」と呼ばれます。もちろん、ページ自身もコンポーネントとして定義することができます。ページ自身も含め、いろいろと使い回せるような部品をいい感じに書くことができる仕組みだと考えておけば良さそうです。

.razor ファイルの記述法は .cshtml ファイルと似ています。ただし以下の違いがあります。

  • @page 命令を記述する場合はパス指定を省略することができない

@page 命令を書く場合は、以下のように必ずパスを指定します。

Counter.razor
@page "/counter"

パス指定を省略して(@pageだけにして)ビルドすると以下のようなエラーになります。
image.png
@page 命令自体を省略することはできます。@page 命令があるとページとして機能し、無ければ部品として機能するようです(後述の SurveyPrompt.razor など)。

  • .razor ファイルには <script> タグが書けない

無理やり書いてみると VS に叱られます。
image.png
ちなみに次回の「実践編」では、 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 コンポーネントの説明をします。

image.png
先頭に@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 を作成してください。

VS Code の場合:
image.png

VS の場合:
image.png

内容を以下のように置き換えます。(違いが分かるようにインクリメント幅を 2 に変更しています)

Counter.razor.cs
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 の中は削除してください。イメージは次のようになります。
image.png
image.png

F5 を押してビルド&実行します。ブラウザが開いたら Counter 画面に移動します。ここで「Click me」ボタンをクリックすると、"Current count" が 2 になるはずです。上記修正が有効に機能しているのが分かると思います。

FetchData.razor

FetchData.razor についても C# コードの分離をやってみましょう。Counter.razor の場合と同様、 FetchData.razor.cs というファイルを作成します。内容を以下で置き換えてください7

FetchData.razor.cs
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 の中を削除します。イメージは以下のようになります。
image.png
image.png
F5 を押してビルド&実行します。ここで以下の行にブレークポイントを設定してみてください。
image.png
ブラウザが開いたら Fetch data 画面に移動します。すると、先ほど仕掛けたブレークポイント8のところで実行が止まっているのが分かると思います。この OnInitializeAsync() というメソッドは、ページがレンダリングされる前の初期化の際にフレームワーク側から呼び出されます。何か初期化処理を行いたい場合は、ここでやるようにします9。VS(または VS Code)をアクティブにし、F5 を押して処理を続行させてください。

元の .razor ファイルにあった @inject あるいは分離した C# コードの [Inject] 属性は、その引数あるいは修飾されている変数に、フレームワークからインスタンスが「注入」されてくることを宣言しています。いわゆる「依存性の注入」(Dependency Injection)と呼ばれる機構です。

注入されてくるインスタンスは、あらかじめフレームワークによってシングルトンとして生成されている必要があります。このことをフレームワーク側に伝えるには Startup.csConfigureService() メソッドで service.AddSingleton<CLASS_NAME>() を呼び出します。実際のコード例は下記のようになります。
image.png
上記31行目で AddSingleton() が実行されています10。ここの WeatherForecastService クラスは Data/WeatherForecastService.cs で定義しています。

Index.razor

Index.razor ファイルは、先頭の @page "/" で分かるようにルートページのコンテンツを提供しています。
image.png
特徴的なのは最後の行の <SurveyPrompt ・・・ /> というタグですね。このように Razor コンポーネント名をタグとして使う11と、その場所に指定の Razor コンポーネント(この場合は SurveyPrompt)が展開されます。さらには Title="How is Blazor working for you?" という形でコンポーネントに値を渡すこともできます。

SurveyPrompt.razor

では SurveyPrompt.razor を見てみましょう。このファイルは Shared というフォルダに置かれており、まさに「共通部品」という扱いになっています。

まず最初の3行。ここは画面表示されるHTML部となります。
image.png
@page 命令が無いのがお分かりでしょうか。前述のように @page のない .razor ファイルはコンポーネント(部品)として機能します。また、3行目、@Title でプロパティ変数 Title を参照しています。
そして末尾の C# コードブロック。
image.png
ここで Title をプロパティとして定義し、かつ [Parameter] 属性を付与しています。この [Parameter] 属性が付与されたプロパティには、先ほどの <SurveyPrompt Title="How is Blazor working for you?" /> のような形で値を渡すことができるようになるわけです。そう考えると引数指定の関数呼び出しみたいにも見えますね。

実際にブラウザに表示されている文言を見ると、たしかに Title の引数として記述した文字列が渡されていることを確認できます。
image.png
Razor コンポーネントがまさに「部品」として使われていることが分かるかと思います。

_Imports.razor

_Imports.razor は特殊なファイルです。このファイルが置かれたフォルダおよびそのサブフォルダに配置した .razor ファイルによって暗黙的にインポートされます。たとえば、これまでに説明した Counter.razorSurveryPrompt.razor、また後述する App.razor など、すべての .razor ファイルによってインポートされることになります。サンプルプロジェクトでは、下記のように @using 命令を記述しておくことで個々の .razor ファイルでは @using を書かかずに済むようにしています。
image.png

App.razor, MainLayout.razor, NavMenu.razor (and _Host.cshtml)

このあたりから鬼門12に突入していきます。

_Host -> App -> MainLayout -> NavMenu
                           -> Body

というような呼び出し構造になっているので、まずはサイトの入り口となる _Host.cshtml を再訪。<body>のところだけ掲出します。
image.png
20行目、<app>13 タグに囲まれた部分に <component type="typeof(App)" render-mode="ServerPrerendered" /> とあります。component はフレームワーク側で用意している TagHelper14 のようです。サーバ側Blazor の render-mode には、ServerPrerendered の他に ServerStatic があります15type属性の値としてtypeof(App) とあるので、おそらくここに下記 App.razor の内容が展開されるのでしょう。

image.png
ここで使われている RouterFound などは Microsoft.AspNetCore.Components 配下で定義されているコンポーネントのようです。

まず <Router AppAssembly="@typeof(Program).Assembly"> で当アプリの Program アセンブリを指定していると思われます。そして、ルーティングすべきデータがあれば <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" /> により、MainLayout.razor に記述されているレイアウトに従ってコンテンツを配置する、ということでしょう。
image.png
先頭の @inherits LayoutComponentBase で、当ファイルから生成されるクラスが LayoutComponentBase から派生すべきことを宣言しています。

その後に<div>が2つあり、左側に sidebar、右側に main というレイアウトになっています。そして、sidebar の中味が NavMenu であること、main の上部に About があって、その下に @Body があること、が分かります。

@Body で参照されている Body は、下図のように派生元である LayoutComponentBase クラスに含まれるプロパティのようです。このプロパティを呼び出すと描画するべきコンテンツが得られるということですね。
image.png
このレイアウトに従って表示すると下図のようなページが得られるというわけです。
image.png

終わりは NavMenu.razor です。3つの部分に分かれています。まずは上部の brand の部分。
image.png
アプリ名 MyBlazorApp を表示し(2行目)、それをクリックすると ToggleNavMenuメソッドを呼び出すようになっています(3行目)。ToggleNavMenuメソッドについては後述するコードブロックの中で定義されています。

次は各ページへのリンクです。
image.png
ここでもクリックすると 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 クラスを切り替える点が異なります。

この文の意味するところをじっくりと考えてみたのですが、おおよそ次のようなことかと思われます。

  • NavLinka タグのような動作をする(hreftarget など 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 の部分を次のように修正します。

Counter.razor
@page "/fetchdata/counter"

NavMenu.razor の Counter の NavLink 部分を次のように修正します。

NavMenu.razor/カウンターページへのリンク
            <NavLink class="nav-link" href="fetchdata/counter">

つまり、 Counter と Fetch data が同じプレフィックス fetchdata を持つようにしたわけです。修正後のイメージは、それぞれ次のようになります。
image.png
image.png

実行してみます。
image.png
「Counter」をクリックした画面ですが、思ったとおり、「Counter」と「Fetch data」の両方がハイライトされていますね!

最後はコードブロックです。
image.png
NavMenuCssClassプロパティとToggleNavMenu()メソッドが定義されています。NavMenuCssClass<div class="@NavMenuCssClass" > という使われ方をしているので、どうやら class を collapse にするか、あるいは class 属性そのものを省略する16か、ということを切り替えているようです。collapse というのは bootstrap という CSS セットで定義されているようなのですが、自分の環境だと見た目の変化は分かりませんでした。

Program.csStartup.cs

最後の鬼門です。

Program.csProgram クラスを定義し、Startup.csStartup クラスを定義しています。
image.png
Program クラスにはプログラムのエントリポイントである Main() が定義されています。Mainからは CreateHostBuilder が呼ばれ、そこで Startup クラスが使われています。

Startup クラスには ConfigureService()Configure() の2つのメソッドがあります。

まず ConfigureService()
image.png
ここでは以下のことを行っているようです。

  • Razor Pages の有効化 (service.AddRazorPages())
  • サーバサイド Blazor の有効化 (services.AddServerSideBlazor())
  • WeatherForecastService クラスのインスタンスをDI可能なシングルトンとして追加する (services.AddSingleton<WeatherForecastService>())

サーバサイド17 Blazor の場合、はじめの2つは必ず呼ばれることになります。AddSingleton<T>() はアプリ全体で使い回したいシングルトンなインスタンスが必要となる場合に呼び出すことになります。

次は Configure()
image.png
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 に設定するということでしょう。

さて次回以降、このサンプルプロジェクトを換骨奪胎して、我々独自のアプリに作りかえていくことにします。

  1. Startup.cs の中で Pages 以外のフォルダをルートに変更することもできます。https://www.learnrazorpages.com/razor-pages/routing#changing-the-default-razor-pages-root-folder を参照。 2

  2. MVVM (Model-View-ViewModel) パターンとも言うらしいです。

  3. 大文字・小文字は区別されないので、/foo/bar も同じファイルを指します。

  4. ルーティング機能は、 Startup.cs の Configure() の中で add.UseRouting(); を呼ぶことで有効になるようです。

  5. まったく紛らわしいのですが、.razor という拡張子を持つファイルが Blazor アプリにおけるコンポーネントになります。そして、正式名称は「Razor コンポーネント」です。Razor Page と Blazor の勉強を始めた当初は、本当に混乱させられました。

  6. .razor ファイルと同じ名前で末尾に .cs を付けます。Visual Studio のソリューションエクスプローラでは、本文の図にもあるように .razor ファイルと束ねられて表示されます。

  7. 実際はここで ComponentBase からの派生の記述は不要です(.razor ファイルから自動生成される partial クラス定義ですでに派生がなされているため)。 OnInitializedAsync() などがどのクラスで定義されているかを明示するためにあえて記述しました。

  8. ここでは分離した .cs ファイルの中でブレークポイントを仕掛けましたが、.razor ファイルのコードブロックでブレークポイントを仕掛けることもできます。

  9. アプリによっては、OnInitializeAsync()2回呼び出されることがあります。対処方法は次回以降の記事で説明予定。

  10. ここの29行目と30行目は、どんなサーバサイドBlazorアプリでも呼び出されます。31行目の AddSingleton() が個々のアプリごとに変わる部分となります。

  11. Microsoftのドキュメント「コンポーネントを使う」に説明があります。

  12. よく理解できていない要素がいっぱい出てくる、という意味です。

  13. 余談ですが、<app> タグは HTML 4.01 の時点ですでに deprecated になってるんですね。そのためか、次の ASP.NET Core 5.0 では変更されるかもしれません

  14. 正直、筆者は TagHelper の仕組みをよく分かっていないのですが、とりあえずは「何らかのマクロ的なモノ」という理解で進みたいと思います。勉強したい方は、たとえば Learn Razor Pages などを参照ください。

  15. render-mode の説明は「ASP.NET Core Blazor ホスティング モデルの構成 - 表示モード」にあります。「Tips編」で説明予定。

  16. Razer Pages で <input type="checkbox" checked="@value"> のような記述をすると、valuefalsenull だった場合は、属性そのものが省略される仕組みになっています。また true の場合は属性名のみが残ります。参考:Learn Razor Pages

  17. 他に WebAssembly を用いたクライアント側の Blazor もあります。

12
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?