📚

【MS Learn】ASP.NET Core を使用して Web UI を作成する をやってみた

2024/04/15に公開

まえがき

ASP.NET Core を始めることになったので、『Develop Web Apps with ASP.NET Core』シリーズの一番最初の『ASP.NET Core を使用して Web UI を作成する』をやってみました。

環境

Windows 11
.NET 8.0
Visual Studio Code

ユニット2. どのようなときに、なぜ Razor Pages を使用するのかを理解する

  • Razor とは?

Razor 構文では HTML と C# を組み合わせて、動的レンダリング ロジックを定義します。

JavaのSpringBootで言うところの、Thymeleaf テンプレート使ってるようなものかな?

追記
Thymeleaf より、FreeMarker の方が近いかも。

  • Razor Pages を使用するタイミング

次のようなときは、ASP.NET Core アプリで Razor Pages を使用します。

  • 動的 Web UI を生成したい。
  • ページに重点を置いたアプローチを優先する。
  • 部分ビューでの重複を減らしたい。
    Razor Pages を使用すると、関連するページとそのロジックを独自の名前空間とディレクトリにまとめて保持することで、ASP.NET Core ページの組織が簡略化されます。

どういうことかわからん。。

注意に

ASP.NET Core では、Web アプリを構築するための Model-View-Controller (MVC) パターンもサポートされています。 モデル、ビュー、コントローラーを明確に分離する場合は、MVC を使用します。 Razor Pages と MVC の両方が同じアプリ内で共存できます。 MVC はこのモジュールの範囲外です。

とあるので、MVCとはまた違った概念っぽいですね。わからん。

次からRazor Pages でやっていくみたいなので、実際に触りながら見ていきます。

ユニット3. 演習 - プロジェクトをカスタマイズする

  1. ソースコードをcloneする
PS > git clone https://github.com/MicrosoftDocs/mslearn-create-razor-pages-aspnet-core
Cloning into 'mslearn-create-razor-pages-aspnet-core'...
remote: Enumerating objects: 197, done.
remote: Counting objects: 100% (101/101), done.
remote: Compressing objects: 100% (63/63), done.
remote: Total 197 (delta 55), reused 38 (delta 38), pack-reused 96
Receiving objects: 100% (197/197), 833.30 KiB | 3.29 MiB/s, done.
Resolving deltas: 100% (89/89), done.
  1. フォルダを移動して、VSCodeを開きます。
PS > cd mslearn-create-razor-pages-aspnet-core
PS > code .

プロジェクト構成の確認

  • wwwroot/ --- 静的ファイル(HTML、CSS、JSなど)が配置

  • Pages/ --- Razor Pages(.cshtml と .chtml.cs) とサポートファイルが配置

  • Razor ページ ファイルとそれらとペアになっている PageModel クラス ファイル
    .cshtml ファイル を Razor ページファイル、.cshtml.cs を PageModel クラス ファイル とここでは呼んでいる

  • Pages ディレクトリ構造とルーティング要求
    ここはわかりやすく、Pages/ フォルダにある .cshtml のファイル名がそのまま URL になる
    Pages/Privacy.cshtml -> www.domain.com/Privacy
    Index の場合は、/ と /Index 両方に対応している。

サブディレクトリがある場合は、そのサブディレクトリ名も URL に追加される
Pages/Products/Index.cshtml -> www.domain.com/Products/Index

  • 共有ファイル
    • _ViewImports.cshtml --- 複数のページ間で使用する名前空間とクラスを一括でインポートしてくれる
    • _ViewStart.cshtml --- すべての Razor ページの既定のレイアウトを指定

すべてのページを既定ってことは、レイアウトが複数ある場合はどうなるんだろうか🤔

  • レイアウト
    • Shared/_Layout.cshtml --- 複数のページ間で共通するレイアウトを定義。 _ViewStart.cshtml で指定される。
    • Shared/_ValidationScriptsPartial.cshtml --- すべてのページに検証機能を提供する

_ValidationScriptsPartial.cshtml は、validation.min.js がいたので、検証系の実装をここですると、全体で呼び出せるようになるってことかな?

プロジェクトを初めて実行する

  1. エクスプローラで ContosoPizza プロジェクトを統合ターミナルで開くようにします。
  2. ターミナルウインドウでコマンドを実行します。
PS mslearn-create-razor-pages-aspnet-core\ContosoPizza> dotnet watch

このコマンドは、次の操作を行います。

  • プロジェクトをビルドします。
  • アプリを起動します。
  • ファイルの変更を監視し、変更が検出されたらアプリを再起動します。

