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

ASP.NET Core

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のCreateとEditメソッドの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

<h1>Delete</h1>

<h3>Are you sure you want to delete this?</h3>
<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

<h1>Details</h1>

<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

<h1>Edit</h1>

<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>

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

Entity Framework Core コードファーストを実行

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

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

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

コードファーストとは簡単に言うと、コード(モデル)の構造に合わせて実際のテーブル構造を作成したり変更ができる手法です。

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

では早速実行してみましょう。「ツール」→「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]:文字数の範囲を指定。「興行収入」では1~1000の値が入力可能。
  • [RegularExpression]:正規表現を指定。正規表現についての解説は省きますが、「レイティング」では「最初の文字が大文字でその後は半角英数字+記号が使用できる」という意味になります。
  • [DataType]:厳密には検証属性ではなく書式属性で、データベースの型よりも具体的なデータ型を指定するために使用します。「公開日」はデータベースでは日付の他に時間の値を持っていますが、画面に表示するのは日付のみのためDateを指定しています。

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

System.ComponentModel.DataAnnotations 名前空間
ASP.NET MVC および ASP.NET データ コントロールのメタデータを定義するために使用される属性クラスが用意されています。

ちなみに検証属性は以下のように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のチュートリアルを丁寧に解説するシリーズは終了となります。

最後まで読んでいただきありがとうございました。

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

.NET 6全般に関する理解を深めたい方

著:増田 智明
¥3,366 (2022/09/28 15:53時点 | Amazon調べ)

ASP.NET Core Web APIについて基礎から学びたい方

コメント

  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

      • 通りすがり より:

        ひらひら様

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

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