読者です 読者をやめる 読者になる 読者になる

しばやん雑記

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

ASP.NET MVC で SelectList を使ったフォームの検証を行ってみる

ASP.NET MVC で Html.DropDownListFor を使った場合、どうやって検証を行っていますか?不正な値が選択されないという想定で作る人もいると思いますが、大抵の場合はブラウザの開発者ツールで値を改ざんされて送られる可能性を考慮する必要があります。

困ったことに ASP.NET MVC は SelectList や SelectListItem を使ってドロップダウンリストは作れるのに、その送られてきた値を検証する方法が無いんですよね。以下のようにアクションで検証する方法はありますが、それだと ModelState.IsValid が true になるのでチェックがめんどくさいですね。

[HttpPost]
public ActionResult Create(UserCreateModel model)
{
    if (ModelState.IsValid)
    {
        // true の時にもう一回チェックしないといけない
        if (!_prefectures.ContainsKey(model.PrefectureId))
        {
            ModelState.AddError("PrefectureId", "都道府県が不正な値です");
        }
    }

    return View(model);
}

アクションがごちゃごちゃするし、再利用もめんどくさいし、とにかくわかりにくいですよね。

という訳で SelectList や SelectListItem を使って属性ベースで入力値の検証を行う Contains 検証属性を作ってみました。

public class ContainsAttribute : ValidationAttribute
{
    public Type Type { get; set; }

    public string PropertyName { get; set; }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        var property = Type.GetProperty(PropertyName);

        var selectList = (IEnumerable<SelectListItem>)property.GetValue(null);

        var strValue = value.ToString();

        if (selectList.All(p => p.Value != strValue))
        {
            return new ValidationResult(ErrorMessage);
        }

        return null;
    }
}

例によってエラー処理などは省いたコードになってますが、誰でも思いつくような実装ですね。属性ベースで行うために、リフレクションを使って SelectList を取得するようにしています。

実際に使う場合には以下のようにモデルに属性を付けます。

// 静的クラスに SelectList を持たせておく
public static class Master
{
    public static IEnumerable<SelectListItem> Prefectures
    {
        get { ... }
    }
}

public class UserCreateModel
{
    [Contains(Type = typeof(Master), PropertyName = "Prefectures", ErrorMessage = "都道府県の値が不正です。")]
    public int PrefectureId { get; set; }
}

型とプロパティ名を指定するのがめんどくさいですが、属性では定数しか持たせられないので仕方ないです。しかし、アクションで検証するのに比べると断然シンプルに書けますよね。

実際にブラウザの開発者ツールで値を改ざんして POST してみると、ちゃんと以下のようにエラーが表示されました。

f:id:shiba-yan:20130301235513p:plain

みんな一度は悩む部分だと思うんですが、案外気にしてないんでしょうか?