ASP.NET Core 3.0 Razor Pages 事始め(10) - 検証機能の追加


## 記事目次

1. [ASP.NET Core 3.0 Razor Pages 事始め(1) - はじめてのRazor Pagesアプリケーション](https://qiita.com/gushwell/items/6d8b801923b639d9ea2e)
2. [ASP.NET Core 3.0 Razor Pages 事始め(2) - スキャフォールディングとDBマイグレーション](https://qiita.com/gushwell/items/7eb76edf7006c0f02be4)
3. [ASP.NET Core 3.0 Razor Pages 事始め(3) - マイグレーションのやり直しとURLルーティング](https://qiita.com/gushwell/items/7ebe70f0d6adfd10ae12)
4. [ASP.NET Core 3.0 Razor Pages 事始め(4) - ページモデルとページハンドラ](https://qiita.com/gushwell/items/1deac26aaed945cc8631)
5. [ASP.NET Core 3.0 Razor Pages 事始め(5) - Postページハンドラとタグヘルパー](https://qiita.com/gushwell/items/daf901d4d76301dadea7)
6. [ASP.NET Core 3.0 Razor Pages 事始め(6) - データベースに初期値を設定する](https://qiita.com/gushwell/items/f7be44e668a6e168fc61)
7. [ASP.NET Core 3.0 Razor Pages 事始め(7) - Viewの変更とコンカレンシー例外処理](https://qiita.com/gushwell/items/913e8e95a0a79949af3c)
8. [ASP.NET Core 3.0 Razor Pages 事始め(8) - 検索機能の追加](https://qiita.com/gushwell/items/134930aba1d181100547)
9. [ASP.NET Core 3.0 Razor Pages 事始め(9) - ページに新しいフィールドを追加する](https://qiita.com/gushwell/items/a330e592f76efda13c42)
10. [ASP.NET Core 3.0 Razor Pages 事始め(10) - 検証機能の追加](https://qiita.com/gushwell/items/12e180ef129ea6a05757) <-- この記事


---

[ASP.NET Core 3.0 Razor Pages 事始め(9)](https://qiita.com/gushwell/items/a330e592f76efda13c42)の続きです。

いよいよ最後となりました。

今回は公式チュートリアルの[ASP.NET Core Razor ページに検証を追加する](https://docs.microsoft.com/ja-jp/aspnet/core/tutorials/razor-pages/validation?view=aspnetcore-3.0&tabs=visual-studio)に沿って進めていこうと思います。

チュートリアルの最後は、ASP.NET Core Razor ページに検証を追加します。

## クライアント側の検証

Movieクラスに、検証属性を追加します。

```c#
using System;
using System.ComponentModel.DataAnnotations;

namespace RazorPagesMovie.Models
{
    public class Movie
    {
       //public string Title { get; set; }
        public int ID { get; set; }

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


        [DataType(DataType.Date)]
        [Display(Name = "リリース日")]
        public DateTime ReleaseDate { get; set; }

        [RegularExpression(@"^[A-Z]+[a-zA-Z""'\s-]*$")]
        [Required]
        [Display(Name = "ジャンル")]
        public string Genre { get; set; }

        [Range(1, 10000)]
        [Display(Name = "価格")]
        [DisplayFormat(DataFormatString = "{0:#,0}", ApplyFormatInEditMode = true)]
        public decimal Price { get; set; }

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

    }
}


```

`Required` 属性は、必須項目であることを示します。

`StringLength`属性は、文字列の最大長を示します。`MinimumLength`を使うことで、最小長を指定することもできます。

`Range`属性は、指定した範囲で値を制限します。

`RegularExpression`は、ユーザが入力できる文字を正規表現で指定します。


次に、クライアント検証を有効にするために以下の記述を Create.cshtml と Edit.cshtml の最後尾に追加します。

```
@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
```

ちなみに、`_ValidationScriptsPartial.cshtml` は以下の通りです。

```html
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
```


ビルドして、F5キーで実行してみます。
新規作成ページで、無効な値を入力してみます。無効な値を入れた場合は、他の項目に移った段階で検証が走り、エラーメッセージが表示されます。
この時、サーバーへは処理は移っていません。

![スクリーンショット 2019-11-20 21.22.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29415/107e7ee8-e6bb-3d03-490e-7c893e810bc0.png "スクリーンショット 2019-11-20 21.22.08.png")


ここで注目すべきは、Movieクラスで指定した検証指定が、２つのページで同じように利用されているという事です。ページごとに検証ルーチンを書く必要がないのは楽だし、統一が取れるのでとてもいいですね。


でも、メッセージが英語です(T T)

```c#
   [Required(ErrorMessage ="{0}は、省略することはできません")]
```

とか、書けばとりあえずはOKです。


![スクリーンショット 2019-11-20 21.25.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29415/1cb96298-37d7-a7b3-f695-5bb6cfbe14aa.png "スクリーンショット 2019-11-20 21.25.26.png")

だけど、すべての検証属性にこれを付けるのは、規模の大きなプログラムではあまりにも大変です。あとでやり方を調べてみます。


## サーバー側の検証

クライアント検証が有効になっていないと、入力されたデータは、サーバー側で検証されるようになっています。

Create.cshtml.csの`OnPostAsync`メソッドには、

```c#
    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }
        ...
```

という記述があります。検証エラーが発生すると、IsValidプロパティがfalseになります。
この時は、`return Page();`で戻っています。

わざわざ検証用の処理を呼び出す必要はないということですね。

試しに、`Create.cshtml` の

```c#
@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
```

を削除します。これでクライアント検証が無効になるはずです。

ビルドしなおし、ブラウザを閉じてから、再度実行してみます。

確かにサーバー側で検証が行われ、ブラウザには先ほどと同様に検証エラーが表示されます。


## `DataType`属性

ちなみに `DataType`属性は、厳密には検証用属性ではありません。Viewに対してデータの書式設定のヒントを提供します。

例えば、

```c#
    [DataType(DataType.Date)]
    [Display(Name = "リリース日")]
    public DateTime ReleaseDate { get; set; }
```

においては、以下のようなHTMLが生成されます。

```html
<input class="form-control input-validation-error" type="date" data-val="true" data-val-required="The &#x30EA;&#x30EA;&#x30FC;&#x30B9;&#x65E5; field is required." id="Movie_ReleaseDate" name="Movie.ReleaseDate" value="" />
```

`type="data"`になっている点に注目です。

この属性が無いと、

`type="datetime-local"` 

となり、利用される入力コンポーネントが時刻も含んだものになります。

話がそれますが、日付の書式は、サーバーの`CultureInfo`に基づき、既定の書式に従って表示されるようです。

@Html.DisplayForで日付を表示した場合、

```
1989/02/12 
```

```c#

[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}")]
```

と属性を追加すれば、

```
1989-02-12 
```

と表示形式を変更できます。

属性は、複数の属性を纏めて１行に書くことも可能です。
以下にその例を示します。


```c#
 [RegularExpression(@"^[A-Z]+[a-zA-Z""'\s-]*$"), Required, StringLength(30)]
```

---

これで、チュートリアルは終了です。公式ページのチュートリアルだけあって、RzaorPagesの基本が押さえられる内容になっていると思います。

基本は抑えられたということで、これからは、チュートリアルをやっていて、疑問に思った点やさらに詳しく知りたいと思った点を調べていこうと思います。

**この記事で作成したプログラムのソースコードは[GitHub](https://github.com/gushwell/ASPNetCoreRazorPagesTutorial)で公開しています。**

##  最後に: ASP.NET Core Razor Pagesを使った感想

ASP.NET Core Razor Pagesの説明をはじめて読んだ時には、デスクトップアプリ（Windows Forms）の開発をやっていた頃を思い出し、コードが分離されずにメンテナンスの悪い構造になってしまうのではないかと、不安を覚えました。


しかし、触ってみた感触だと、心配するようなことは無いかなというのが僕の感想です。MVCの欠点が消えて逆にいい感じです。

MVCの場合も、Modelをどうすべきかはアプリケーション設計者に任されていたわけです。
それを考えると、ViewとControllerが、Page(.cshtml)とPageModel(.cshtml.cs)に置き換わっただけです。

PageとPageModelは、常に1対1の関係ですから、コードを書いている時に、ControllerとViewとModel(ViewModel)の間をいったりきたりする必要がないので、とても快適です。

Webアプリの性質上、ページモデルに大量のメソッドを定義するようなことも無いと思います。
また、Controllerをどう分割すべきかという悩ましい問題からも解放されます。
Pagesフォルダの中に、サブフォルダを作成すれば、見通しも良くなりそうです。


僕の理解は以下のような感じです。

MVC | Razor Pages
--------|---------------
View (chsthm)   |  Page (chsthm)
Controller | PageModel  (cshtml.cs)
Actioneメソッド | PageModel内の Razorページハンドラー
ViewModel | PageModel ただし完全な置き換えではない。独自にViewModelを定義するケースもあり。
Model   | Model (多くの場合は、EFのEntityクラス)