記事内に広告が含まれています

ASP.NET Core MVCのチュートリアルを丁寧にやってみた④(プロパティと検証属性の追加)

C#

ASP.NET Core MVCの公式チュートリアルを丁寧に解説する記事の第4回(最終回)です。

第3回では、タグヘルパーやPOSTリクエストの処理について確認し、最後に検索機能の追加を行いました。

最終回となる第4回では、前半でプロパティ(列)の追加を行い、後半では検証属性の追加を行っていきます。

公式チュートリアルのパート8~10に対応しています。

パート 8、ASP.NET Core MVC アプリへの新しいフィールドの追加
ASP.NET Core MVC のチュートリアル シリーズのパート 8。

Movieモデルに新しいプロパティの追加

まずはMovie.csに新たにRating(レイティング)というプロパティ(列)を追加してみましょう。
ちなみにレイティングとは↓のような分類のことです。

また、それぞれのプロパティ名が日本語で画面に表示されるようにDisplayName属性を追加します。

using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

namespace MvcMovie.Models
{
    public class Movie
    {
        public int Id { get; set; }

        [DisplayName("タイトル")]
        public string? Title { get; set; }

        [DisplayName("ジャンル")]
        public string? Genre { get; set; }

        [DisplayName("興行収入(億円)")]
        public decimal Revenue { get; set; }

        [DisplayName("公開日")]
        [DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }

        [DisplayName("レイティング")]
        public string? Rating { get; set; }
    }
}

新たなプロパティを追加したので、まずはMoviesControllerのCreateEditメソッドのBind属性を修正します。

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("Id,Title,Genre,Revenue,ReleaseDate,Rating")] Movie movie)

<略>

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("Id,Title,Genre,Revenue,ReleaseDate,Rating")] Movie movie)

次にViews/Movies/index.cshtmlにRatingプロパティを追加します。

<略>

<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Movies[0].Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies[0].ReleaseDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies[0].Genre)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies[0].Revenue)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies[0].Rating)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Movies)
        {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ReleaseDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Genre)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Revenue)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Rating)
            </td>
            <td>
                <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
                <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
                <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
            </td>
        </tr>
        }
    </tbody>
</table>

その他のテンプレートファイルにも同様にRatingを追加していきます。

Create.cshtml

<略>

<div class="row">
    <div class="col-md-4">
        <form asp-action="Create">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Title" class="control-label"></label>
                <input asp-for="Title" class="form-control" />
                <span asp-validation-for="Title" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Genre" class="control-label"></label>
                <input asp-for="Genre" class="form-control" />
                <span asp-validation-for="Genre" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Revenue" class="control-label"></label>
                <input asp-for="Revenue" class="form-control" />
                <span asp-validation-for="Revenue" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="ReleaseDate" class="control-label"></label>
                <input asp-for="ReleaseDate" class="form-control" />
                <span asp-validation-for="ReleaseDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Rating" class="control-label"></label>
                <input asp-for="Rating" class="form-control" />
                <span asp-validation-for="Rating" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<略>

Delete.cshtml

<略>

<div>
    <h4>Movie</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Title)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Title)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Genre)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Genre)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Revenue)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Revenue)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.ReleaseDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.ReleaseDate)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Rating)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Rating)
        </dd>
    </dl>

<略>

Details.cshtml

<略>

<div>
    <h4>Movie</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Title)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Title)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Genre)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Genre)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Revenue)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Revenue)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.ReleaseDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.ReleaseDate)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Rating)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Rating)
        </dd>
    </dl>
</div>

<略>

Edit.cshtml

<略>

<h4>Movie</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Edit">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="Id" />
            <div class="form-group">
                <label asp-for="Title" class="control-label"></label>
                <input asp-for="Title" class="form-control" />
                <span asp-validation-for="Title" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Genre" class="control-label"></label>
                <input asp-for="Genre" class="form-control" />
                <span asp-validation-for="Genre" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Revenue" class="control-label"></label>
                <input asp-for="Revenue" class="form-control" />
                <span asp-validation-for="Revenue" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="ReleaseDate" class="control-label"></label>
                <input asp-for="ReleaseDate" class="form-control" />
                <span asp-validation-for="ReleaseDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Rating" class="control-label"></label>
                <input asp-for="Rating" class="form-control" />
                <span asp-validation-for="Rating" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<略>

