Quantcast
Channel: C#タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9691

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

$
0
0

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

導入編:サンプルを動かす
探究編:Blazorの仕組みを理解する(当記事)
実践編:Chart.jsでグラフを描く
Tips編:BlazorやChart.jsのTipsなど
発展編: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というフォルダを特別扱いしていて、ここが動的なページツリーのルート("/")に相当するようになっています2

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(コントローラ)を一つのファイルにまとめて記述できるようになったもの、ということらしいです3

実際 Blazor は、この Razor Pages を拡張したものです。 Startup.csの中を見ると次のようなコードがあります。
image.png
ここの service.AddRazorPages()により Razor Pages が有効になり、services.AddServerSideBlazor()により Blazor が有効になるようです。

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

@page "/foo"

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

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

この、URLで指定されているパスと実際に表示されるファイルを結びつける役割を果たしているのが「ルーティグ」(routing) と呼ばれる機構5です。ルーティングという機能は、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 コンポーネント6」と呼ばれます。もちろん、ページ自身もコンポーネントとして定義することができます。ページ自身も含め、いろいろと使い回せるような部品をいい感じに書くことができる仕組みだと考えておけば良さそうです。

.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.cs7を作成してください。

VS Code の場合:
image.png

VS の場合:
image.png

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

Counter.razor.cs
namespaceMyBlazorApp.Pages{publicpartialclassCounter{privateintcurrentCount=0;privatevoidIncrementCount(){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というファイルを作成します。内容を以下で置き換えてください8

FetchData.razor.cs
usingSystem;usingSystem.Threading.Tasks;usingMicrosoft.AspNetCore.Components;usingMyBlazorApp.Data;namespaceMyBlazorApp.Pages{publicpartialclassFetchData:ComponentBase{[Inject]privateWeatherForecastServiceForecastService{get;set;}privateWeatherForecast[]forecasts;protectedoverrideasyncTaskOnInitializedAsync(){forecasts=awaitForecastService.GetForecastAsync(DateTime.Now);}}}

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

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

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

Index.razor

Index.razorファイルは、先頭の @page "/"で分かるようにルートページのコンテンツを提供しています。
image.png
特徴的なのは最後の行の <SurveyPrompt ・・・ />というタグですね。このように Razor コンポーネント名をタグとして使う12と、その場所に指定の 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)

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

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

というような呼び出し構造になっているので、まずはサイトの入り口となる _Host.cshtmlを再訪。<body>のところだけ掲出します。
image.png
20行目、<app>14タグに囲まれた部分に <component type="typeof(App)" render-mode="ServerPrerendered" />とあります。componentはフレームワーク側で用意している TagHelper15のようです。サーバ側Blazor の render-modeには、ServerPrerenderedの他に ServerStaticがあります16type属性の値として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/カウンターページへのリンク
<NavLinkclass="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 属性そのものを省略する17か、ということを切り替えているようです。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>())

サーバサイド18 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. 今後の記事も毎週月曜の朝に投稿予定です。 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


Viewing all articles
Browse latest Browse all 9691

Trending Articles