ASP.NET MVC で formValidation を使う

formValidation

ASP.NET MVC のプロジェクトを作ると、デフォルトで jQuery ValidationjQuery Unobtrusive Validation がインストールされます。
しかし、これ、どうもイケてないと思いませんか?
bootstrap も一緒にインストールされるわけですが、デフォルトでは、エラーのあるフィールドを赤くすることもできません(少々 JavaScript を書けば実現できます)。

というわけで今回は、jQuery Validation に代わって、高機能なバリデーション ライブラリである formValidation を使ってみることにしました。
有料ですが、多数のバリデーターを持ち、足りなければバリデーターを自作することもできます。
また、bootstrap 以外の、あるいは、bootstrap を拡張するような、様々なライブラリと組み合わせることも可能です。

まずは一度、公式サイトのデモを触ってみてください。

なお、今回のサンプルコードは以下のリポジトリにあります。適宜参照しながらお読みください。github.com

ASP.NET で使う準備

まず、jQuery Validation と jQuery Unotrusive Validation は NuGet から削除してしまいましょう。併せて BundleConfig.cs からも消しておきましょう。
jQuery 本体までアンインストールしないように注意してください。

次に、プロジェクト ディレクトリに formValidation のライブラリをコピーします。今回は lib というディレクトリを作り、その下に置きました。
続いて、_Layout.cshtml ファイルを開き、formValidation のファイルへの参照を追加します。
とりあえずこんな感じで。

_Layout.cshtml

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0">

  <title>@ViewBag.Title - マイ ASP.NET アプリケーション</title>

  @Styles.Render("~/Content/css")
  <link rel="stylesheet" type="text/css" href="@Url.Content("~/lib/formValidation/dist/css/formValidation.min.css")"/>

  @Scripts.Render("~/bundles/modernizr")
</head>
<body>
  <div class="navbar navbar-inverse navbar-fixed-top">
    <div class="container">
      <div class="navbar-header">
        <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
        </button>
        @Html.ActionLink("アプリケーション名", "Index", "Home", new { area = "" }, new { @class = "navbar-brand" })
      </div>
      <div class="navbar-collapse collapse">
        <ul class="nav navbar-nav">
          <li>@Html.ActionLink("ホーム", "Index", "Home")</li>
          <li>@Html.ActionLink("詳細", "About", "Home")</li>
          <li>@Html.ActionLink("連絡先", "Contact", "Home")</li>
        </ul>
      </div>
    </div>
  </div>
  <div class="container body-content">
    @RenderBody()
    <hr />
    <footer>
      <p>&copy; @DateTime.Now.Year - マイ ASP.NET アプリケーション</p>
    </footer>
  </div>

  @Scripts.Render("~/bundles/jquery")
  @Scripts.Render("~/bundles/bootstrap")

  <script type="text/javascript" src="@Url.Content("~/lib/formValidation/dist/js/formValidation.min.js")"></script>
  <script type="text/javascript" src="@Url.Content("~/lib/formValidation/dist/js/framework/bootstrap.min.js")"></script>
  <script type="text/javascript" src="@Url.Content("~/lib/formValidation/dist/js/language/ja_JP.js")"></script>

  @RenderSection("scripts", required: false)
</body>
</html>

css を 1 つと、js を 3 つ参照しています。
js の内訳は、

  • コア ライブラリ
  • bootstrap 用ファイル
  • 日本語言語ファイル

です。
bootstrap 以外にも様々な UI ライブラリに対応しているので、bootstrap 依存部分はこのように別ファイルに切り出されているわけですね。

フォームとアクションの追加

てきとーに作りましょう。てきとーにね。

PostModel.cs

public class PostModel
{
    [Required]
    public string Name { get; set; }

    public string Description { get; set; }
}

HomeController.cs

public ActionResult Post()
{
    return this.View();
}


