RangeAttributeにてDateをモデル検証すると、クライアントサイド検証でエラーになる現象とその対応

前回の続きです。

mrgchr.hatenablog.com

環境

RangeAttributeによる検証をDateに対して行う

RangeAttribute(以下Range)は、プロパティにバインドされる値が指定した最小値と最大値の間にあるか検証するためのものです。
例えば、次の例ではReviewScoreにバインドされる値が1以上10以下であることを意図しています。

[Range(1, 10)]
public int ReviewScore {get; set;}

Rangeは整数や浮動小数点以外にも任意の型を指定することができます。
よく使われるのは日付(Date)に対してカレンダーの最小日(最古日)と最大日(最新日)を指定するというパターンです。
C#には標準のDate型は無いのでDateTime型で代用します。

// Models/RangeDateDemoVM.cs
public class RangeDateDemoVM
{
  [DataType(DataType.Date)]
  [Range(typeof(DateTime), "2019-09-09", "2019-09-19")]
  public DateTime DemoDate { get; set; }
}
@page
@model WebApplication2.Pages.RangeDateDemoModel
@{
  ViewData["Title"] = "RangeDateDemo";
}

<h2>@ViewData["Title"].</h2>
<h3>@Model.Message</h3>

<form method="post">
  <div asp-validation-summary="All"></div>
  @Html.EditorFor(m => m.RangeDateDemo)
  <input type="submit" value="Submit" />
</form>
public class RangeDateDemoModel : PageModel
{
  public string Message { get; set; }

  [BindProperty]
  public RangeDateDemoVM RangeDateDemo { get; set; }

  public void OnGet()
  {
    Message = "";
  }

  public void OnPost()
  {
    if (!ModelState.IsValid)
    {
      Message = "Server Side Validation Error";
    }
    else
    {
      Message = @$"DemoDate: {RangeDateDemo.DemoDate} is Created.";
    }
  }
}

上記の例では、サーバーサイドの検証は問題なく動作します。
クライアントサイド検証は行っていないことにご注意ください。

下記の図のように、Range範囲内の値を指定するとサーバーサイド検証に合格します。 f:id:mrgchr:20190930203823p:plain

そして、Range範囲外の値を指定するとエラー(サーバーサイド検証による)になります。

f:id:mrgchr:20190930203834p:plain

エラーメッセージが時間分秒まで見えるのがイヤというときは下記のようにすれば変更できます。

[DataType(DataType.Date)]
[Range(typeof(DateTime), "2019-09-09", "2019-09-19"
    , ErrorMessage = "Value for {0} must be between {1:d} and {2:d}")]
public DateTime DemoDate { get; set; }

f:id:mrgchr:20190930204549p:plain

ちなみに、サーバーサイドエラーが"YYYY/MM/dd"表示でブラウザーの入力側では"MM/dd/YYYY"表示なのは、日本語OS上で英語(米国)版Firefoxだからです。

クライアントサイド検証を有効にする

ここで、クライアントサイド検証を有効にします。

<form method="post">
  <div asp-validation-summary="All"></div>
  @Html.EditorFor(m => m.RangeDateDemo)
  <input type="submit" value="Submit" />
</form>

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

そうすると、Rangeの範囲内の値であってもクライアントサイドエラーになります。
(Range範囲外も当然エラーになります) f:id:mrgchr:20190930210246p:plain

生成されたHTMLは下記のとおりです(成形済み)。

<input class="text-box single-line input-validation-error"
  data-val="true"
  data-val-range="Value for DemoDate must be between 2019/09/09 and 2019/09/19"
  data-val-range-max="09/19/2019 00:00:00"
  data-val-range-min="09/09/2019 00:00:00"
  data-val-required="The DemoDate field is required."
  id="RangeDateDemo_DemoDate"
  name="RangeDateDemo.DemoDate"
  type="date"
  value=""
  aria-describedby="RangeDateDemo_DemoDate-error"
  aria-invalid="true">

これはMVC5の時にも存在していた振る舞いで、どうやら数以外のRangeは文字列として比較していることで、それに由来している動作のようです。
直す方法は、クライアントサイドのバリデーションロジックを上書きする方法が手っ取り早いかと思われます。

修正する

jQueryのvalidatorを修正する必要があります。今回は伝えやすさの都合からPages/_Shared/ValidationScriptsPartial.cshtmlに直接書きましたが、本来は別ファイルにして読み込んだ方がよいかと思います。

stackoverflow.com

<!-- Pages/_Shared/ValidationScriptsPartial.cshtml -->
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
@*ここから追加*@
<script>
  //original: https://stackoverflow.com/questions/52543413/datetime-client-side-validation-fails-due-to-formatting
  $.validator.methods.range = function (value, element, param) {
    if ($(element).attr('type') == 'date') {
      var min = $(element).attr('data-val-range-min');
      var max = $(element).attr('data-val-range-max');
      var date = new Date(value).getTime();
      var minDate = new Date(min).getTime() || 0;
      var maxDate = new Date(max).getTime() || 8640000000000000;
      return this.optional(element) || (date >= minDate && date <= maxDate);
    }
    // use the default method
    return this.optional(element) || (value >= param[0] && value <= param[1]);
  };
</script>
@*ここまで追加*@

これにより、先ほどまでエラーになっていたクライアントサイド検証も合格になります(画像省略)。

mrgchr.hatenablog.com