ラベル xVal の投稿を表示しています。 すべての投稿を表示
ラベル xVal の投稿を表示しています。 すべての投稿を表示

2009年10月4日日曜日

ValidationAttributeがちょっと違う

ASP.NET MVC V2も既にPreview 2が出ましたね。入力検証がDataAnnotationsを前提にした設計(IDataErrorInfoじゃ物足りないしね)になってるのと、Templateベースのモデル描画がV2での重要ポイントですよね。

入力検証にxValと同じようにModelValidatorProviderとクライアントサイドバリデーションにjQuery.validateが導入されてのが興味深い。

そもそもV1にも追加コードとして公開されてるDataAnnotationsModelBinderがあって、System.ComponentModel.DataAnnotationsのValidationAttrbiuteを使えるようになてます。が、残念ながら標準アセンブリじゃなく別途配布されてるDLLを参照設定して使うようにしないと、全く機能しないというのがあります。何でかというとValidationAttributeクラスの実装が違うのと、その検証属性の実行方法が違うから。

.NET 3.5 SP1に含まれるVaridationAttributeクラスってIsValid(object value)なのに対し、配布されてるアセンブリではIsValid(object value)に加え、IsValid(object value, ValidationContext validationContext, out ValidationResult validationResult)があります。入力値を取得して検証するだけなら対象となる、プロパティ値があればそれで事足りるかも知れないけど、たとえばCompareValidatorは実装できないですよね。同じモデルインスタンス内の他のプロパティを見れないと比較できないじゃないですか。V1の場合は、特殊なアセンブリだからいいかもしれないけど、V2は標準アセンブリだからVaridationAttribute実装にValidationContextなんて無い(実はある?)。先日1.0がリリースされたxValが使用するアセンブリも標準アセンブリ参照だから当然インスタンス参照なんてない。

と、言うわけで、V1でxValを使うときの入力検証でモデルの他プロパティを参照する際のCompareAttributeクラスを書いて遊んでみました。何に使うのかは後で考える。

まずは、モデルインスタンスを渡せるIsValidを実装するためのインターフェイス定義。ValidationAttributeとそのインターフェースを実装するCompareAttributeクラスを定義。

  public interface IInstanceValidationAttribute
  {
    bool IsValid(object instance, object value);
  }

  public class CompareAttribute : ValidationAttribute, IInstanceValidationAttribute
  {
    public string PropertyName { get; set; }
    
    public CompareAttribute(string propertyName)
    {
      PropertyName = propertyName;
    }

    public override bool IsValid(object value)
    {
      throw new NotImplementedException();
    }

    public bool IsValid(object instance, object value)
    {
      var property = instance.GetType().GetProperty(PropertyName);
      if(property==null)
        throw new ArgumentException("パラメータ間違えてるよ");

      var targetValue = property.GetValue(instance, null);
      if (targetValue != null && targetValue.Equals(value))
        return true;

      return false;
    }
  }

CompareAttributeを使うモデルクラスの定義。他にも必須チェックやら付けてみる。

  public class Drink
  {
    [Required]
    [StringLength(10)]
    public string Name { get; set; }

    [Compare("Name",ErrorMessage = "一致しないよ!")]
    public string CheckName { get; set; }
    
    [Range(10, 50)]
    public int Size { get; set; }
  }

続いて、xValのサンプルを参考に、入力検証を実行するためのDataAnnotationsValidationRunnerクラスを実装。

  internal static class DataAnnotationsValidationRunner
  {
    public static IEnumerable<ErrorInfo> GetErrors(object instance)
    {
      var attrs = from prop in TypeDescriptor.GetProperties(instance)
.Cast<PropertyDescriptor>() from attribute in prop.Attributes.OfType<ValidationAttribute>() select new { Property = prop, Validator = attribute, IsInstanceValidator = attribute is IInstanceValidationAttribute }; foreach(var attr in attrs) { bool isvalid; if (!attr.IsInstanceValidator) isvalid = attr.Validator.IsValid(attr.Property.GetValue(instance)); else isvalid = (attr.Validator as IInstanceValidationAttribute)
.IsValid(instance, attr.Property.GetValue(instance)); if (!isvalid) yield return new ErrorInfo(attr.Property.Name,
attr.Validator.FormatErrorMessage(string.Empty),
instance); } } }