以上でプロパティ追加に伴う修正は完了です。

コードファーストマイグレーションでテーブルを実行

このままアプリをデバッグ実行すると、「’Rating’という名前の列がテーブルに存在しない」というエラーが出てしまいます。

解決するためには、既存のテーブル構造をMovieモデルの構造に合わせる必要があります。

いくつか方法はありますが、今回は既に登録済みのデータを削除したくないため、コードファーストマイグレーション(Code First Migrations)という手法でテーブルに新たな列の追加を行います。

コードファーストマイグレーションとは、コード(モデル)の構造に合わせて実際のテーブル構造を作成したり変更ができる手法です。

通常であればCreate Table文を実行してテーブルを作成する必要がありますが、コードファーストを使えばC#のコードを書くだけで自動でテーブルを作成してくれます。

では早速実行してみましょう。※第2回で実行したマイグレーションと同じ手順です。

「ツール」→「NuGetパッケージマネージャー」→「パッケージマネージャーコンソール」でコンソールを開き、以下のコマンドを実行します。

Add-Migration Rating
Update-Database

※「Rating」という名前は任意です。変更内容が一目でわかるような名前を付けるといいでしょう。

実行すると「Migrations」フォルダ内に「<日付と時刻>_Rating.cs」というファイルが作成され、テーブルに「Rating」という列が追加されます。

「SQL Server オブジェクトエクスプローラー」からMovieテーブルを開いて確認してみましょう。

なお、このままではRating列がすべてNULLになっていてデバッグ実行するとエラーになってしまうので、Rating列に以下のように適当な値を入力しておきます。

ここまでできたら、デバッグ実行してみてください。

「レイティング」列が表示されていれば成功です。

検証属性の追加

ここからは、Movieモデルに新しい検証属性を追加します。以下のように書き換えてください。

using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

namespace MvcMovie.Models
{
    public class Movie
    {
        public int Id { get; set; }

        [DisplayName("タイトル")]
        [StringLength(60, MinimumLength = 3)]
        [Required]
        public string? Title { get; set; }

        [DisplayName("ジャンル")]
        [StringLength(30)]
        [Required]
        public string? Genre { get; set; }

        [DisplayName("興行収入(億円)")]
        [Range(1, 1000)]
        public decimal Revenue { get; set; }

        [DisplayName("公開日")]
        [DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }

        [DisplayName("レイティング")]
        [RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$")]
        [StringLength(5)]
        [Required]
        public string? Rating { get; set; }
    }
}

それぞれの検証属性について簡単に説明します。

