2011年3月12日土曜日

ASP.NET MVCでDataAnnotationsのエラーメッセージをカスタム

ちょっとこの質問見てみてくださいよ!

Asp.Net MVC 2 - Changing the PropertyValueRequired string. - Stack Overflow

ASP.NET MVCのDefaultModelBinderにはResourceClassKeyっていうプロパティがあって、そこに自作リソースを指定して、文字列リソースのキー名にPropertyValueInvalid/PropertyValueRequiredっていうのを作っておくと、アプリケーション全体にその文字列が適用される。でもInvalidは動作するけどRequiredがぜんぜん適用されません!っていう内容なんですね。

これ質問もなかなか面白いからか、特別ポイントが付与される質問だったんです。なので、張り切って思ってソース追っかけたりしながら動作を見てみたわけですよ。そしたら確かにInvalidはメッセージの置き換えができるのにRequiredは置き換えがおきない。

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

  [Required]
  [Range(0,100)]
  public int? Age { get; set; }

  [DataType(DataType.Date)]
  public DateTime Birthday { get; set; }

  [DataType(DataType.EmailAddress)]
  public string Email { get; set; }
}

msg1

protected void Application_Start()
{
  AreaRegistration.RegisterAllAreas();

  RegisterGlobalFilters(GlobalFilters.Filters);
  RegisterRoutes(RouteTable.Routes);

  DefaultModelBinder.ResourceClassKey = "Messages";
}

msg2

なんでかな~、と調べてみたらDataAnnotationsのRequiredAttributeなんかは内部で参照するリソース名を固定で保持してて、DefaultModelBinderのほうの設定を参照しないんですね。分かってしまえば、そりゃそうなんですけど(日本語のエラーちゃんと出るし、GACに入ってるリソースを参照するのが正しい挙動な感じするしね)、それでもASP.NET MVCならできる方法がありそうな気がするんですよね。これだけ拡張ポイントたくさんあるんだから。

いろいろ見ていくとちゃんと用意されてるのを発見しました。

Reusable Validation Error Message Resource Strings for DataAnnotations

DataAnnotationsModelValidatorProvider.RegisterAdapterでValidationAttributeの型ごとにAdapterを指定できるんです。このAdapterのコンストラクタにはValidationAttributeクラスのインスタンスがわたってくるんですが、このAdapterを経由させてから、MVCはエラーメッセージを生成したりするので、AdapterのコンストラクタでインスタンスプロパティのErrorMessageResourceTypeとErrorMessageResourceNameを書き換えてあげれば、アプリケーション全体のメッセージを変更できるっていう仕組みです。

分かりやすいやり方としてはね、カスタムValidationAttributeを作って、ErrorMessage~の設定を上書いておくか、Modelに指定するときに属性プロパティに指定する方法なんだと思うけど、それだと属性を指定する箇所がすごい数になっちゃうじゃないですか。それでもいいけど、そうじゃなく一括して変更したいっていうのが質問の内容じゃないですか。

んで、ちゃんとサンプル書いて返信したんですよ。

public class MyRequiredAttributeAdapter : RequiredAttributeAdapter
{
  public MyRequiredAttributeAdapter(ModelMetadata metadata, ControllerContext context, RequiredAttribute attribute) : base(metadata, context, attribute)
  {
    attribute.ErrorMessageResourceType = typeof (Messages);
    attribute.ErrorMessageResourceName = "PropertyValueRequired";
  }
}

アダプター用意して、Global.asaxに

protected void Application_Start()
{
  AreaRegistration.RegisterAllAreas();

  RegisterGlobalFilters(GlobalFilters.Filters);
  RegisterRoutes(RouteTable.Routes);

  DefaultModelBinder.ResourceClassKey = "Messages";
  DataAnnotationsModelValidatorProvider.RegisterAdapter(
    typeof(RequiredAttribute),
    typeof(MyRequiredAttributeAdapter));
}

↑こう書けばカスタムできるよ!って。

msg3

なのに!なのになのに!カスタムValidationAttributeを作るほうに特別ポイントが!!ガッカリデス。ガッカリ過ぎてスネ毛が少し抜けたよ...。

こういう一括していの方法があることを知らなかったので、勉強になりました。