なんかちょっとダサいけど、まぁ、その辺はセンスが無いってことで勘弁してください。 最後にControllerにアクションを追加してコードは完成。

    public ActionResult Drinks()
    {
      var model = new Drink();
      return View(model);
    }

    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult Drinks(Drink model)
    {
      var errors = DataAnnotationsValidationRunner.GetErrors(model);
      if (errors.Any())
        new RulesException(errors).AddModelStateErrors(ModelState,"");
      
      return View(model);
    }

実行したのが↓これ。

xVal xVal2 

ここまでが、V1の話で、ここからV2 P2で同じことをしてみようと思います。こんなに違うのかと思えるほどDataAnnotationsの組み込みと、ModelValidatorProviderに感動です。CompareAttributeクラスとモデルクラス(Drinkクラス)は一切いじりません。追加で作成の必要なクラスは以下の1つのみ。

  public class CompaireValidator : DataAnnotationsModelValidator<CompareAttribute>
  {
    public CompaireValidator(ModelMetadata metadata, ControllerContext context, CompareAttribute attribute) : base(metadata, context, attribute) { }
    internal static ModelValidator Create(ModelMetadata metadata, ControllerContext context, ValidationAttribute attribute)
    {
      return new CompaireValidator(metadata, context, (CompareAttribute)attribute);
    }

    public override IEnumerable<ModelValidationResult> Validate(object container)
    {
      if (!(Attribute as IInstanceValidationAttribute).IsValid(container, Metadata.Model))
        yield return (new ModelValidationResult
                        {
                          MemberName = Metadata.PropertyName, 
                  Message = Attribute.FormatErrorMessage(Metadata.GetDisplayName())
                        });
    }
  }

すごいっすね~。たったこれだけ。で、このValidatorクラスを登録するためにGlobal.asax.csのApplication_Startに1行追加。

    protected void Application_Start()
    {
      RegisterRoutes(RouteTable.Routes);
      DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(CompareAttribute), typeof(CompaireValidator));
    }

プロパティーの取出しからAttributeの取り出しまで一切合財がMVCの処理の範疇になってます。DataAnnotations関連のProvider/Validatorを使って作ってるけど、この辺も自分でベースクラスから派生させたり、AssociatedValidatorProviderからの派生で作ることも可能。もちろん簡単なのはDataAnnotations関連のクラスを派生させて作ること。

ちなみにControllerでの使用は何も考える必要も無く以下のようにしとくだけ。

    public ActionResult Drinks()
    {
      return View(new Drink());
    }

    [HttpPost]
    public ActionResult Drinks(Drink model)
    {
      if(ModelState.IsValid)
      {
        // ... success code
      }
      return View(model);
    } 

ValidationAttributeクラスは標準クラスのままなので、モデルクラスのインスタンス参照を持ってないのは今後もそうだと思うので、今回のような方法でうまいことインスタンスを渡せるオーバーロードを定義することで、対応していくのがいいんじゃないかと思う次第です。

ASP.NET MVC Reference
Brad Wilson: Enterprise Library Validation example for ASP.NET MVC 2
Shaan's Official Blog : New features in ASP.NET MVC 2 Preview

2009年1月11日日曜日

xVal

まだバージョンも0.5だし、これからなプロジェクトなんだろうけど、これはちょっと目が離せないかも。

ASP.NET MVCでサーバーサイドのバリデーション(入力検証)を行うとき、DynamicDataで導入されたDataAnnotationsを使うのが、現時点ではベストな選択なんじゃないかと思うんだけど、いかんせんサーバーサイドでの属性ベースのテクノロジなので、クライアントサイドでの検証は別途実装しなきゃなところ。 クライアントサイドでの検証があるだけで、入力をいちいちサーバーにポストしなくても何がエラーなのか、入力と同時に分かるから便利だけど、その為に同じルールを何度もコーディングするのは面倒だよね。面倒だけど仕方なく実装する感じ。

