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

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年3月27日金曜日

DomainServiceが気になる

誰も彼もが気になってしかたがない.NET RIA Services。そうでもない?その中でも特に気になるのがデータ操作をラッピングしてサーバーサイドとのプロキシ動作を司るDomainServiceからはしばらく目が離せそうに無いですよね。得意のなんの脈絡もない展開ですいませんね。

.NET RIA Services - Building Data-Driven Applications with Microsoft Silverlight and Microsoft ASP.NET - MIX Videos

Microsoft ASP.NET 4.0 Data Access: Patterns for Success with Web Forms - MIX Videos

あれですよ、とにかく上記2つのMIX09セッションを見るべし、ですよ。英語だけど気にすんなよな!

ビデオ見た感じSilverlight3が目立ってる感じするかもしれないけど、そこじゃねっす。まじっす。気になるのはデータアクセス部分っす。Silverlight3からもASP.NETからも、はたまたJavaScript(これはちょっと特殊だけど)からも同一のサーバーサービスを呼び出してるよね。サーバーサービスを呼び出してるというか、サーバーサービス定義を元にジェネレートしたクライアント用クラスを使ってるよね。中身はサーバーサービスの呼び出しと、データ操作のトラッキングだろうと。サーバーサイド実装が何を使ってるのか気になる。ヒントはJavaScriptから呼び出してるデモの所で、DataService.axdがURIに含まれてるから、基本HttpHandler内に組み込まれてる機能なんだろうとは思うけど、それがWCFなのか独自実装(だとしたらパネーっす)なのか。Windows 7じゃないんだからWindows 7: Web Services in Native Code | pdc2008 | Channel 9ってことはないでしょうが。そんなこんなで"絶対読めよ"と↓こんな資料も。

Public Sector Developer Weblog : A MUST READ: Microsoft .NET RIA Services Overview

しょうがないからダウンロードして読んでみる。がんばる。読むというか見る...。

前半40ページくらいまで、こういう感じよ、こうやって書くよ、こんな感じで作るのよ、な説明が続くので、ビデオ見たのでそこはサラッと流す。EnableClientAccess属性が付いてるとクライアントコードをジェネレートするって書いてるけど、ジェネレートしなくてもサーバー上でだけでも使うならそれもよし。

こっからが、少し気になる部分の説明に入っていく。CRUD操作のR部分。デモでは何気なくGet~で書いたものをプロキシクラス呼び出すクライアントではLoad~って書いてロードしてるよね。ItemsSourceのところはコレクションプロパティ(this.Entities.GetEntityList<T>()って何が返るのかな~)を指定してるし。どういう風な関連付けがされてるのか気になるっす。空のコレクションを返しといて、あとはObserveに任せるのかな。Get~の部分はプレフィックスが「“Get”, “Fetch”, “Find”, “Query”, “Retrieve”, or “Select”」なら何でもいいみたいね。というか、Query属性をつけるなら、それすら関係無い。CoCなり。読み込みクエリーはLoad~が非同期で実行。

更新系もデモだとInsert/Update/Deleteがプレフィックスに付いてるけど、ここも何種類か規約でプレフィックスが用意されてる。データ取得と同じく、属性ベースでも指定可(Update/Insert/Delete属性)。クライアントでの更新結果はEntities.GetChanges()でEntityChangeSetを取得することですべて抽出できる。たぶんLINQ to SQLのDataContext.GetChangeSet()と同じようなものなんだろう。SubmitChanges()で更新情報をサーバーに送信して確定させるか、RejectChanges()で破棄。サーバー上では6つのパイプライン(クライアントから受信したもの→認可→検証→実行→永続化→同時実行エラー)に更新セットを流す(?)。トランザクション制御はやらないので、必要なら自分でオーバーライドしてTransactionScope使うべし。

Custom属性をつけたメソッド内での更新はクライアントのコンテキスト上で実行されるみたいで、ちゃんとクライアントてSubmitChangesしないとサーバーには反映されない。プロキシクラス内にそのまんまメソッドが展開されるんでしょうね。更新を伴わない場合はCustom属性じゃなくてServiceOperation属性。どっちも非同期みたい。

フィールドレベルの検証はDataAnnotationsを使ってMetadataクラスに定義。この辺はだいたいみんな同じような作り方になるのね。独自検証の場合はShared属性をつけたクラスで実装しとけば、それも自動生成に含めてくれるから、単純な検証以上の事をしたい場合はこれで。ビジネスロジックとしての検証はどこに書くのかな~?モデルクラスなのかな~?

後は認証や、認可の属性と続いてSilverlight、ASP.NET内で使うDomainDataSourceコントロールの説明と続く。ページングやらフィルタの指定もXAML内でできるのがカッコイイ。

後は、サンプルコードみたりしないとちょっとよく分からないけど、ビデオでは紹介されてない実装の細かいところはこのドキュメントにある程度書かれてる感じです。でも、もっと知りたいし、出来れば.NET RIA Services(Silverlight3環境必須)を入れなくてもDomainServiceだけでも試したいな~。コードサンプルが小野さんに教えてもらったASP.NET Dynamic Data 4.0 Preview 3にあるので、そっちを眺めてみようと思います。

dotnetConf2015 Japan

https://github.com/takepara/MvcVpl ↑こちらにいろいろ置いときました。 参加してくださった方々の温かい対応に感謝感謝です。