ASP.NET CoreのMVC部分のファイル構成と内容を見ていく。
プロジェクトの追加から[ASP.NET Core Web アプリケーション]、[Web アプリケーション(モデル ビュー コントローラー)]と進んでいくと作成することができるので、これをベースに解説する。
ASP.NET CoreのMVCテンプレートは幾つかのファイルやコンポーネントから構成される。全体像は以下の図で示す。
フロントエンドとは利用者が直接触れる部分を示していて、HTMLやJavaScriptライブラリなどを記述していくことを示す。
ライブラリにはテンプレートでjQueryとBootstrapが使われている。Bootstrapは表示用に使用されている。jQueryはモデルに[Required]や[StringLength(30)]などの検証属性が使われたときに使用される。もちろん、使いたいライブラリがあれば追加できる。
HTML/CSS/JavaScriptはウェブページ制作ではおなじみだが、従来からの「Webフォーム」と比べればすっきりしたコードを仕上げることができる。[wwwroot]フォルダには、直接外部接続にやり取りする静的ファイルが置かれている。
Razorはビュー・エンジンでcshtmlファイルに@で始まるインライン式を埋め込むことができる。例としては、@RenderBody()、@ViewBag.Message、 @Html.Partial("_Partial")、@Url.Content 、@ViewData、@Html.ActionLink、@Html.LabelFor(model => model.Id) など。@Html.Raw(ViewBag.Message)は便利だが、クロスサイト・スクリプティング脆弱性に注意が必要である。@@でエスケープする。if、switch、while、for、foreachなども使える。@* ~ *@はコメントである。Razor表示の呼び出しはC#で行い、ASP.NET CoreではMVCのコントローラーで呼び出す。
ASP.NET MVCは本ページのメインテーマである。モデル(Model)はデータ用のクラスで表現できる。ビュー(View)は画面の表示を担当する。コントローラー(Controller)はブラウザと、モデル・ビューの橋渡しを担当する。また、ビューにRazor機能があるため、ビューはモデルからデータを取得できる。図ではフォルダ分けの意味でASP.NETに接しているが、機能的にはRazorの中や上の位置と考えることもできる。
ASP.NETはメインとなる開発環境になる。プログラムのエントリポイントは一般的なC#のテンプレートと同じくProgram.csに記述されている。Program.csには外部との接続方法を記述する。またサービスプロバイダの初期化処理を記述することができる。
内部接続の設定についてはStartup.csに記述される。
IIS/IIS ExpressはともにWebサーバーである。IIS Expressは開発用に使用され、VisualStudioでも標準で利用することができる。運用時にはIISなどを利用する。
Webサーバーを使わずに、直接起動することもできる。ツールバーの「IIS Express」の右の小さな▼をクリックし上から3番目の項目、「IIS Express」の下の「プロジェクト名」が表示されているところをクリックする。この状態で実行すると、プロンプトが表示され、ページが開く。[プロジェクト[>[プロパティ]>[デバッグ]>[プロファイル]が変更されている。
インターネット/イントラネット/localhostはGoogleChromeなどのブラウザがアクセスする。プログラムの実行中にページにアクセスすることができる。開発中はおもにlocalhostへアクセスする。GoogleChromeでサーバーを確認できる。ページを右クリックし[検証]>[Network]>[localhost]の[ResponseHeader]内の[Server]が「Microsoft-IIS/10.0」であればIISまたはIIS Expressが稼働しているサーバーであることを示し、「Kestrel」であれば直接ASP.NETが稼働していることを示す。
とりあえず、実行してみよう。実行方法はいくつかある。デバッグ実行の場合はツールバーの「IIS Express」をクリックするか、メニューの[デバッグ]>[デバッグの開始]をクリックする、もしくはF5キーを押す。デバッグなしで実行するには、メニューの[デバッグ]>[デバッグなしで開始]をクリックする、もしくはCtrl+F5キーを押す。
ツールバーのIIS Expressで実行する以外の方法だと、「コンソール ウィンドウ ホスト」というプロセスが作成される。これはブラウザを閉じても終了しない。必要に応じてタスクマネージャでプロセスを終了しよう。
実行するとブラウザが起動し、以下のようなページが表示される。
タイトルは「Home Page - MvcTemplate」となっているが、「MvcTemplate」はプロジェクト名である。以降、このページではMvcTemplateはプロジェクト名とする。ページ左上にも同じテキストがある。パス名は表示されない。このページはプロジェクトの[Views]>[Index.cshtml]に相当するページである。
「Privacy」をクリックするとページが切り替わり、パス名は「Home/Privacy」となる。Privacyページはプロジェクトの[Views]>[Privacy.cshtml]に相当するページである。Index.cshtmlと共通するレイアウトがあることに注目しよう。これはレイアウトの機能が使われている。
パス名を「」(パス名なし)または「Home」、「Home/Index」とすると、最初に開いたページと同じ内容が表示される。これは、コントローラーのメソッドに追加する属性で表すなら[Route(""), Route("Home"), Route("Home/Index")]である。一方、パス名を「index.html」とすると「ページが見つかりません」というエラーが表示される。
また、「obj\Debig(またはRelease)\netcoreapp3.1\Razor\Views」には中間ファイルであるC#ファイルが生成されている。
ASP.NET Coreでは「Controllers」と「Models」、「Views」の3つのフォルダがテンプレートで用意される。
「Controllers」フォルダはコントローラーファイルを配置する。テンプレートでは「HomeController.cs」が配置されている。ファイル名のController.csの部分はコントロールを示すため固定である。「Home」の部分は任意のコントロール名である。ブラウザと、モデル・ビューとの橋渡しを行うこのファイルである。コードのIndexメソッドは return View(); は[Views]>[Home]フォルダの「Index.cshtml」を表示する。「return View("Privacy");」に変えれば、インデックスでもプライバシーページが表示される。「NotFound();」であれば「HTTP ERROR 404 」、「BadRequest();」であれば「HTTP ERROR 400 」を表示する。「Content("str");」であればテキスト"str"を表示する。後でまた説明する。
「Models」フォルダはモデルファイルを配置する。テンプレートでは「ErrorViewModel.cs」が配置されている。このファイルは[Views]>[Shared]フォルダの「Error.cshtml」とデータをやり取りするファイルである。
「Views」フォルダはビューファイルを配置する。その直下には、HomeとSharedフォルダ、_ViewImports.cshtml、_ViewStart.cshtmlが配置されている。アンダースコアから始まるファイルはレイアウトファイルである。レイアウトファイルはページのヘッダーやフッターなどの共通要素を含み、これを直接ブラウザに表示することはできない。
Index.cshtmlとPrivacy.cshtml はコンテンツファイルである。Error.cshtml はエラー用のコンテンツファイルである。
_ViewImports.cshtml は共通の名前空間インポート、共通の依存関係の挿入を行う。共有ディレクティブのインポート。C言語でのcommon.hのような役割で#includeを複数行うようなものである。「@using MvcMovie.Models」は同一フォルダとその子孫フォルダのcshtmlファイルでクラスを指定する際の使用できる名前空間として機能する。@using MvcMovie.Modelsを削除するとビルドエラーになるが、Error.cshtmlの「@model ErrorViewModel」が名前空間の参照できないためである。(この場合「@model MvcMovie.Models.ErrorViewModel」とすれば参照可能)「@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers」はタグヘルパー機能を提供する。_Layout.cshtmlにある「asp-area」や「asp-action」をプログラムとして機能させることができる。
_ViewStart.cshtml は各ビューの前に実行するコードを記述する。テンプレートでは「@{ Layout = "_Layout"; }」となっている。
_Layout.cshtml はレイアウトの本体である。「@ViewData["Title"]」はタイトルをデータバインドしている。Index.cshtmlでは「@{ ViewData["Title"] = "Home Page"; }」となっているので、「Home Page」が表示されるという仕組みだ。データバインドはRazorのビュー・エンジン機能である。
「@RenderBody()」は「Index.cshtml」などのページコンテンツを埋め込んでいる。レイアウトを使用する場合は「@RenderBody()」を必ず1つ呼び出される。「@RenderBody()」が無ければページを表示できず、2つ目以降は無効である。
「@RenderSection("Scripts", required: false)」はIndex.cshtmlなどのコンテンツファイルに記述される部分で、Scriptsセクションとして利用できる。「required: false」というのはScriptsセクションが無くてもエラーにならないという意味で、省略するとtrueがデフォルトで設定される。
_ValidationScriptsPartial.cshtml はクライアントで検証行う機能を提供する。モデルの値に[RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$")]のような検証属性を付与したとき、cshtmlに「@section Scripts{ @{await Html.RenderPartialAsync("_ValidationScriptsPartial"); } }」を記述することで、クライアント検証を行う。ただし、クライアント側はクライアント検証を無効にできるので、サーバー検証に抜けがあってはならない。
全体を振り返って、Indexへのリクエストに「Welcome」と表示する流れとしては以下の図のようになる。
「Controllers」フォルダのHomeController.csに戻る。_logger変数が定義されているので使ってみよう。Indexメソッドの開始直後に「_logger.LogInformation("InfolLg Index()");」を追加しIIS Expressで実行する。Indexページが読み込まれるたびにVisualStudio出力ペインに「MvcTemplate.Controllers.HomeController: Information: InfolLog Index()」と出力される。
次にクラスのインスタンス化について検証しよう。以下のようにコードを変更する。
public class HomeController : Controller { private readonly ILogger<HomeController> _logger; private static int s_Count = 0; private int m_Count = 0; ・・・ public IActionResult Index() { _logger.LogInformation("s_Count=" + s_Count + ", m_Count=" + m_Count); s_Count++; m_Count++; return View(); } ・・・これで実行し、何度かIndexページをリロード等して何度も読み込むと出力ペインを確認する。
m_Countがカウントアップしないことから、HomeControllerクラスはページ読み込みの度に再インスタンス化されることが確認できる。また、s_Countがカウントアップしていることから、同一プロセスでページ読み込み処理をしていることが分かる。
[Views]フォルダ>[Shared]フォルダ>[Error.cshtml]をまだ表示していない。これは運用時のエラー表示に利用するためのページファイルである。表示するには、例外ハンドラで表示されるよう設定し、例外を発生させる。
まずは例外を発生させてみる。「Controllers」フォルダのHomeController.csでIndexメソッドを以下のように変更する。
public IActionResult Index() { throw new Exception("例外発生"); return View(); }実行すると、開発モードでの例外ページが表示される。
Error.cshtmlを表示するには、ここからハンドラーを設定する。Startup.csのConfigureメソッドを確認する。メセッドのすぐ後の部分のコードである。
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Home/Error"); app.UseHsts(); } ・・・このelse節の方を処理できるようにする。「if (env.IsDevelopment())」を「if (false && env.IsDevelopment())」に変えてもよい。因みに、条件式の「env.IsDevelopment()」というのは環境変数「ASPNETCORE_ENVIRONMENT」に「Development」が設定されているなら true が返る。既定では、プロジェクトを右クリックし[プロパティ]>[デバッグ]>[環境変数]に設定されている。
実行すると、以下のようなページが表示される。
英語ページではあるが、Error.cshtmlの内容がレイアウトされた上で表示されていることを確認できる。
Modelsフォルダ内のErrorViewModel.csの RequestId が表示されている。「get; set;」の部分を「set { } get { return "MyRequestId"; }」と変えて再度実行すると「Request ID」が「MyRequestId」に変わったページが表示される。[Views]>[Shared]フォルダ内のError.cshtmlの「@Model.RequestId」とあるので、この部分にデータバインドされていることを確認しよう。
おまけで、Razorマップを追加してみる。MVCではない方のテンプレートを参考にする。プロジェクトに「Page1」フォルダ作成する。その中にMVCではない方のテンプレートから「Index.cshtml」と「Index.cshtml.cs」をコピーする。さらに、MVCテンプレートのビューフォルダから「_ViewImports.cshtml」を「Page1」フォルダにコピーする
Startup.csをRazorPages用に2行追加する。
・・・ public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews(); services.AddRazorPages(rpo => { rpo.RootDirectory = "/Pages1"; }); } ・・・ app.UseEndpoints(endpoints => { endpoints.MapRazorPages(); endpoints.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); }); ・・・実行すると、以下のエラーが出る。
エラー CS0246 型または名前空間の名前 'IndexModel' が見つかりませんでした (using ディレクティブまたはアセンブリ参照が指定されていることを確認してください)。
他のプロジェクトからファイルコピーしたことによる、中間ファイルでの名前空間のエラーである。Page1/Index.cshtml.csの名前空間「RazorPagesMovie.Pages」を_ViewImports.cshtmlに表示されている名前空間に合わせる。これでビルドできるので、実行するとPage1フォルダの方のファイルが表示される。
因みに、Index.cshtmlの「@model IndexModel」を削除すれば、Index.cshtml.csは空でも、ファイルがなくともビルドできるし、ページも表示される。
Index.cshtml.csのOnGet()に「_logger.LogInformation("info OnGet()");」などログ出力を記述すれば、VisualStudioのログ出力等にログを表示できる。ログはページ読み込みの度に出力される。