しばやん雑記

Azure とメイドさんが大好きなフリーランスのプログラマーのブログ

ASP.NET MVC 3 で独自の検証属性を作成して、クライアントサイド検証を行う

ASP.NET MVC 2 から HTTP POST 時の検証以外にも JavaScript を利用したクライアントサイド検証がサポートされました。入力中にも自動的にエラーを表示してくれるのは非常に快適ですが、独自の検証属性の場合はそのままではクライアントサイド検証を使うことはできません。

MVC 2 向けには ナオキにASP.NET(仮) : ASP.NET MVC 2 でカスタム属性の作成と、クライアントサイド検証のポイント だけ読んでおけば問題ないのですが、MVC 3 では IClientValidatable インターフェースが追加されて MVC 2 では必要だったアダプタクラスを作る必要が無くなりました。

IClientValidatable インターフェースに関しては ASP.NET MVC でアップロードされたファイルを検証する - しばやん雑記 で独自の検証属性を作成した時に気になっていたので調べたのですが、IClientValidatable と Unobtrusive JavaScript Validation について日本語で書かれている情報は皆無だったのでこれはチャンスとばかりに書いていきます。

IClientValidatable インターフェースについて

IClientValidatable Interface (System.Web.Mvc)

MSDN にも英語ですがリファレンスがちゃんとあります。非常にシンプルなインターフェースで、メソッドは GetClientValidationRules 一つしかありません。

IEnumerable<ModelClientValidationRule> GetClientValidationRules(
	ModelMetadata metadata,
	ControllerContext context
)

DataAnnotationsModelValidator にも同名のメソッドが存在していて、MVC 2 ではこのメソッドをオーバーライドしてクライアントサイド検証で必要なエラーメッセージなどをクライアント側に渡していました。実際のコードはナオキさんの記事を参考にしてください。

基本的には IClientValidatable インターフェースの GetClientValidationRules でもやることはあまり変わりません。今までは 2 つクラスが必要だったのを 1 つの属性で出来るようにしたというイメージです。せっかくですので、ナオキさんの記事と同じように CERO を検証する属性を作ってみます。

CeroAttribute を作る

まずは検証属性から作成しましょう、といっても IClientValidatable を実装している以外は全く同じなので、特に説明は必要ないと思います。

// IClientValidatable を実装している点以外は同じ
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public sealed class CeroAttribute : ValidationAttribute, IClientValidatable
{
    public CeroAttribute()
    {
        ErrorMessage = "検証に失敗しました。";
    }

    public char Rating { get; set; }

    public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
    {
        // ValidationType は JavaScript 側でも使うので注意
        var rule = new ModelClientValidationRule
        {
            ValidationType = "cero",
            ErrorMessage = FormatErrorMessage(metadata.GetDisplayName()),
        };

        // 追加のパラメータが存在する場合はセット
        rule.ValidationParameters["rating"] = Rating;

        // 作成したルールを返す
        yield return rule;
    }

    public override bool IsValid(object value)
    {
        // null の場合には検証しない
        if (value == null)
        {
            return true;
        }

        var age = (int)value;

        // レーティングと年齢で検証する
        switch (Rating)
        {
            case 'Z':
                return age >= 18;
            case 'D':
                return age >= 17;
            case 'C':
                return age >= 15;
            case 'B':
                return age >= 12;
            case 'A':
                return true;
            default:
                return false;
        }
    }
}

重要なのは ModelClientValidationRule を作成する部分で、エラーメッセージ、検証名、追加のパラメータをセットして返します。

モデルとビューの準備

モデルとビューですが、ビューはスキャフォールディングに任せておけば良いので省略します。今回は MVC 3 プロジェクトを作成した時に自動生成される LogOnModel に以下のような定義を追加しました。

[Display(Name = "Age")]
[Cero(Rating = 'Z', ErrorMessage = "18歳以上が対象です。")]
public int Age { get; set; }

これでフレームワークが検証に必要な情報を以下のような形でタグ内に埋め込んでくれます。

<div class="editor-label">
    <label for="Age">Age</label>
</div>
<div class="editor-field">
    <input data-val="true" data-val-cero="18歳以上が対象です。" data-val-cero-rating="Z" data-val-number="The field Age must be a number." data-val-required="Age フィールドが必要です。" id="Age" name="Age" type="text" value="" />
    <span class="field-validation-valid" data-valmsg-for="Age" data-valmsg-replace="true"></span>
</div>

ちゃんと追加パラメータの Rating = "Z" も属性として追加されていることがお分かりいただけると思います。後は jQuery 側がこの属性に従って検証をしてくれるのですが、独自の検証属性を作成した場合には MVC 2 と同様に JavaScript でも検証ロジックを書く必要があります。

クライアントサイドスクリプトを書く

MVC 3 では標準で jquery.validate を使っているので MVC 2 の頃とは書き方が多少異なります。とりあえず書いてしまいましょう。

// 最初の引数は ValidationType
jQuery.validator.addMethod("cero", function (value, element, param) {
    if (value == false) {
        return true;
    }

    // param には rating の値が入っている
    switch (param) {
        case 'Z':
            return value >= 18;
        case 'D':
            return value >= 17;
        case 'C':
            return value >= 15;
        case 'B':
            return value >= 12;
        case 'A':
            return true;
        default:
            return false;
    }
});

jQuery.validator.addMethod を使って独自の検証ロジックを登録します。addMethod の引数は 1 つめが GetClientValidationRules メソッドで作成したルールの ValidationType と同じ値にする必要があります。この値が一致しないと検証が行われませんので注意してください。

大勢の方が疑問に思っているであろう param に rating の値が入っている理由なのですが、この後にコードを出します jquery.validate.unobtrusive への登録によってこのような挙動になっています。

この段階ではまだクライアントサイド検証は動作しません。最後に jquery.validate.unobtrusive へ ValidationType を登録します。

// ValidationType とパラメータを登録する
jQuery.validator.unobtrusive.adapters.addSingleVal("cero", "rating");

はい、たったの一行です。検証のロジックは既に追加しているので、ValidationType の値と追加パラメータの名前を登録します。先ほどの param に rating の値が入っていた理由は、addSingleVal メソッドを使って登録したからです。

今回はパラメータが 1 つしかないので addSingleVal メソッドを使いましたが、他にも様々なメソッドがあります。詳しくは jquery.validate.unobtrusive のリファレンスへどうぞ!

これで独自の検証型でクライアントサイド検証を使う準備がすべて整いました。早速実行してみます。

Z 指定なので 18 歳未満でエラーが表示されました!POST 処理が裏で走っているというわけではなく、入力しているその瞬間に検証が行われています。

少し長くなりましたが手順をまとめると

  1. IClientValidatable を実装した検証属性を作成する
  2. jquery.validate に検証ロジックを登録する
  3. jquery.validate.unobtrusive に名前とパラメータを登録する

の 3 ステップで実装出来てしまいます。最後の jQuery 周りが駆け足となってしまったので、また別エントリでまとめることが出来ればと思います。