  • [Required]:入力必須。空白はNG。
  • [StringLength]:文字数の上限を指定。MinimumLengthで最小文字数を指定可能。
  • [Range]:値の範囲を指定。[Range(1, 1000)]の場合は1~1000の値が入力可能。
  • [RegularExpression]:正規表現を指定。正規表現についての解説は省きますが、「レイティング」では「最初の文字が大文字でその後は半角英数字+記号が使用できる」という意味になります。
  • [DataType]:厳密には検証属性ではなく書式属性で、データベースの型よりも具体的なデータ型を指定するために使用します。「公開日」はデータベースでは日付の他に時間の値を持っていますが、画面に表示するのは日付のみのためDateを指定しています。

ここで紹介したのは一部で、この他にも多くの検証属性が用意されています。詳細は下記ドキュメントを参照してください。

System.ComponentModel.DataAnnotations 名前空間

ちなみに検証属性は以下のように1行で記述することもできますが、可読性はあまりよくないので分けて書いた方がいいでしょう。

[DisplayName("ジャンル"), StringLength(30), Required]
public string Genre { get; set; }

それではアプリを実行して「新規作成」からCreate画面を開き、わざとエラーになるような入力をして検証属性の動作を確認してみましょう。

正しく検証できていることが確認できました。これらはブラウザ(jQuery)で検証が行われ、エラーがなくなるまでサーバ側にデータを送信することができません。

Modelで検証属性を指定すれば、ControllerやView(Create.cshtmlなど)を変更しなくてもすべての画面で検証が適用できる点が便利です。

今回であれば、Create NewだけでなくEdit画面でも同様の検証が適用されています。

なお、エラーメッセージを日本語化したい場合は次のようにErrorMessageプロパティを指定すればOKです。

[DisplayName("タイトル")]
[StringLength(60, MinimumLength = 3, ErrorMessage = "3文字以上、60文字以内で入力してください。")]
[Required(ErrorMessage = "入力必須です。")]
public string Title { get; set; }

おわりに

前半ではプロパティの追加とコードファーストマイグレーションによるDBの更新、後半では検証属性の追加について確認しました。

以上で、全4回のASP․NET Core MVCのチュートリアルを丁寧に解説するシリーズは終了となります。

最後までお疲れさまでした。

今後さらに理解を深めたい方に向けて、おすすめ書籍や当ブログの記事をご紹介します。

Azure上にデプロイしてアプリを公開したい

AzureのApp Serviceというサービスを使えば、作成したASP.NET Coreアプリをお手軽に公開することができます。

下記記事ではアプリをデプロイする手順を解説しています。

ASP.NET Core の基本的な仕組みを知りたい

下記記事では、Program.csに記載されている内容を中心に、ASP.NET Core の裏側の動きについて解説しています。

ASP.NET Core MVC以外のフレームワークも学んでみたい

これからASP.NET Coreで本格的にアプリケーションを作るなら、近年需要が高まっており、Microsoftが開発に力を入れているBlazorを学ぶのもおすすめです。

下記記事では、BlazorでCRUD機能を持つアプリを作る方法を解説しています。

コメント

  1. 通りすがり より:

    セクション「検証属性の追加」に記載のMovieモデルの状態で、Createアクションにてすべて空白で登録しようとしたとき、「~ is required.」という必須項目のエラーメッセージが出ます。
    「Revenue」と「ReleaseDate」は必須ではないにもかかわらずです。
    これってどういうことなんでしょう?
    ちなみに、javascriptを無効にして実行してみるとメッセージは「The value ~ is invalid.」に変わりました。
    つまり、JSが効いている状態だと、入力が必須ではない項目が用意できない気がするんですけど…

    当方の環境はVS2022です。

    • ひらひら より:

      コメントありがとうございます。
      Required属性については私も正しく理解していなかったので、公式ドキュメントをもとに仕様を整理してみました。
      ※環境はVS2019+.NET5で、JavaScriptが有効の場合です。

      Null許容型(stringなどの参照型)
      ・Required属性を指定しなければ、空白でもエラーにならない。

      非Null許容型(int, decimal, DateTimeなどの値型)
      ・裏でRequired属性が指定されているため、明示しなくても自動的に必須入力項目となる。
       (ただしエラーメッセージを任意のものに変更したい場合は、Required属性でメッセージを指定する必要がある。)
      ・必須入力項目にしたくない場合は、Null許容型(int?など)に変更する。
       例)public decimal? Revenue { get; set; }

      ※Null(空白)は許容したくないが、空文字(スペース)は許容したいという場合は、[Required(AllowEmptyStrings = true)]と指定する。

      以上の仕様を考慮することで、JavaScript有効時でも任意の入力項目を用意することができます。

      【参考】「null 非許容の参照型と [Required] 属性」
      https://docs.microsoft.com/ja-jp/aspnet/core/mvc/models/validation?view=aspnetcore-5.0#non-nullable-reference-types-and-required-attribute

      • 通りすがり より:

        ひらひら様

        公式ドキュメントに基づき解説してくださり、ありがとうございました。
        よく理解できました。
        今まで公式ドキュメントをあまり読んだことがなかったので、これからは読むように心がけたいと思います。

  2. いち より:

    1回目から読ませていただきました。
    大変分かりやすく勉強になりました。
    ありがとうございました。

タイトルとURLをコピーしました