hotreloadもやってくれるコマンドなのか。すご。

  1. ブラウザが起動して、画面が表示されます。

  2. レンダリングされたページと Pages/Index.cshtml を比較して、解説を見ながらふむふむします。

ランディング ページをカスタマイズする

ここからはコードの書き換えです。(・_・D フムフムしながら進めていきます。
ソースコードを保存したら、hot reload してくれるので便利です。

  1. Pages/Index.cshtml で、C# コード ブロックを次のコードに置き換えます。
Pages/Index.cshtml
@{
    ViewData["Title"] = "The Home for Pizza Lovers";
    TimeSpan timeInBusiness = DateTime.Now - new DateTime(2018, 8, 14);
}

タイトルが変わりました。

2. HTML を次のように変更します。

  • h1 の変更
Pages/Index.cshtml
<h1 class="display-4">Welcome to Contoso Pizza</h1>
  • p タグの変更
Pages/Index.cshtml
<p class="lead">The best pizza in town for @Convert.ToInt32(timeInBusiness.TotalDays) days!</p>


見出しと開業からの経過時間が表示されるようになりました。

Convertクラスが所属する System 名前空間は ContosoPizza.csproj の <ImplicitUsings> を enable することによって、自動インポートされます。なるほどー

ユニット4. 演習 - 新しい Razor ページを追加する

Pizza List ページを作成する

  1. もう一つ統合ターミナルを開きます。
  2. 新しいターミナルで次のコマンドを実行して、Razor ページを追加します。
PS mslearn-create-razor-pages-aspnet-core\ContosoPizza> dotnet new page --name PizzaList --namespace ContosoPizza.Pages --output Pages
テンプレート "Razor ページ" が正常に作成されました。

2つのファイルが追加されました。

3. Pages/PizzaList.cshtml にコードを追記します。

Pages/PizzaList.cshtml
@{
    ViewData["Title"] = "Pizza List 🍕";
}
<h1>Pizza List 🍕</h1>

<!-- New Pizza form will go here -->

<!-- List of pizzas will go here -->

ナビゲーション メニューに Pizza List ページを追加する

  1. Pages/Shared/_Layout.cshtml を開きます。
  2. Privacy リンクのある <li> タグの次に、PizzaList の要素を追加します。
Pages/Shared/_Layout.cshtml
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/Index">Home</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/Privacy">Privacy</a>
                        </li>
+                        <li class="nav-item">
+                            <a class="nav-link text-dark" asp-area="" asp-page="/PizzaList">Pizza List 🍕</a>
+                        </li>

Pizza List のリンクが追加されました。

選択すると、Pizza List のページに遷移します。

PizzaService クラスを依存関係挿入コンテナーに登録する

ピザ一覧ページは PizzaService に依存しているらしいので、コンテナに登録します。

  1. Program.cs を開きます。
  2. 次のコードを追加します。
Program.cs
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddDbContext<PizzaContext>(options =>
    options.UseSqlite("Data Source=ContosoPizza.db"));
+ builder.Services.AddScoped<PizzaService>();

ピザの一覧を表示する

PizzaService オブジェクトからピザのリストを取得し、プロパティに格納します。

  1. Pages/PizzaList.cshtml.cs を開きます。
  2. 次の using ステートメントをファイルの先頭に追加します。
Pages/PizzaList.cshtml.cs
using ContosoPizza.Models;
using ContosoPizza.Services;
  1. ContosoPizza.Pages 名前空間ブロック内で、PizzaListModel クラス全体を次のコードに置き換えます。
Pages/PizzaList.cshtml.cs
namespace ContosoPizza.Pages
{
    public class PizzaListModel : PageModel
    {
        private readonly PizzaService _service;
        public IList<Pizza> PizzaList { get;set; } = default!;

        public PizzaListModel(PizzaService service)
        {
            _service = service;
        }

        public void OnGet()
        {
            PizzaList = _service.GetPizzas();
        }
    }
}

PizzaService が DI されて、OnGet メソッドでピザの一覧を取得して PizzaList というプロパティに設定しているようです。

default! で 『null』 で初期化するようになり、null safety チェックをしなくなるみたいです。なるほど!!!
参考:「C# での Null Safety」- 意図の表現

public IList<Pizza> PizzaList { get;set; } = default!;

ピザの一覧を表示する

画面側で表示されるように修正します。

  1. Pages/PizzaList.cshtml を開きます。
  2. 末尾にあるコメントを以下のコードに書き換えます。
Pages/PizzaList.cshtml
<table class="table mt-5">
    <thead>
        <tr>
            <th scope="col">Name</th>
            <th scope="col">Price</th>
            <th scope="col">Size</th>
            <th scope="col">Gluten Free</th>
            <th scope="col">Delete</th>
        </tr>
    </thead>
    <tbody>
    @foreach (var pizza in Model.PizzaList)
    {
        <tr>
            <td>@pizza.Name</td>
            <td>@($"{pizza.Price:C}")</td>
            <td>@pizza.Size</td>
            <td>@(pizza.IsGlutenFree ? "✔️" : string.Empty)</td>
            <td>
                <form method="post" asp-page-handler="Delete" asp-route-id="@pizza.Id">
                    <button class="btn btn-danger">Delete</button>
                </form>
            </td>
        </tr>
    }
    </tbody>
</table>

ブラウザを見るとリストが表示されました。

ピザやっすいなー と思ったら、Currency で表示しているせいですね。

<td>@($"{pizza.Price:C}")</td>

ユニット5. タグ ヘルパーとページ ハンドラーについて

タグ ヘルパー

このプロジェクトでは4つのタグヘルパーが使われている

  • Partial
  • Label
  • Input
  • Validation Summary Message

詳しくは次のユニットにて。

ページ ハンドラー

PageModel クラスには、HTTPリクエストやレンダリングに使用されるデータのための、ページハンドラーが定義されている。

  • OnGet --- ページの初期表示用
  • OnPost --- フォームデータを受け取るメソッド

ユニット6. 練習 — 新しいピザフォームを追加する

ピザを新規登録、削除できるようにします。

新しいピザを作成するフォームを追加する

  1. Pages\PizzaList.cshtml.cs を開き、次のプロパティを PizzaListModel クラスに追加します。
Pages\PizzaList.cshtml.cs
[BindProperty]
public Pizza NewPizza { get; set; } = default!;

BindProperty は Razor ページのプロパティをバインドするために使う。
2. HTTP POST のページハンドラーを追加します。

Pages\PizzaList.cshtml.cs
public IActionResult OnPost()
{
    if (!ModelState.IsValid || NewPizza == null)
    {
        return Page();
    }

    _service.AddPizza(NewPizza);

    return RedirectToAction("Get");
}
  1. Ctrl + R でリビルドします。

表示側にもコードを追加します。

  1. Pages\PizzaList.cshtml を開き、<!-- New Pizza form will go here --> を次のコードに置き換えます。
Pages\PizzaList.cshtml
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
    <label asp-for="NewPizza.Name" class="control-label"></label>
    <input asp-for="NewPizza.Name" class="form-control" />
    <span asp-validation-for="NewPizza.Name" class="text-danger"></span>
</div>
<div class="form-group">
    <label asp-for="NewPizza.Size" class="control-label"></label>
    <select asp-for="NewPizza.Size" class="form-control" id="PizzaSize">
        <option value="">-- Select Size --</option>
        <option value="Small">Small</option>
        <option value="Medium">Medium</option>
        <option value="Large">Large</option>
    </select>
    <span asp-validation-for="NewPizza.Size" class="text-danger"></span>
</div>
<div class="form-group form-check">
    <label class="form-check-label">
        <input class="form-check-input" asp-for="NewPizza.IsGlutenFree" /> @Html.DisplayNameFor(model => model.NewPizza.IsGlutenFree)
    </label>
</div>
<div class="form-group">
    <label asp-for="NewPizza.Price" class="control-label"></label>
    <input asp-for="NewPizza.Price" class="form-control" />
    <span asp-validation-for="NewPizza.Price" class="text-danger"></span>
</div>
<div class="form-group">
    <input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
  • asp-validation-summary でモデル全体の検証エラーを表示
  • asp-validation-for で各フォームのフィールドの検証エラーを表示
  1. ページの下部に次のコードを追加します。
Pages\PizzaList.cshtml
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}

このコードでクライアント側の検証スクリプトがページに挿入されます。
3. ファイルを保存して、ブラウザを更新すると、ページにフォームが追加されます。

4. 新しいピザを追加すると、一覧に追加されます。

ピザを削除するページ ハンドラーを追加する

ボタンはあるけれど、機能しない削除ボタンを機能するようにします。

  1. Pages\PizzaList.cshtml.cs に以下のメソッドを追加します。
Pages\PizzaList.cshtml.cs
public IActionResult OnPostDelete(int id)
{
    _service.DeletePizza(id);

    return RedirectToAction("Get");
}
  • 削除ボタンの asp-page-handler 属性に Delete が設定するため、ページがこのメソッドを使用することを認識します。
  • asp-route-id によって、URL内の id パラメータにバインドされます。
  1. 保存して、削除ボタンをクリックし、削除されることを確認します。

あとがき

View の html と Controller がページ単位で強制的にセットになった感じですかね?
ハンドラーがいろいろ用意されているようで面白いですね。
引き続き勉強しまーす📝

Discussion