そこを橋渡しするのがこのxVal。

xVal – Home

DataAnnotationsで属性に指定した検証ルールをJson形式に変換して、クライアントに書き出すことで、ルール定義は1箇所で済ませようという、楽したいがタメに生まれたナイスなプロジェクト。 サーバーサイドの属性の抽出用プロバイダとして、System.ComponentModel.DataAnnotationsだけじゃなく、Castle.Components.Validatorも実装。

クライアントサイドの検証にはjQuery.validateの他にも、ASP.NET標準の検証コントロールを使った実装もある(けど、こっちはあんまり興味なし)。xVal.ClientSidePluginsに入ってるこの2つのクライアントサイドの実装は、たぶんこれ以外にも例えば prototype.js版とか作れるってことだよね。AllPossibleRulesを全部書けば...。誰か作ってくれるんじゃないかな~。作ってくれないかな~。

xVal - a validation framework for ASP.NET MVC « Steve Sanderson’s blog ↑ここで、どんな物なのか書かれてる。

サンプルプロジェクトもあるので、実際に動かしてみるのが分かりやすいね。 って、ことで、早速ダウンロード。

BookingsDemoにはxValのアセンブリは含まれてるけど、ソースは含まれてないのでソースはCodePlexから。

xVal - Source Code

なるほど~。すばらしいくらいリフレクション。 DataTypeRule/RangeRule/RegularExpressionRule/RequiredRule/StringLengthRuleの5つのルールに属性を変換するんだね。

サンプルでレンダリングされるJsonは↓。

<script type="text/javascript">
 xVal.AttachValidator("booking",
   {"Fields":[
       {"FieldName":"ClientName","FieldRules":[
           {"RuleName":"StringLength","RuleParameters":{"MaxLength":"15"}},
           {"RuleName":"Required","RuleParameters":{}}]
       },
       {"FieldName":"NumberOfGuests","FieldRules":[
           {"RuleName":"Range","RuleParameters":{"Min":"1","Max":"20","Type":"decimal"}},
           {"RuleName":"DataType","RuleParameters":{"Type":"Integer"}}]
       },
       {"FieldName":"ArrivalDate","FieldRules":[
           {"RuleName":"DataType","RuleParameters":{"Type":"Date"}},
           {"RuleName":"Required","RuleParameters":{}}]
       }]
   })
</script>

見ての通り、入力値の単純な検証は変換するけど、ビジネスルールはサーバーサイドのみで実行。 それが、どこにあるのか探してみると、BookingManager.PlaceBooking。 その中で、属性ベースの検証の実行と、ビジネスルールの検証の両方を実装。

アクションはどうやってるのか見てみると

        [AcceptVerbs(HttpVerbs.Post)]
       public ActionResult CreateBooking(Booking booking)
       {
           try {
               BookingManager.PlaceBooking(booking);              
           }
           catch(RulesException ex) {
               ex.AddModelStateErrors(ModelState, "booking");
           }

           return ModelState.IsValid ? RedirectToAction("Completed")
                                     : (ActionResult) View();
       } 

RuleException が発生(PlaceBooking内でエラーがあったらスロー)したら、ModelStateにエラーを入れる。これで、JavaScriptオフでも入力検証はきちんと実行されるて、エラーフィールドにはinput-validation-error、エラーメッセージも表示(<%= Html.ValidationMessage(モデル名)%>)されるっていうすばらしさ(ModelState.AddModelErrorでエラーを入れれば、Htmlヘルパー経由の場合、ちゃんと反映されるっていうASP.NET MVCの設計がこういうとき威力を発揮するね)。 RCがなかなかリリースされないけど、この辺見てるだけでも楽しいかも~。 ※RCでは動かなかったりするかもしれないけどね。