[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Post(
    [Required] PostModel model)
{
    if (!this.ModelState.IsValid)
    {
        return this.View(model);
    }

    return this.RedirectToAction("Post");
}

Post.cshtml

@model PostModel

@{
  ViewBag.Title = "title";
}

@using (Html.BeginForm(null, null, FormMethod.Post, new { @class = "form-horizontal" }))
{
  @Html.AntiForgeryToken()

  <div class="form-group">
    @Html.LabelFor(x => x.Name, new { @class = "col-sm-3 control-label" })
    <div class="col-sm-3">
      @Html.TextBoxFor(x => x.Name, new { @class = "form-control", required = "required" })
    </div>
  </div>

  <div class="form-group">
    @Html.LabelFor(x => x.Description, new { @class = "col-sm-3 control-label" })
    <div class="col-sm-3">
      @Html.TextBoxFor(x => x.Description, new { @class = "form-control" })
    </div>
  </div>

  <div class="form-group">
    <div class="col-sm-9 col-sm-offset-3">
      <button type="submit" class="btn btn-primary">決定</button>
    </div>
  </div>
}

はい、てきとー。

バリデーションの追加

この時点では何のバリデーションもしていません。
required 属性による、ブラウザー組み込みの不格好な必須チェックがあるだけです。

ここからが本番。

formValidation の設定は、基本的に JavaScript で行います。

なお、今回の基本方針ですが、共通化できる部分は基本的に _Layout.cshtml に書き、各フォーム固有のことだけを各ページに書くということにしたいと思います。
というわけで、まずは共通部分。

_Layout.cshtml

<script type="text/javascript" src="@Url.Content("~/lib/formValidation/dist/js/formValidation.min.js")"></script>
<script type="text/javascript" src="@Url.Content("~/lib/formValidation/dist/js/framework/bootstrap.min.js")"></script>
<script type="text/javascript" src="@Url.Content("~/lib/formValidation/dist/js/language/ja_JP.js")"></script>

<script type="text/javascript">
  $(function () {
    $("form").formValidation({
      framework: "bootstrap",
      locale: "ja_JP",
      err: {
        container: function ($field, validator) {
          return $field.closest(".form-group").find(".validation-message");
        }
      }
    });
  });
</script>

@RenderSection("scripts", required: false)

framework は使用する UI ライブラリの設定。今回は bootstrap です。
locale はエラーメッセージに日本語を使うということ。
err は後程説明しますが、エラーメッセージを表示する領域の指定です。今回は bootstrap の Horizontal Form を使っているので、入力欄の右側に表示するようにしました。

それから、フォームページにも少し手を入れます。

<div class="form-group">
  @Html.LabelFor(x => x.Name, new { @class = "col-sm-3 control-label" })
  <div class="col-sm-3">
    @Html.TextBoxFor(x => x.Name, new { @class = "form-control", required = "required" })
  </div>
  <div class="validation-message col-sm-6"></div>
</div>

validation-message というクラスを持つ div 要素を追加しました。これがエラーメッセージの表示エリアになります。
通常、ASP.NET でエラーメッセージを表示するには ValidationMessageFor メソッドを使いますが、今回は formValidation との相性が良くないので使っていません。

ともかく、ここまで書けば、必須入力エラーについては、テキストボックスの右側に表示されるようになったはずです。よかったら試してみてください。

f:id:aetos382:20150503164822p:plain

ちゃんと、エラーメッセージまで含めたフィールド全体が赤くなっていますね。
送信ボタンも無効化されています。

サーバーサイドのエラーへの対応

さて、formValidation は JavaScript ライブラリなので、クライアントサイドの検証しかしてくれません。
ASP.NET MVC の場合、コントローラーのアクション メソッドでエラーが起きる場合もあります。

というわけで、エラーを起こすコードを書いてみましょう。

HomeController.cs

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Post(
    [Required] PostModel model)
{
    if (!this.ModelState.IsValid)
    {
        return this.View(model);
    }

    if (model.Name.All(char.IsUpper))
    {
        this.ModelState.AddModelError("Name", "全部大文字はダメです。");
        return this.View(model);
    }

    return this.RedirectToAction("Post");
}

Name にすべて大文字で入力すると怒られるというよくわからない仕様にしました。
これだけでは、View の側が何も対応していませんので、エラーが起きたことがわかりません。
ValidationMessageFor メソッドを使っていれば、エラーメッセージは簡単に表示できるのですが、今回はそれは無し。

ではどうするのか。
要は ValidationMessageFor がやっているようなことを自分でやればよいわけです。

Post.cshtml

@Html.TextBoxFor(x => x.Name, new { @class = "form-control", required = "required", data_fv_blank = "true" })

まず、テキストボックスに data-fv-blank="true" 属性を追加します(属性名のハイフンは、代わりにアンダースコアで書くってことはご存知ですよね)。
data-fv-validator という属性は、この要素に対して validator で指定したバリデーターを有効化するという意味です。
つまり、このテキストボックスに対して、blank バリデーターを設定しているわけです。
blank というバリデーターは formValidation の組み込みバリデーターの一覧には載っていませんが、使えます。
ソースコード(formValidation.js)を blank で検索すると、このように書いてあります。

Placeholder validator that can be used to display a custom validation message returned from the server.
Example:

(1) a "blank" validator is applied to an input field.
(2) data is entered via the UI that is unable to be validated client-side.
(3) server returns a 400 with JSON data that contains the field that failed validation and an associated message.
(4) ajax 400 call handler does the following:

bv.updateMessage(field, 'blank', errorMessage);
bv.updateStatus(field, 'INVALID');

サーバーから返されたカスタム メッセージを表示するためのプレースホルダ―とのことですので、今回のような用途にうってつけです。

これに対して、上記の説明のように updateMessageupdateStatus をしてやればいいわけです。
これは共通部分に書くことにしました。

_Layout.cshtml

<script type="text/javascript">
  $(function () {
    $("form").formValidation({
      framework: "bootstrap",
      locale: "ja_JP",
      err: {
        container: function ($field, validator) {
          return $field.closest(".form-group").find(".validation-message");
        }
      }
    });
  });
</script>

@RenderSection("scripts", required: false)

@{
  var ms = ViewData.ModelState;
  if (!ms.IsValid)
  {
    <script type="text/javascript">
      $(function () {
        var fv = $("form").data("formValidation");

        @foreach (var key in ms.Keys)
        {
          if (!ms.IsValidField(key))
          {
            string message = string.Join("", ms[key].Errors.Select(x => x.ErrorMessage));

            <text>
              fv
                .updateMessage("@key", "blank", "@message")
                .updateStatus("@key", "INVALID", "blank");
            </text>
          }
        }
      });
    </script>
  }
}

Controller で設定したエラーメッセージは ViewData.ModelState から取得できます。
IsValidField メソッドでエラーがあるかどうか判定して、エラーメッセージを設定しています。
エラーメッセージは一つのコントロールに複数あり得るので、すべて連結しています。

f:id:aetos382:20150503175650p:plain

formValidation は、入力内容が一文字でも書き変わるたびにバリデーションを行います。
blank バリデーターは入力内容を確認せず、常に有効と判断しますので、何か書き換えを行えば、エラー状態はクリアされ、エラーメッセージは消えます(ついでに送信ボタンの有効化・無効化処理もしてくれます)。
ValidationMessageFor メソッドを使わなかった理由はここにあります。
ValidationMessageFor メソッドが生成する要素は fromValidation の管理下にないので、入力が書き変わったのを検知してエラーメッセージをクリアする処理を自分で書かなければなりません。
blank バリデーターを使うことで、そのあたりの処理は formValidation に任せることができるわけです。
その分、メッセージの表示自体は若干面倒になりましたが、個人的にはこちらの方がきれいだと思います。

まとめ

いかがでしょうか。
是非 formValidation を使ってみてください。

formvalidation.io