2012年2月12日日曜日

プロパティのバインドに失敗するときのエラーメッセージをカスタム

stackoverflow.com。 今でもたまに書き込んでます。

c# - Regular Expression stop crashing - Stack Overflow

随分前にも似たようなガッカリ感を味わったんだけど、今回もまた似たような気分を味わってます。とはいえ、質問の真意を履き違えての結果の可能性は否めない。

この質問、必須項目にしたDateTime型のプロパティへのバインドに失敗するから、正規表現でチェックしたんだけど、”0000/00/00 00:00:00”って入力するとエラーになるんだよね。なんで?って言う感じでしょーか。

質問者のモデルを見るとPostedは必須項目。DateTimeには有効な日時しか入れられないっていう条件をあわせて考えると、不正な文字列の時にエラーメッセージを表示できれば要件は満たす。かな。正規表現がどうのこうのっていう問題解決の仕方ではなく、プロパティにセット出来なかった時のエラーメッセージがカスタム出来ればいいかもね、と思ったんですねー。

private static string GetValueInvalidResource(ControllerContext controllerContext) {
	return GetUserResourceString(controllerContext, "PropertyValueInvalid") ?? MvcResources.DefaultModelBinder_ValueInvalid;
}

private static string GetValueRequiredResource(ControllerContext controllerContext) {
	return GetUserResourceString(controllerContext, "PropertyValueRequired") ?? MvcResources.DefaultModelBinder_ValueRequired;
}

↑このようにMVCのDefaultModelBinderではそんな時のエラーメッセージをプロパティ”個別”にカスタムはできません。どんなプロパティにも共通のメッセージ。

で考えた結果、投稿したコードが↓これ。

public class CustomModelBinder : DefaultModelBinder
{
  protected override void SetProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor, object value)
  {
    base.SetProperty(controllerContext, bindingContext, propertyDescriptor, value);

    var propertyMetadata = bindingContext.PropertyMetadata[propertyDescriptor.Name];
    var invalidMessage = propertyMetadata.AdditionalValues.ContainsKey("PropertyValueInvalid")
                 ? (string)propertyMetadata.AdditionalValues["PropertyValueInvalid"]
                 : string.Empty;
    if (string.IsNullOrEmpty(invalidMessage))
    {
      return;
    }

    // code from DefaultModelBinder
    string fullPropertyKey = CreateSubPropertyName(bindingContext.ModelName, propertyDescriptor.Name);
    if (!bindingContext.ValueProvider.ContainsPrefix(fullPropertyKey))
    {
      return;
    }
    ModelState modelState = bindingContext.ModelState[fullPropertyKey];
    foreach (ModelError error in modelState.Errors.Where(err => String.IsNullOrEmpty(err.ErrorMessage) && err.Exception != null).ToList())
    {
      for (Exception exception = error.Exception; exception != null; exception = exception.InnerException)
      {
        if (exception is FormatException)
        {
          string displayName = propertyMetadata.GetDisplayName();
          string errorMessageTemplate = invalidMessage;
          string errorMessage = String.Format(CultureInfo.CurrentCulture, errorMessageTemplate,
                            modelState.Value.AttemptedValue, displayName);
          modelState.Errors.Remove(error);
          modelState.Errors.Add(errorMessage);
          break;
        }
      }
    }
  }
}

これを利用するためにモデル定義を以下のように変えてしまいましょう。

[Required]
[AdditionalMetadata(
 "PropertyValueInvalid",
 "Wrong Syntax Entered, Needed:day/Month/Year Hour:Minutes:Seconds")]
public DateTime? Posted { get; set; }

AdditionalMetadataにカスタムメッセージを指定して、プロパティのバインドがエラーになったらそのメッセージを表示する。素敵だと思うんだけどなー。

で、コメントが「this code is like giving a baby a gun」。ぎゃふん。