検索キーワード「ModelBinder」に一致する投稿を関連性の高い順に表示しています。 日付順 すべての投稿を表示
検索キーワード「ModelBinder」に一致する投稿を関連性の高い順に表示しています。 日付順 すべての投稿を表示

2010年2月28日日曜日

Demotterについて

ASP.NET MVC 2になって変更された箇所はとても多いです。MVC 2での新しくなった部分を紹介するサンプルとしてMvcPresenterというのを作成することにしました。Demotterという名前はEdtterへのオマージュ(?)。

MVC 2の新機能のうちマニアにはたまらないだろうなと思って目をつけたのが各種Providerモデルへのリファクタリング部分で、MvcPresenterではそのうちIValueProviderを実装したValueProviderFactoryのカスタム化と言う部分をメインに実装しています。

いきなりそんな話をされても意味がわからないと思うので、順を追って説明していきます(ASP.NET MVCについての基本的な知識は前提です)。

そもそもMVCではポストバックがないので、TextBoxやRadioButtonなどの入力用サーバーコントロールは使用しません。HTMLとしてのinputやtextarea、selectを使用するのみです。そうすると入力値をサーバーサイドで取得するにはどうすればいいかというと以下の3通りあります。

  • Request.Formで取得
  • FormCollection型の変数をAction引数に指定する
  • ModelBinderに任せる

Request.FormとFormCollectionを使用する方法はあまりにも原始的すぎます。入力に対する検証も自分で行う必要があり、とても煩雑なコードになります。

Viewとして以下のようなものがあるとします。

  <% using (Html.BeginForm("FormPost1")) { %>
  
    <% = Html.TextBox("name") %>
    <% = Html.TextBox("age") %>
    <% = Html.CheckBox("isDeveloper") %>
    
    <input type="submit" value="do post" />
    
  <% } %>

これを受け付けるActionとしてRequest.FormやFormCollectionの場合↓こうなります。

    public ActionResult FormPost1()
    {
      var name = Request.Form["name"];
      int age;
      int.TryParse(Request.Form["age"], out age);
      bool isDeveloper;
      bool.TryParse(Request.Form["isDeveloper"], out isDeveloper);

      // 処理

      return View();
    }

    public ActionResult FormPost2(FormCollection form)
    {
      var name = form["name"];
      int age;
      int.TryParse(form["age"], out age);
      bool isDeveloper;
      bool.TryParse(form["isDeveloper"], out isDeveloper);

      // 処理

      return View();
    }

ほとんど同じなんですが、テストする際にRequestなどのコンテキストに依存させないようにするためにFormCollectionを使用するという書き方が存在します。

これに対しModelBinderを利用するスタイルの場合は以下のようになります。

    public ActionResult FormPost3(string name, int age, bool isDeveloper)
    {
      // 処理

      return View();
    }

Action引数に直接入力値が入ってきます。型変換も自動です。変換できないならエラーになるという便利なものです。でも、これだと細かく入力エラーを処理できないです。しかも値が多いとAction引数がとんでもないことになります。

なので以下のようにクラスを定義して、そのクラスのインスタンスをAction引数に取得するというスタイルがオーソドックスな手法となるはずです。

  public class Person
  {
    public string Name { get; set; }
    public int Age { get; set; }
    public bool IsDeveloper { get; set; }
  }

↑これがクラス定義で、↓これがAction。

    public ActionResult FormPost4(Person person)
    {
      // 処理

      return View();
    }

何が違うかは一目瞭然。本来personという仮引数名を使用する場合、Formのname属性にプレフィックスとして"person."とつけておくんですが、そこは自動でプロパティ名とname属性をみて一致するなら埋めてくれます。なので、あえて"person.name"や”person.age”とname属性に指定しなくてもModelBinderは賢いのでなんとかしてくれるんですね。明示的に分けたいときにname属性にプレフィックスを指定する必要があります。

クラスを指定するのも基本型を指定するのもModelBinderにしてみれば同じことです。固有のクラスを使用して、DefaultModelBinderがきちんとインスタンスを生成できないときには独自のModelBinderを作成することになると思いますが、MVC 2ではそういう手法はあまりとらないんじゃないかと思ってます。理由はValueProviderFactoryが指定できるようになったからです。

不思議に思わないですか?Routeに指定した場合でもAction引数に割り当てられるし、FormからPostしても割り当てられる。もちろんQueryStringの場合でも自動でAction引数に値がわたってくるんですよ?データの出所がそれぞれ違うじゃないですか。RouteとQueryStringはURLだから同じだと見ることもできるんですけど。

ここで、もうひとつ忘れてはいけないのがUpdateModelとTryUpdateModelです。これはIValueProviderを指定するか、Formの値を利用するかのどちらかでクラスのインスタンスを生成してくれるんですが、それもValueProviderFactoryを利用することでデータの出所を意識しなくても良くなります。

さっきから"データの出所(でどころ)"という言葉を使ってますが、それってどういう意味かというと、ModelBinderが値を復元する際にどこから値を持ってくるのか?ということです。Request.FormなのかRequest.QueryStringなのか、RouteData.Valueなのか、ですね。じゃーRequest.Cookieから復元させたいときはどうすればいいと思いますか?MVC 1の時はAction内でRequest.Cookieを直接みて自分で変数に割り当てるか、カスタムModelBinderを作成し、そこでRequest.Cookieを参照してモデルに復元させる必要がありました。MVC 2になるとデータの出所をValueProviderFactoryから取得するという仕様になっているので、カスタムなValueProviderFactoryを作成し、Global.asaxでValueProviderFactoriesに追加しておけば、標準のValueProviderFactoryで見つからなかった場合、カスタムValueProviderFactoryから値を取得して、ModelBinderが値(クラスのインスタンスか基本型)を復元してくれます。Cookieから値を取得して復元させたければ、ModelBinderを作成するのではなく、そこはDefaultModelBinderに任せたまま、CookieValueProviderFactoryを作成するとなるでしょう。そうすることでDataAnnotationも有効な状態で値を取得できます(カスタムModelBinderでもできますがそれはまた別の時に)。

ModelBinderのデータの出所(データ取得元)を自分で好きなように指定できるということです。すごいことですよね。ちなみにValueProviderFactoryとして実装しなければいけない唯一のメソッドは

public override IValueProvider GetValueProvider(ControllerContext controllerContext)

です。IValueProviderの基本実装はDictionaryValueProvider<object>で、KeyValueなディクショナリです。ModelBinderの仕組みそのものはMVC 1の時から変わってないので、詳細ははしょりますが(書いたほうがいいですか?)、キーとしてForm要素のname属性やQueryStringのKeyを指定するのを想定して値を取得します。

なのでDictionaryとしてCookieから取得した値を返そうが、JSONをデシリアライズしてキー指定で取得できるようにしたものを返そうがXMLをキー指定で取得できるようにしたとしても、Dictionaryとして取得できるならなんでもいいんです。データの出所だけではなくデータのフォーマットにも依存させなくて済むということです。

JSONとして以下のようなデータがあったとしましょう。

{
  name:'たけはら',
  age:15,
  isDevelopper:true
}

ここから以下のように値が取得できるDictionaryを返すことができれば、ModelBinderは値を復元できるということです。

dict["name"] = "たけはら";
dict["age"] = 15;
dict["isDeveloper"] = true;

おなじ理屈でXMLでもいいですよね。自分で取りやすいスキーマさえ定義しておけばいいので。つまり、ファイルシステムにKeyValueでアクセスできるValueProviderFactoryを作成するなら、Zipでアップロードしたファイルを解凍し、フォルダ構造とファイル名がキーになっていてファイルの実体が取得できるようなものも作成できるわけです。実際にファイルの実体をbyte[]なんかで復元するのはリソースの無駄遣いになるので、ModelBinderが復元するのはファイルのパスにしておくというのが現実的でしょう。MvcPresenterがまさにそのように処理をしています。

ちょっと長くて疲れてきたので、続きは今度にしてもいいですかね。いいですよね。中途半端でさーせん。眠いっす。

続Demotterについて

途中まで書いて力尽きたので、続きを。

ValuProviderFactoryについての説明まで進みましたが、そもそものIValueProviderは何なのかというところをすっ飛ばしてます。IValueProvider自体はMVC 1の頃から存在してるものです。GetValueでValueProviderResultとして値を取得するためのインターフェースです。UpdateModelなんかもIValueProviderを渡すオーバーライドがちゃんとあります。FormCollection.ToValueProvider()なんかを渡しますね。

ようは何でもいいからキーに対応する値を渡せばいいんです。基本実装のDictionaryValueProviderは値の実体そのものをキーと共に保持してるものなんですが、キーに対応する値を常に保持しておく必要はなく、GetValueのたびに取得しにいくという実装もアリです。HttpCookieValueProviderの例としてとてもいいサイトが有ります。

Dive Deep Into MVC - IValueProvider - Mehdi Golchin's blog

このサンプルコードではGetValueのたびにRequest.Cookieを参照する作りなのは、値の実体はHttpContextにそもそも保持されてるので、Dictionaryとして二重に保持する必要がないからです。ただ、これだとテストはHttpContextBaseのモック作成から必要になるんで、少し面倒ですけど。

Demotter(MvcPresetner)で作成したのは、PresentationZipValueProviderFactoryでプロジェクトルートにあるやつです。

mvcp

これまで説明したValueProviderFactoryのサンプル実装としてZipファイルをアップロードすると、サーバーサイドで解凍し、Presentationクラス(特にModelBinderの対象となるクラスを限定する縛りはないです)のインスタンスを生成するためにDefaultModelBinderから利用出来るようになっています。

なんで、ここに注目してるのかというと、SRP(Single Responsibility Principal)でテストを簡単にしたいからです。やっぱり楽して作りたいというのがあるからね。Actionのテストってモデルクラスを渡してしまえば、実行コンテキストに依存させなくてもいいじゃないです。DefaultModelBinderは標準機能だからテストなんかしなくていい。そうすると、カスタムValueProviderFactoryだけがHttpContextBaseのMockを使ったテスト対象になるわけです(ViewのテストはWebサーバーで実際に動かしてSeleniumとかでどうぞ)。素晴らしいリファクタリングだと思います。

Zipファイルを解凍するために、外部の依存アセンブリとしてDotNetZip Libraryを利用しています。ZipFileクラスのReadでZipファイルを指定し、ExtractAllで全解凍です。使ってる機能はそれだけ。

PresentationのリポジトリとしてStoragePresentationRepositoryクラスを作ってます。IPresentationRepositoryのファイルストレージ保存用の実装です。なので、ここはデータベースに保存するようなRepositoryを実装すれば、データファイルの保存場所は上位のサービス層(このサンプルではサービス層はなくControllerで直接Repositoryを使ってます)が知る必要はないような作りです。

実ファイルを”~/App_Data”に保存するようにしてるのでServer.MapPathを使う必要があり、Controller.InitializeでIPresentationRepositoryの実装クラスのインスタンスを作成してるので、LinqToSqlPresentationRepositoryを作成したとしても、単純変更はできないのが、手を抜いたところです。RepositoryのResolverというかCreatorを作っておいて、そいつに任せるようにしておく実装であれば依存性を排除できますね。

    private IPresentationRepository _repository;
    public static Func<RequestContext,IPresentationRepository> RepositoryCreator = 
      (requestContext) => new StoragePresentationRepository(
requestContext.HttpContext.Server.MapPath("~/App_Data")
); protected override void Initialize(RequestContext requestContext) { base.Initialize(requestContext); // Serverプロパティなどの参照はInitialize以降じゃないと // できないので気をつけましょう _repository = RepositoryCreator(requestContext); }

たとえば、現状のコードを↑こうしてみるとか。LinqToSqlPresentationRepositoryはRequestContextを必要としないですけど、簡単にするにはこういうのを渡すルールにしておくのもいいんじゃないですかね。Global.asaxなんかで以下のようにCreatorを変えちゃえば、うまくいくはず。

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

      RegisterRoutes(RouteTable.Routes);

      ValueProviderFactories.Factories.Add(new PresentationZipValueProviderFactory());

      HomeController.RepositoryCreator = (_) => new LinqToSqlPresentationRepository();

      // カスタムModelBinderを使うなら↓ここで登録忘れずに。
      //ModelBinders.Binders.Add(typeof(Presentation), new PresentationModelBinder());
    }

ここはMVC関係ないところ。今回のサンプルではControllerが生成されるたびに、毎回Repositoryの中でApp_Dataを見てPresentationのインスタンスを取得するので無駄が多いですが、その辺もサンプルということで勘弁してもらえると助かります。

ModelBinderは標準のDefaultModelBinderを使っていて、DataAnnotationModelValidatorがそのまま機能します。カスタムModelBinderでもDataAnnotationを機能させるなら、多分以下のような作り方になると思います。

  /// ValueProviderFactoryを定義しない従来の手法だと、ModelBinderを作成して
  /// 以下のように自分でマップしたモデルに対してValidationを実行することになります。
  public class PresentationModelBinder : DefaultModelBinder
  {
    public override object BindModel(ControllerContext controllerContext, 
ModelBindingContext bindingContext) { // ここでモデルを生成してModelMetadataに入れておくと、 // CreateModelでは生成せずに、OnModelUpdating/OnModelUpdated // を内部で呼び出してくれるようになる。 // でも、この書き方であってるのか自信無いですが...。 var valueResult = bindingContext.ValueProvider.GetValue("Name"); var model = StorageAccessor.Load(
PresentationZipValueProviderFactory.UploadTempPath,
true, valueResult.AttemptedValue); bindingContext.ModelMetadata.Model = model; return base.BindModel(controllerContext, bindingContext); } }

これがあってるのかどうかは自信がないです。BindModelの戻り値にインスタンスをそのまま返すだけではDataAnnotationが効かないので、BindingContext.ModelMetadata.Modelに対象モデルのインスタンスを入れて、後はbase.BindModelに任せてしまう実装です。いいのかな~。ちゃんと動くのは確認してます。

リポジトリから取得できたものをHTMLとして生成するために、Viewにモデルを渡し(Presentationクラスのインスタンス)、後はViewにまるなげです。

スライドとして表示したいデータをMarkdown書式で送信したものを利用するようにしてるので(Stackoverflow.comをまねっこしてみたかった)、Markdown書式からHTMLに変換する必要があります。クライアントでの変換実装としてWMDというのもありますが、今回はサーバーサイドでHTMLに変換するMarkdownSharpを利用しています。2個目の外部依存アセンブリです。Markdownで厳しいところはUL/LIの入れ子が2段までしかできないところ。できる方法があるんだろうか。当たり前ですが、利用目的がスライドじゃないのでしかたないところですね。

スライドの動き自体はカーソルの上下でフェードさせながらの切り替え、左右でアニメーション無しでの切り替えの2種類のみで、リッチなアニメーションは実装してないです。その辺はS5やS6なんかがあるので、そっちに差し替えてもらえればいいかな~、なんて。

そんなこんなで、実行すると↓こんなです。

mvcp2

後は、F11でフルスクリーン表示にしておけば、それっぽく見えます。

あとアップロードしたコンテンツの削除をHttpDelete属性を指定したActionで実装してますが、これだけだとHttpVerbs.DeleteなリクエストじゃないとActionInvokerの対象として選択されないです。一般的なブラウザではGET/POSTしか送信してくれないので困りもの。でも、MVC 2ではHttpVerbsのオーバーライドを簡単にできるように拡張メソッドも用意されてるので、HtmlHelperの拡張メソッドHttpMethodOverrideをForm内で呼び出せば、POSTでもオーバーライド(hiddenに埋め込まれる)されてうまく動くようになります。Railsなんかでも"_method"でHttpVerbsをオーバーライドできるのでそれと同じです。

Viewでは以下のように書いてます。

    <ul>
    <% foreach (var item in Model) { %>
    
    <li>
      <%= Html.RouteLink(item.Name, "Viewer", new { id = item.Name })%>
      &nbsp;-&nbsp;
      <% using (Html.BeginForm("Delete", "Home", new { id = item.Name }, 
FormMethod.Post, new { style = "display:inline;" })) { %> <%= Html.HttpMethodOverride(HttpVerbs.Delete) %> <input type="submit" value="削除" /> <% } %> </li> <% } %> </ul>

簡単ですね。

mvcp3

↑こんなボタン出てきます。なんで、わざわざHTTPメソッドでActionを選択するのかというのはRESTfulなアーキテクチャスタイルの話になるので割愛。ただ、この実装方法であれば、ブラウザ以外からDeleteやPutのリクエストと、ブラウザからの同リクエストを区別するようにActionを書かなくて済むのがいいですよね。もちろんAction名が”Remove”とか”Update”とかでPostで処理をするようにしても、結果は一緒ですけどね。

DemotterことMvcPresenterが何を実装したサンプルなのか、だいたい分かってもらえたでしょうか。これを10分で話すのはさすがに無理ですね。詰め込みすぎました。

2008年9月3日水曜日

ModelBinderに気をつけねば

前のエントリでModelBinderを使って、ActionのパラメータにDBから取ってきたエンティティモデルをそのまま渡す方法を書いちゃったけど、あれはダメでした。

DataContextをstaticにもつサンプルだったけど、そうだとしてもいつDisposeされるのかなんて分かんないから、試しに実装してみたらケチョンケチョン...。

で、正しい使い方はこちら↓。 How to use the ASP.NET MVC ModelBinder - Melvyn Harbour というのを、昨日ガスリー君のブログでも書かれててホッとした。 ASP.NET MVC Preview 5 and Form Posting Scenarios - ScottGu's Blog

サンプルをチェックしてると、ProductをModelBinderでとってるじゃないかと思えるかもしれないけど、あくまで新規登録時の空エンティティの時にしか使ってないですよね。 で、更新処理の時にはDataContextから取得したProductに対して、ModelUpdateを使って値の書き込み。 この方法だとですね、更新時にはProductBinderのGetValue走らない。サンプルだから両方乗せてるんだろうけど(ModelUpdateとModelBinder)、最初はどんな意味が込められてるのか混乱しちゃった。 ModelUpdateでセットされるデフォルトのエラーメッセージが気に入らなかったらどこで書き換えればいいのかはちょっと分かんなかった。リソースファイルに持ってるのをどうすればいいんだろか。

で、 ProductクラスはLINQ to SQLのクラスなんだけど、これに対してModelStateDictionaryへメッセージを突っ込むコードを書くと、クラスが密結合しすぎちゃうがために、あえてRuleViolationクラスを作って(後でModelStateDisctionaryに入れやすくするために)、 IRuleEntityを実装。

ProductクラスのOnValidateはSubmitOnChangeの時に自動で呼び出してくれるっていうのがミソですね! でも、実際の開発は、たぶんだけどLINQ to SQLのエンティティクラスに対して直接入力しないよね。もう一つ間にViewDataクラスをはさんで、ViewDataにDBから読み込んだ値を入れてFormに表示。更新の時にViewDataに読み込んだ後、エンティティクラスに値をマッピングしていって更新。そんな流れになると思うので、入力検証のサンプルが↓これ。

Maarten Balliauw {blog} - Form validation with ASP.NET MVC preview 5

このサンプルではViewDataに直接値を入れてるけど、ViewDataのクラスを用意してViewPage<UserViewData>でレンダリングをView(model)にするんだと、↓こんな感じになるんじゃないかと思いますがどうですかね。

public class UserViewData { public name {get;set;} public email {get;set;} public message {get;set;} }

Contactアクション(POST)の入力値の取得で

var viewData = new UserViewData(); ModelUpdate(viewData, new[]{"name","email","message"});

って、すれば個々に入力値を取得しなくてもviewDataに埋め込みますわね。 でも、それだと入力検証できませんわね。全部stringだし。 なので、UserViewDataクラスに検証用のメソッドを追加して、それを呼び出すときにModelStateDictionaryを渡すのが簡単でいいんじゃないかと思います。 例えば、↓こんな。

public bool Validate(ModelStateDictionary modelStates)
{
 if (string.IsNullOrEmpty(name))
  modelStates.AddModelError("name",name,"名前入れてね!");
 else if (name.length < 4)
  modelStates.AddModelError("name",name,"4文字以上で名前入れてね!");

return modelStates.IsValid;
}

で、アクションでは↓。

 var viewData = new UserViewData();
 if (TryModelUpdate(viewData, new[]{"name","email","message"})) {
  viewData.Validate(ViewData.ModelState);
 } 

なんかヘンテコなコード...。 ※ModelUpdateも中でModelStateDictionaryにエラー値を入れてくれます。キャストできないとか。 ※Prefixをつけた場合、今までは最後に'.'(ドット)を自分でつけなきゃいけなかったのに、自動でつくようになってちょっと涙目...。気がつくのに時間かかった。

ちなみにUpdateModelのキー名はmodelに持ってる項目だけにすべし。Modelの項目を指定して、Formにない場合はエラーにならないけど、その逆はエラー(FormにあってModelにない)になるので気をつけよう。 ModelState情報はうまく利用すれば、エラーフィールドを強調できる(class属性にinput-validation-errorが自動でつく)ので超便利です。

ModelState はRenderPartial時にViewDataDictionaryを渡さないと(ViewData.Modelだけだとダメ)ユーザーコントロールで取得できないから、入力項目を持つユーザーコントロールのRenderPartial時には問答無用でViewDataも渡すようにするのが吉!

その他気になったところ。

Default option label for DropDownList in ASP.NET MVC Preview 5 - Shiju Varghese's Blog

便利にはなるよね。でも、必須にしなくてもいいじゃないかと、思ってしまうんですよ。 LINQで取り出した、ソースの最初の行に空(ここでいうoptionLabel)のレコードを連結させるコードを書いてたから、それがなくなるのはいいんだけど。ちなみにotionLabelに空文字""を指定すると何も起きないからレンダリング結果は今まで通り。

Maarten Balliauw {blog} - ASP.NET MVC preview 5's AntiForgeryToken helper method and attribute Steve Sanderson’s blog » Blog Archive » Prevent Cross-Site Request Forgery (CSRF) using ASP.NET MVC’s AntiForgeryToken() helper 何に使うのかサッパリわからなかったけど、こうやって使うんだね。CookieとForm(HIDDEN)POSTの値を比較して有効なリクエストか判定。

file download as attachment in latest preview (like this blog post) - ASP.NET Forums

FileResultの簡単な使い方。FileResultはResponse.TransmitFileとかで結果を直接返さずにActionResultとして返すことで、テストしやすくなるよね。 AntiForgeryToken/FileResultともにMicrosoft.Web.Mvcに入ってるデス。

2011年6月18日土曜日

mvcPhotosの出来るまで

ちょっと書いてみます。あんまり面白く無いですよ。

ASP.NET MVCをメインにした企画をやってみたいとチャックから連絡があって、喋る人が決まったところで各々にテーマがふられました。んで、たけはら担当として「クライアントサイドのテクノロジをメインに扱うセッションを」というリクエストから始まるんですが、ぶっちゃけ「それMVC関係ないじゃない...」と愕然としたものです。

とはいえ、テーマを無視するのもあれだからと、まずはセッション概要を伝え(もちろんこの時にはまだ何をするのか決めてないので、ぼかしまくった感じで)募集開始。そろそろ真面目に考えないとね~、と思いつつ仕事も忙しかったしで、ほっといたらあっという間に5月中旬。そろそろスライドだけでも書かねばと書き始めるものの、何を作るかは全く決まらず...。とりあえずの方向性としては

  1. 作ったものに参加者もその場でアクセスできる(だけど会場内のネットワークでは自マシンに参加者がアクセス出来ないはず)
  2. アーキテクチャを意識する
  3. MVC使いつつクライアントサイドモリモリ
  4. コードは参加者へのプレゼント

の4点。

※ネットワークについては、昨年のTechEDでダメだったのを経験してたので。

はてさて、どうしたものか。Twitterは他の人が絶対利用するだろうからパス。単純に外部サーバーにWebアクセスしてみんなでワイワイする感じで何がいいかな~、って考えるとどうしてもMvcGraffiti(みんなでお絵かき)とかぶる。となると、手堅い方法はメールか。ケータイなら絶対繋がるはず。メールということはPOP3の実装は必要だな~。メールなら写メがいいかも。という流れでとりあえずPOP3の実装と画像のリサイズ部分だけ先行コーディング。

いろいろ悩んだ挙句、普通に写真共有っていうところに落ち着くんだけど、どうやってクライアントサイドモリモリを達成するか。そこは後回しにしてアーキテクチャ。この時点でAppHarborをプラットフォームにして、ストレージはGoogle Storage使ってみようかと実験。でもGoogle Storageが思ったようにいかないからS3にチェンジ。モデルはCodeFirstで、ストレージは置き換えられるようにProviderとして実装。MVCでのサーバーサイドはシンプルにAPI的なものと、UA切り替え出来るような仕組みをどうするか考えつつ、ワーカーによるバックグラウンド処理でメールとストレージのつなぎをやろうと決める。UAに合わせてViewを切り替えるのはいろんなやり方があるけど、出回ってるやり方を実装してもつまらないので、随分悩んだ末にRoutingとViewEngineのコンビネーションで行う方法を思いつく。さすがオレ。

クライアントサイドはこのころすごく気になってたknockout.jsとModernizrを使うことで、うまいことやろうと思いつつ具体的なことは決めずにサーバーサイドをゴリゴリ作る。あと、それっぽくテストコードも用意することで、スタックというかレイヤというか、その辺を意識しやすいようにしておこう。

ちょっと横道にそれますが、ControllerのテストをしやすくするためにFormCollectionをパラメータに使う例をよく見ますね。でも意味ないですよね。そこは普通に入力モデルを渡せばいいじゃないっていうふうに思うわけですよ。だって、FormなりのValueProviderからModelBinder経由した入力モデルへのマッピングって、Controllerのテストじゃ無いからですよ。通常のFormを想定した場合、FormCollectionからUpdateModelをするなら、それはもうMVCが提供してくれるModelBinderのテストをするようなもんでしょう。意味無いじゃん!なので、ModelBinderのテストはMVCの開発チームに任せて、自分たちの書いたコードに対するテストを書きましょう。ただ、今回Controllerのテストは書いてないですけどね!てへ。

ここまで全然コード解説じゃないですけど、セッションで話したいのはそもそも製品の紹介じゃなくて「アプリケーションの作り方」。何をどういう設計で作るかを決めることで詰みです。「拳(コード)で語る」のがプログラマーですよ。だって、ASP.NET MVCも1~3へと進み、4以降になったら製品の使い方、コードの書き方なんて変わるわけですよ。Razorなんてものも出てくるし。だけど、考え方とか適用の仕方ってMVCっていうアーキテクチャスタイルとか、デザインパターンとかって基礎として使い続けるじゃん?そこ意識すること大事だと思うんす。

ただでも、このやり方は諸刃で、聞きに来てくれる層によってはドン引きされるんです(経験あり)。だって「お前の作ったアプリケーションを見に来たんじゃないんだよ!」とか「そのアプリでオレのプロジェクトは解決できない!」という意見もあるからね。いろいろです。どんな意見も、それぞれの人のコンテキストでは正論っす。ただ、自分にとっては、目の前の問題の答えを提示する局所最適じゃなく、全体最適を目指すほうが楽しい。なので、これからもこのやり方は変わらないでしょう。

はっ!熱くなってた!しっけいしっけい。

サーバーサイドが概ね出来たところで、クライアントサイドの実装に入るわけですが、PC,iPad,iPhone、そして日本が誇る超精密パーソナル通信機器、通称ガラケー対応も無視できない。モダンブラウザ向けにはjQuery Mobile使おうと思ってたんだけど、Azure担当大臣のだいちゃんが「jQuery Mobileでモテモテになるっす!」とか言い出しやがって、かぶるじゃねーかよ、的なね。まぁ、いいか。んじゃ、オレ適当に実装する、ということになりました。なので、見た目かなりしょぼくなったけど、オレのせいじゃないから!エロ大臣のせいだから!!Azureチーム金持ってるからって可愛いキャラ多すぎなんだよ。ASP.NET界隈では緑のキグルミしかいないっつーの。羨ましくなんか..うっぐ。泣かない。

はいはい。クライアントサイドでどういう感じに動かすのか、図に書いて最初に実装したのが、↓こんなやつ。

sample

受信画像をタイル状にランダム表示。緑も意識してみた。だけどコレがまたダサいのなんのって。イメージ通りに作ったはずなのに。自分のセンスに絶望。

sample2

ランダムがだめなのかと思って順番に表示するようにしたりしたけど...。根本的におかしい。マジやべー。

sample3

色がだめなのか!?と思って黒くしてみた。

sample4

で、最終的には↑こうなるんだけど、みんな知ってた?ウィンドウサイズに合わせてサムネイルの画像サイズは100→50→25と収まりよくなるようにリサイズするんだよ?

http://mvcphotos.takepara.com/

試してみてね。

ソースはこちら。

http://mvcphotos.codeplex.com/

Source Codeタブをクリックして右端のLatest VersionにあるBrowseで見たり、Downloadで取得してね。

source

書き疲れた...。もういい?仕様書もマニュアルもなしで、コードを追いかけるのも大変だと思うので、今回作成したmvcPhotosの実装をザックリ書きだしておきます。

  • POP3でメール受信
  • クラウドストレージとローカルストレージを切り替えやすくするためのストレージプロバイダ化
  • 画像のリサイズ
  • EF CodeFirstによるDAL
  • データベースアクセスをRepositoryにより抽象化
  • サーバーサイドでもDAL用のモデルと、入出力用のモデルを分けることでレイヤ分離と検証ルールの明確化
  • 動的画像リサイズを行うためのコンカレント制御と非同期Controller
  • 複数のUserAgentを同一Controllerで処理するためのViewEngineとRouting制御
  • モダンブラウザの判定と、動的スクリプト読み込みにModernizr
  • knockout.jsによるクライアントサイドでのMVVM実装
  • 自作Service Locatorと、DependencyResolver実装
  • DIを3パターン実装(探してみてね!)

こんな感じです。

ちなみにパネルディスカッションの最後でゴニョゴニョ言ってたことなんですけど「大事なのはMVCの心を理解しようとし、SoC - Separation of Concerns - 関心の分離を意識すること」。つまりきちんと役割を分離して、実装も可能なかぎり分離して参照関係を単純にしましょうね、と言いたかったですが言葉足らずのドヤ顔で失礼しました。

あと、一色さん、変なやりとりでスマセンした。失礼ぶっこいてスマセンした。ホントはすごいシャイボーイなんです。自分で言うのもなんですが、草食系男子なんです。型は古くて時化には強いタイプですけどシャバいやつなんす。

今後ともご贔屓に~。

2010年7月6日火曜日

DeserializingModelBinder

モダンチョキチョキズを知ってるのはどのくらい少数派なのかが気になる今日この頃。

ASP.NET MVC 2 Futuresに含まれているHtml.SerializeとDeserializeAttributeをクラスのプロパティに対して適用するのにお悩みな方へ。DeserializeModelBinder自体がなぜかDeserializeAttributeクラスのprivate sealedクラスと定義されてしまっているので、全く同じものを以下のように定義することで、ModelBinderAttributeを指定して比較的簡単に出来るようです。

  public class DeserializingModelBinder : IModelBinder
  {

    private readonly SerializationMode _mode;

    public DeserializingModelBinder() : this(SerializationMode.Plaintext) { }
    public DeserializingModelBinder(SerializationMode mode)
    {
      _mode = mode;
    }

    public object BindModel(ControllerContext controllerContext, 
ModelBindingContext bindingContext) { if (bindingContext == null) { throw new ArgumentNullException("bindingContext"); } var vpResult = bindingContext.ValueProvider
.GetValue(bindingContext.ModelName); if (vpResult == null) { // nothing found return null; } MvcSerializer serializer = new MvcSerializer(); string serializedValue = (string)vpResult.ConvertTo(typeof(string)); return serializer.Deserialize(serializedValue, _mode); } }

↑これはソースからコピペです。MvcSerializerが使えなかったらどうしようかと思いましたが、こちらは平気ですね。

  public class Division
  {
    public string Name { get; set; }
    public Person Boss { get; set; }
    public People People { get; set; }
  }

  [Serializable]
  [ModelBinder(typeof(DeserializingModelBinder))]
  public class People : List<Person>
  {}

  [Serializable]
  public class Person
  {
    public string Name { get; set; }
    public DateTime MemorialDay { get; set; }
  }

↑このようなモデルクラスたちを定義してみました。あえてPeopleクラスを定義しているのはModelBinderAttributeがプロパティに指定できないからです。DeserializeAttributeにいたってはParameterにしか指定できないし、ModelMetadataProviderらへンに手を入れる必要がある気がしなくもなく(たぶんメタデータを見てModelBinderを切り替えるような仕組みでしょうか)、難しそうだったので使っていません。

このようにクラスを定義した上で、ModelBinderAttributeでModelBinderを指定する方法が比較的簡単でスマート(?)じゃないかと思います。こうしておくとアクションでは何も意識する必要なく以下のようにアクションパラメータを生成してくれるようになります。

  public ActionResult Division()
  {
    var model = new Division
    {
      Name = "ブチャラティチーム",
      Boss = new Person { Name = "ブローノ・ブチャラティ", 
MemorialDay = new DateTime(2010, 1, 1) }, People = new People { new Person {Name = "ジョルノ・ジョバァーナ",
MemorialDay = new DateTime(2010, 2, 1)}, new Person {Name = "レオーネ・アバッキオ",
MemorialDay = new DateTime(2010, 3, 1)}, new Person {Name = "グイード・ミスタ",
MemorialDay = new DateTime(2010, 4, 1)}, new Person {Name = "ナランチャ・ギルガ",
MemorialDay = new DateTime(2010, 5, 1)}, new Person {Name = "パンナコッタ・フーゴ",
MemorialDay = new DateTime(2010, 6, 1)} } }; return View(model); } [HttpPost] public ActionResult Division(Division model) { return View(model); }

Viewは以下のようにシンプルです。

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<MvcApplication1.Models.Division>" %>
<%@ Import Namespace="Microsoft.Web.Mvc" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
  Division
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

    <h2>Division</h2>

  <% using(Html.BeginForm("Division","Home")) { %>
    <%= Html.EditorFor(m=>m.Name) %>
    <h3>Boss</h3>
    <%= Html.EditorFor(m=>m.Boss) %>

    <h3>People</h3>
    <% foreach (var person in Model.People) { %>
      <%= Html.DisplayFor(m=>person) %>
    <% } %>

    <%= Html.Serialize("People",Model.People) %>
    <input type="submit" value="送信" />
  <% } %>
</asp:Content>

これを実行するとこのようにきちんと復元してくれます。

serialize1 serialize2 serialize3

@jsakamotoさん、いかがでしょうか?

2009年2月11日水曜日

ASP.NET MVC RCの入力検証

とにかく、簡単に検証したい。

RCになってからIDataErrorInfoをモデルクラスに実装することで、DefaultModelBinderがプロパティ毎にValidationを呼び出すようになったりました。なのでコレを上手く生かしたい。 なおかつIDataErrorInfoの実装時にプロパティ名毎の検証ロジックを自分でコーディングしないでDynamic Dataで導入された、DataAnnotationsを使うようにしたい。

結果的にかなりシンプルに実装出来ることが判明。自分でDefaultModelBinderを派生させたクラスを作る必要もなく! DataAnnotationsっていうのはナオキさんの書いてるASP.NET Dynamic Dataの記事「簡単なデータ編集はお任せ! ASP.NET Dynamic Dataアプリケーション:CodeZine」に分かりやすい説明があるのでそちらを見てね。

簡単に言うとクラスのプロパティに属性ベースで検証ルールとエラーメッセージを指定するもの。 データベースのモデルクラス(DBO)に、検証ルールを入れたいときなんかにはMetadataType属性をクラスに指定しとけば、別のクラスで検証ルール属性を定義できたり、そりゃ~もう、便利そうでたまらない機能ですよ。

ScreenCast - How to work with DataAnnotations (VS2008 SP1 Beta) - Noam King's Blog ↑ここでビデオでもどんなものか確認できます。

ちょっと古いしUIに関することにも触れてるけど、DataAnnotationsの簡単な使い方が分かると思います(動的検証はまた別の話なので今回は触れてません)。

DataAnnotationsそのものは検証を実行してくれないので、検証の実行は自分で書く。書くと行っても属性指定しているValidationAttributeクラス(各検証属性の基底クラス)のIsValid()を呼ぶだけなんだけどね。

この呼び出し部分をIDataErrorInfoのItemプロパティ(インデクサ)に書く。 入力検証はDBOじゃなくViewModelに属性を定義して、そこでコードを書くようにしたい。なので、イメージ的には↓こんな感じ(図にしたほうが分かりやすいんだけど、面倒くさい)。

ViewModel : IDataErrorInfo
{
// モデルのプロパティを定義
 [検証属性A]
プロパティA
 [検証属性B]

プロパティB

// IDataErrorInfoの実装
Error {
 モデルに対する検証
}
this[columnName] {
 プロパティに対する検証
  → 検証属性クラスのIsValidを呼ぶ
}
} 
簡単そうでしょ?

ということで、前に書いたコード(ASP.NET MVC RCでIDataErrorInfoの使い方)をベースに進めることにします。 ここでちょっと訂正です。↑このエントリーでList<WeaponViewModel>の復元がDefaultModelBinderでは出来ないと書いちゃってて、個別にUpdateModelを呼び出さないきゃいけないよね~、なんて思いっきり間違いを書いてました。

コード見て分かるとおりPersonViewModelクラスのWeaponsがプロパティじゃなくてパブリックメンバ変数になってるから復元出来ないだけでした。 プロパティにすればちゃんと復元されます。

何でかというと、DefaultModelBinderで値を復元するときに、どのプロパティを対象にするか抽出するのに TypeDescriptor.GetProperties()を使ってるから(DefaultModelBinderクラスの316行目 GetModelPropertiesのコード参照)。名前の通りメンバ変数じゃなくてプロパティを抽出する関数なんだよね。そりゃ復元されないわ。 ってことで、DefaultModelBinder最強! ごにょごにょ言うよりコード見た方がたぶん早いと思うんでコードを載せときます。

1.IDataErrorInfoの実装

  public class BaseViewModel : IDataErrorInfo
 {
   public virtual string Error
   {
     get { return null; }
   }

   public string this[string columnName]
   {
     get
     {
       // ここで検証実行させる
       return this.Validate(columnName);
     }
   }

   // ↑ここまでがIDataErrorInfoの実装

   private PropertyInfo GetProperty(string name)
   {
     return this.GetType()
                .GetProperties()
                .Where(p => p.Name == name)
                .FirstOrDefault();
   }

   private string Validate(string columnName)
   {
     var property = this.GetProperty(columnName);
     if (property == null)
       return "致命的!";

     return Validate(property);      // 検証
   }

   private string Validate(PropertyInfo property)
   {
     // 検証ルール取得
     var validators = property.GetCustomAttributes(typeof(ValidationAttribute), true);
     foreach (ValidationAttribute validator in validators)
     {
       var value = property.GetValue(this, null);
       // 検証!
        if (!validator.IsValid(value))
         return validator.ErrorMessage;
      }

     return null;
   }
 } 
BaseViewModelクラスがIDataErrorInfoを実装。ViewModelは基本的にこのクラスを派生させる。

何でかというとItemインデクサの処理はどのクラスでも全く一緒だからっていうのと、Errorはシンプルなモデルクラスなら空(null)でイイから。 太字の部分がValidationAttributeの呼び出し。1つのプロパティには複数の属性をセットしてもいいので、全部の検証がOKかどうかチェック。

2.ViewModelクラスの定義

  public class WeaponViewModel : BaseViewModel
 {
    [Required(ErrorMessage="タイプは絶対!")]
   [StringLength(10, ErrorMessage = "タイプは10文字以内でね")]
    public string Type { get; set; }

    [Required(ErrorMessage = "名前は絶対!")]
   [RegularExpression("[^a-zA-Z0-9]*", ErrorMessage = "名前に半角英数含んだらダメ")]
    public string Name { get; set; }
 }

 public class PersonViewModel : BaseViewModel
 {
   public int Id { get; set; }

   [Required(ErrorMessage="名前は?")]
   public string FirstName { get; set; }

    [Required(ErrorMessage="名字は?")]
    public string LastName { get; set; }

    [Required(ErrorMessage="整数で入れてね")]
   [Range(0,150,ErrorMessage="0歳から150歳で")]
    public int? Age { get; set; }

   // ちゃんとプロパティにしときます。
   public List Weapons { get; set; }

   public PersonViewModel()
   {
     Weapons = new List();
   }

   public override string Error
   {
     get
     {
       if (Weapons == null || Weapons.Count == 0)
         return "武器、っていうか必殺技は?";

       return null;
     }
   }
 } 

※太字がDataAnnotationsのValiudationAttribute。 PersonViewModelではモデルのエラーチェックをしたい(Weaponsプロパティのアイテム数チェック)から、Errorをオーバーライド。 ホントはRequire属性で判定出来るんじゃないかと思ってたんだけど、Listに1件もポストされない時に呼び出されないってことが判明(復元対象の値が存在しないから)。 試しにValidationAttributeクラスを派生させて、ListRequireAttributeクラスなんてものを書いてみたけど、そもそも呼び出してくれないから意味なかった。

  public class ListRequireAttribute : ValidationAttribute
 {
   public override bool IsValid(object value)
   {
     var list = value as IList;

     if (list == null)
       return false;

     if (list.Count == 0)
       return false;

     return true;
   }
 } 

※全く無意味なクラス。 ↑ こんなクラスを書いても、List<T>の検証が呼び出されない(Listに1つでもアイテムが追加されるなら呼び出されるけど、何も追加されない場合はスルー)ので、IDataErrorInfo.Errorをオーバーライドしてモデル検証としてチェックするようにしました。 AgeプロパティがintじゃなくてNullable<Int32>なのは、何も入力しなかった場合にintだと0になっちゃうのと、整数じゃなくて実数を入れたときにも0になっちゃう(intの初期値)のが都合悪いからです。

3.DefaultModelBinderで値を復元

    [AcceptVerbs(HttpVerbs.Post), ValidateAntiForgeryToken]
   public ActionResult Edit(int id, PersonViewModel person)
   {
     if(ModelState.IsValid)
       return RedirectToAction("Index");

     return View(person);
   } 

ココまででコントローラでは普通に復元されるようになります。 入力エラーが分かりやすくなるように、Viewも少しいじったのでEdit.aspxのソースも貼り付けときます(と、言ってもValidationMessageを入れただけ)。

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<Mvc.RC.Models.PersonViewModel>" %>

<asp:Content ID="Content1" ContentPlaceHolderID="head" runat="server">
   <title>Edit</title>
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

   <h2>Edit</h2>

   <%= Html.ValidationSummary() %>

   <% using (Html.BeginForm()) {%>

       <fieldset>
           <legend>Fields</legend>
           <p>
               <label for="Id">Id:</label>
               <%= Html.Encode(Model.Id) %>
           </p>
           <p>
               <label for="FirstName">FirstName:</label>
               <%= Html.TextBoxFor(p=>p.FirstName) %>
               <%= Html.ValidationMessage("FirstName", "*") %>
           </p>
           <p>
               <label for="LastName">LastName:</label>
               <%= Html.TextBoxFor(p=>p.LastName) %>
               <%= Html.ValidationMessage("LastName", "*") %>
           </p>
           <p>
               <label for="Age">Age:</label>
               <%= Html.TextBoxFor(p=>p.Age) %>
               <%= Html.ValidationMessage("Age", "*") %>
           </p>
         
           <p>
             <% foreach(var weapon in Model.Weapons){
                  var index = Model.Weapons.IndexOf(weapon);
                  %>
               タイプ
               <%= Html.TextBox(string.Format("Weapons[{0}].Type", index), weapon.Type)%>
               <%= Html.ValidationMessage(string.Format("Weapons[{0}].Type", index), "*")%>
               名前
               <%= Html.TextBox(string.Format("Weapons[{0}].Name", index), weapon.Name)%>
               <%= Html.ValidationMessage(string.Format("Weapons[{0}].Name", index), "*")%>
               <br />
             <% } %>
           </p>
           <% = Html.AntiForgeryToken() %>
           <p>
               <input type="submit" value="Save" />
           </p>
       </fieldset>

   <% } %>

   <div>
       <%=Html.ActionLink("Back to List", "Index") %>
   </div>

</asp:Content>

これを実行したときの画面が↓これら。

img.aspx10

前回同様、一覧。Editでルフィーを選択。

img.aspx11 名前とタイプのところが、List<WeaponsViewModel>の入力フォーム。 この状態のまま、Saveボタンを押してポストする。

img.aspx12

バッチリ復元されました。 まぁ、これだとDataAnnotations関係ないので、あえて入力エラーになるように、フォームの内容を変更。

img.aspx13 今度はこれをポスト。

img.aspx14 すると、DataAnnotationsがちゃんと動作してるのが確認できます。 すばらしい。

ただ、コレだとちょっとエラーメッセージが分かりにくいと思う。 メッセージそのものの文章が変とかっていう話じゃなくて、List<WeaponsViewModel>のどのアイテムがエラーなのかが分かりにくいんじゃないかと。

なので、エラーメッセージにアイテムのインデックスを含めたい。 でも、ValidationAttributeクラスのErrorMessageって固定文字列。動的に生成出来ないですよね。 これは参ったな~。DataAnnotationsの限界か!? と、思ったけど、ちょっと待て。 エラー情報は、どのフォームフィールドなのかと入力値(ValueProviderResult)、それとエラーメッセージ(ModelError)は自動でModelStateDictionaryに入るので、これを強制的に書き換えればいいんじゃないかななんて思ったわけです。 なので、エラーメッセージにインデックス番号を含めるように置き換える拡張メソッドをModelStateDictionaryに追加することにしました。

4.ModelStateDicrionaryの拡張メソッド

  public static class ModelStateExtensions
 {
   public static void ReplaceSequencialErrorMessage(this ModelStateDictionary modelState, string prefix, string format)
   {
     foreach (var ms in modelState)
     {
       var replaceErrors = new Dictionary();
       if (ms.Key.StartsWith(prefix + "["))
       {
         // 置き換え対象のエラー検索
         foreach (var error in ms.Value.Errors)
         {
           if (!string.IsNullOrEmpty(error.ErrorMessage))
           {
             var start = prefix.Length + 1;
             var end = ms.Key.IndexOf("]", start);
             if (end > start)
             {
               var indexVal = ms.Key.Substring(start, end - start);
               int index;
               if (int.TryParse(indexVal, out index))
               {
                 replaceErrors.Add(error, new ModelError(
                   format.NamedFormat(new { index = index + 1, message = error.ErrorMessage })
                 ));
               }
             }
           }
         }
       
         // 消して追加
         foreach (var e in replaceErrors)
         {
           ms.Value.Errors.Remove(e.Key);
           ms.Value.Errors.Add(e.Value);
         }
       }
     }
   }
 } 

ModelError のErrorMessageが直接書き換えできるならもっと簡単に書けるけど、残念ながらこのプロパティはリードオンリー(getterしかない)。なので、ModelErrorを削除してErrorMessageを書き換えたModelErrorを追加することで、この機能を実現。なんか遠回りだね。 エラーメッセージを置き換えたいプロパティ名と、どういう書式でエラーメッセージを書き換えるのかのフォーマット文字列を引数に指定する。ここで先日作成したNamedFormat(いまだ最速?)を使ってみました(ソース中太字のところ)。 これらを利用するようにアクションを少し変更。

    [AcceptVerbs(HttpVerbs.Post), ValidateAntiForgeryToken]
   public ActionResult Edit(int id, PersonViewModel person)
   {
     if(ModelState.IsValid)
       return RedirectToAction("Index");

      ModelState.ReplaceSequencialErrorMessage("Weapons", "{index}番目の{message}");
      return View(person);
   } 

※これまた1行追加するだけ。 この状態でさっきと同じ入力エラーを発生させる。

img.aspx15

5.DefaultModelBinderのイベント

ちょっと、長くなってきたけど最後にRCになって変更されたModelBinderのイベントについて少し確認してみました。と、言っても、DefaultModelBinderを派生させて各タイミングでログを出すだけなんですけどね。

リリースノート17ページの「ModelBinder API Changes」にまるっと全部書いてることですけど、以下のメソッドをオーバーライドすることで、各イベントに合わせて処理を実行出来るようになってます。 1. CreateModel 2. OnModelUpdating 3. GetModelProperties 4. BindProperty a. OnPropertyValidating b. SetProperty c. OnPropertyValidated 5. OnModelUpdated まんまですが、以下のような単純なクラスを書いて、PersonViewModelとWeaponsViewModelのModelBinderとして宣言してみました。

  public class DebugModelBinder : DefaultModelBinder
 {
   public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
   {
     Debug.WriteLine(
       string.Format("BindModel - {0}", bindingContext.ModelName)
     );
     return base.BindModel(controllerContext, bindingContext);
   }


protected override void BindProperty(ControllerContext
controllerContext, ModelBindingContext bindingContext,
System.ComponentModel.PropertyDescriptor propertyDescriptor)
   {
     Debug.WriteLine(
       string.Format("BindProperty - {0}", propertyDescriptor.Name)
     );
     base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
   }


protected override object CreateModel(ControllerContext
controllerContext, ModelBindingContext bindingContext, Type modelType)
   {
     Debug.WriteLine(
       string.Format("CreateModel - {0}", modelType.Name)
     );
     return base.CreateModel(controllerContext, bindingContext, modelType);
   }


protected override System.ComponentModel.PropertyDescriptorCollection
GetModelProperties(ControllerContext controllerContext,
ModelBindingContext bindingContext)
   {
     Debug.WriteLine("GetModelProperties");
     return base.GetModelProperties(controllerContext, bindingContext);
   }

   protected override void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)
   {
     Debug.WriteLine(
       string.Format("OnModelUpdated")
     );
     base.OnModelUpdated(controllerContext, bindingContext);
   }

   protected override bool OnModelUpdating(ControllerContext controllerContext, ModelBindingContext bindingContext)
   {
     Debug.WriteLine(
       string.Format("OnModelUpdating")
     );
     return base.OnModelUpdating(controllerContext, bindingContext);
   }


protected override void OnPropertyValidated(ControllerContext
controllerContext, ModelBindingContext bindingContext,
System.ComponentModel.PropertyDescriptor propertyDescriptor, object
value)
   {
     Debug.WriteLine(
       string.Format("OnPropertyValidated - {0} = {1}",
                     propertyDescriptor.Name,
                     value )
     );
     base.OnPropertyValidated(controllerContext, bindingContext, propertyDescriptor, value);
   }


protected override bool OnPropertyValidating(ControllerContext
controllerContext, ModelBindingContext bindingContext,
System.ComponentModel.PropertyDescriptor propertyDescriptor, object
value)
   {
     Debug.WriteLine(
       string.Format("OnPropertyValidating - {0} = {1}",
                     propertyDescriptor.Name,
                     value)
     );

     return base.OnPropertyValidating(controllerContext, bindingContext, propertyDescriptor, value);
   }


protected override void SetProperty(ControllerContext
controllerContext, ModelBindingContext bindingContext,
System.ComponentModel.PropertyDescriptor propertyDescriptor, object
value)
   {
     Debug.WriteLine(
       string.Format("SetProperty - {0} = {1}",
                     propertyDescriptor.Name,
                     value)
     );
     base.SetProperty(controllerContext, bindingContext, propertyDescriptor, value);
   }
 } 

※BindingContextに復元に必要なすべての情報が入ってます。 ViewModelの先頭で以下の属性を追加。

[ModelBinder(typeof(DebugModelBinder))] public class PersonViewModel : BaseViewModel {...}

これを試すために、ルフィーのデータをそのままポストしてみました。 出力された内容が↓こちら(インデントは自分で入れてます)。

BindModel - person CreateModel - PersonViewModel OnModelUpdating GetModelProperties BindProperty - Id OnPropertyValidating - Id = 1 SetProperty - Id = 1 OnPropertyValidated - Id = 1 BindProperty - FirstName OnPropertyValidating - FirstName = ルフィー SetProperty - FirstName = ルフィー OnPropertyValidated - FirstName = ルフィー BindProperty - LastName OnPropertyValidating - LastName = モンキー SetProperty - LastName = モンキー OnPropertyValidated - LastName = モンキー BindProperty - Age OnPropertyValidating - Age = 17 SetProperty - Age = 17 OnPropertyValidated - Age = 17 BindProperty - Weapons BindModel - Weapons[0] CreateModel - WeaponViewModel OnModelUpdating GetModelProperties BindProperty - Type OnPropertyValidating - Type = ゴムゴム SetProperty - Type = ゴムゴム OnPropertyValidated - Type = ゴムゴム BindProperty - Name OnPropertyValidating - Name = ガトリング SetProperty - Name = ガトリング OnPropertyValidated - Name = ガトリング OnModelUpdated BindModel - Weapons[1] CreateModel - WeaponViewModel OnModelUpdating GetModelProperties BindProperty - Type OnPropertyValidating - Type = ゴムゴム SetProperty - Type = ゴムゴム OnPropertyValidated - Type = ゴムゴム BindProperty - Name OnPropertyValidating - Name = 鞭 SetProperty - Name = 鞭 OnPropertyValidated - Name = 鞭 OnModelUpdated OnPropertyValidating - Weapons = System.Collections.Generic.List`1[Mvc.RC.Models.WeaponViewModel] SetProperty - Weapons = System.Collections.Generic.List`1[Mvc.RC.Models.WeaponViewModel] OnPropertyValidated - Weapons = System.Collections.Generic.List`1[Mvc.RC.Models.WeaponViewModel] OnModelUpdated

無駄に長い貼り付けになっちゃったけど、BindModelでインスタンス化する変数名(またはプロパティ名)を決定し、CreateModelでインスタンス作成、OnModelUpdating~OnModelUpdatedの中でプロパティ毎の処理。プロパティはモデルと同じように BindPropertyで対象を決めて、SetPropertyでセット。セットの前後でOnPropertyUpdatingと OnPropertyUpdatedが呼ばれる。

なので、ModelBinderの中で直接検証したりするときには、OnModelUpdatedでモデル検証、OnPropertyUpdated(セット前に検証するならOnPropertyValidating)でプロパティの検証でいいのかな。

思いの外、挙動の確認に手間取ったけど、リリース版でもModelBinderはこの流れだろうから、DataAnnotationsを使った検証はかなり有効な手段だと思います。お試しアレ。

2009年2月7日土曜日

強力になったDefaultModelBinder

配列を保持するときに、コレまでHiddenにプレフィックス+”.Index”の名前でインデックス番号を保持しておかないと、きちんと復元してくれなかったのが、Index無しでもちゃんと復元出来るようになってる!

DefaultModelBinderクラスのUpdateCollectionのコードのリファクタリングを進めて、Index値を内部でループで回すように変更した結果だね。

なので0から始まる連番じゃないのは困っちゃう(-1から始めるとか1,3,5とか)けど、基本的に連番にするだろうから問題ないと思われる。 そもそも連番じゃないなら、違うフィールド(Hidden)に持つなりするはずだし。

コレまで、このHiddenのIndexが曲者で、一度Postされたあとに消して(ModelStateDictionaryの値が自動で復元されるルールが適用されて) おかないときちんと復元出来なかったのが、Indexそのものを使用しなくなったおかげで、Indexの出力も削除も不要に。

RC ModelBinder breaking changes for collections - ASP.NET Forums

コレクションをInput要素に展開する場合に、若い番号の値群を削除してもModelStateから若い番号の値が復元されてしまうってことなんで、結局 Indexを持つFormを作る時には、自分でModelStateの値を消して再構築するなり、Input生成時に値を渡すようにするか、 ViewDataに入れとくかという事はやらないとね。

ベータになって

↑こういう問題があったのを↓解決させてた。

あぁ~、そうか、こうすればいいんだ

※AttemptedValueに直接null入れてるけど。

ベータの時のコードをRCに移植する際にエラーになってしまった物として、ModelStatesに入ってる値を消す方法がコレまで ModelState.Value.SetAttempedValueだったのが、RCから綺麗さっぱりそのメソッドは無くなって (ValueProvider経由のValueProviderResultで取得)、代わりにModelStates.SetModelValueでキー名とValueProviderResultを渡すようになったので、この問題に気がついた次第です。

Custom ModelBinder and Release Candidate - ASP.NET Forums

ベータの消し方 foreach(var ms in modelStates.Where(ms=>ms.Key == "消したいキー")) { ms.SetAttemptedValue(null); // これでModelStateの値が消える }

RCの消し方 foreach(var ms in modelStates.Where(ms=>ms.Key == "消したいキー")) { modelStates.SetModelValue( ms.Key, new ValueProviderResult(null,null,null) ); // これでModelStateの値が消える }

DefaultModelBinderがらみでもう一つ。 フォームポストされるデータを、アクションの引数にクラスを使って復元させるとき、クラスにHttpPostedFileBase(ファイルアップロード)を含んでいると、そのままじゃ復元してくれない罠。 何でだろね。あと、デフォルト動作としてValidateRequestが有効になるようになってる。

例えば、以下のようなアクションをデフォルトで作成されるHomeControllerに定義。

    [ActionName("Index"), AcceptVerbs(HttpVerbs.Post)]
   public ActionResult IndexPost(string textArea, HttpPostedFileBase uploadFile)
   {
     return View();
   } 
んで、Indexページに以下のコードを書く。
  <% using (Html.BeginForm("Index", "Home", FormMethod.Post, new { enctype = "multipart/form-data" })) { %>

 <fieldset>
 <legend>フォームテスト</legend>
 <% = Html.TextArea("textArea")%><br />
 <input type="file" name="uploadFile" /><br />
 <% = Html.SubmitButton("send", "送信")%>
 </fieldset>

 <% } %>

img.aspx4 こんな感じの単純な物なんだけど。 例えばコレで、テキストエリアに"<script />"なんて入れて送信すると...。

img.aspx5 見慣れたエラーが出るね。 だけど、アクションにRCで導入されたValidateInputAttributeを指定して以下のような定義に書き換えると、ASP.NETの入力チェックがスルーされてコレまでと同じ動きをしてくれます。

    [ValidateInput(false), ActionName("Index"), AcceptVerbs(HttpVerbs.Post)]
   public ActionResult IndexPost(string textArea, HttpPostedFileBase uploadFile)
   {
     return View();
   } 
Viewsフォルダのweb.configやaspxのPageディレクティブ指定のValidateRequestはページに対しての指定で、アクションに対する指定じゃないので、気をつけましょう。

WebFormsの時はPageにPostBackされてたけどMVCだとControllerにPostするので、その違いがこんな所に出てきてます。まぁ、デフォルト安全動作っていうのはいいことだね。ベータ以前から移行の場合は修正箇所は増えるけど。

肝心のファイルアップロードといえば、以下の通り。

img.aspx6

普通に入ってるね(Vistaにサンプルで入ってる写真をポスト)。 今度はアクションの引数に自作クラス(ViewModel)を用意して、DefaultModelBinderに復元してもらうようにする場合。

以下のようなクラスを用意。

  public class FormPost
 {
   public string textArea { get; set; }
   public HttpPostedFileBase uploadFile { get; set; }
 } 

んで、アクションを以下のように書き換え。

    [ValidateInput(false), ActionName("Index"), AcceptVerbs(HttpVerbs.Post)]
   public ActionResult IndexPost(FormPost post)
   {
     return View();
   } 

そうすると今度はどうなるかと言うと...。 ※Viewは書き換えてないです。

img.aspx7

post.uploadFileはnullになってますね。見にくいですけど。 これは、以下のようにViewを書き換えることでちゃんととれるようになります。

  <% using (Html.BeginForm("Index", "Home", FormMethod.Post, new { enctype = "multipart/form-data" })) { %>

 <fieldset>
 <legend>フォームテスト</legend>
 <% = Html.TextArea("textArea")%><br />
 <input type="file" name="uploadFile" /><br />
 <% = Html.Hidden("uploadFile.exists", true) %>
 <% = Html.SubmitButton("send", "送信")%>
 </fieldset>

 <% } %>

input=fileのフォーム要素と同じ名前+".exists"のhiddenを作成して、valueに"true"を入れる。 これだけなんだけど、なかなか気がつかないよね。

img.aspx8

今度はuploadFileがnullじゃな~い。 HttpPostedFileBase bug when binding - ASP.NET Forums

2009年5月26日火曜日

UnityとEntityのAttach

第2回 スキャフォールディング機能で軽々DB連携アプリケーション - @IT

まだ2回目だけど、楽しみにしてる連載です。今回の記事で少し気になる部分があったので、実際に動くコードを書いてみました。暇人...。いやいや、お勉強デス!

その気になる部分っていうのが、ページ5の「既存のレコードを変更したい今回のようなケースでは、UpdateModelメソッドを使用する必要がある。」という部分。いや、もちろん、ModelBinderのみでDBに保存なんてしないし、これが間違ってるという分けじゃないけど、単純に出来る出来ないという話なら出来るっていう話です。

EntityKey and ApplyPropertyChanges() - Stack Overflow

なんだかんだと、実際にコードを書いてみたわけじゃなく、たんなる耳年増なだけだと、ちょっとカッコ悪し。

efunity2

Home/Indexが一覧。Editを用意。

efunity3

"Chai"を"Chai XXX"に編集。

efunity4

ちゃんと保存されました。

↓これがObjectContextにアタッチされていないエンティティを使って、DB更新してみるコード。

   public void Save<TEntity>(TEntity entity, Func<TEntity, TEntity> setIDFunc) 
where TEntity : new() { var entitySetName = _dataContext.DefaultContainerName + "." + EntitySetName(typeof(TEntity)); setIDFunc(entity); // 空のエンティティをアタッチしておいて、更新情報はクリア _dataContext.AttachTo(entitySetName, setIDFunc(new TEntity())); _dataContext.AcceptAllChanges(); _dataContext.ApplyPropertyChanges(entitySetName, entity); }

オブジェクトのアタッチ (Entity Framework)

AttachToでObjectContextに空のエンティティをアタッチしておいて、ModelBinderで復元したエンティティの値をApplyPropertyChangesで反映させる流れです。ObjectStateManagerを使うともっと綺麗にできるのかも?

↑このコードはRepositoryクラスに。コレを呼び出すControllerコードは↓。

    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult Edit(int id, Product entity)
    {
      if (ModelState.IsValid)
      {
        _repository.Save<Product>(entity, e =>
        {
          e.ProductID = id;
          return e;
        });
        _repository.SaveChanges();

        return RedirectToAction("Index");
      }

      return View(entity);
    }

認証、認可、入力検証ははしょってるんですが、何となく雰囲気は伝わるかな~、と思います。データベースはNorthwindで、Entity Frameworkを使ってProducts/Categoriesだけのエンティティクラスを作成してます。

efunity

HomeControllerのIndexが一覧ページになるようにしておいて、Editを追加しただけのプロジェクトで試してます。今回はコレに加えてUnityを使ってDIでObjectContextをRepositoryにコンストラクタインジェクションと、RepositoryをControllerにコンストラクタインジェクションさせるようにしてみました。

Unityを使ったControllerFactoryなんかはMvcContribにもコードがあったりするので、そちらを参考にするのが近道です。

でもMvcContribのUnityControllerFactoryはLifetimeManagerを指定しないので、Resolveの度に新しいインスタンスを作ります(TransientLifetimeManager)。HttpContextにObjectContextを入れておいて、同じコンテキストなら無駄使いしないようにするためのHttpContextLifetimeManagerクラスを用意。「ASP.NET MVC Tip: Dependency Injection with Unity Application Block - Shiju Varghese's Blog」に書かれてる、HttpContextLifetimeManagerを使わせてもらいました。Get/Set/RemoveのoverrideでHttpContextを使うようにしてるだけですね。

ObjectContextのインスタンスがどうなってるのかを確認するコードをHomeControllerに書いて確認。

    INorthwindRepository _repository;

    public HomeController(INorthwindRepository repository)
    {
      _repository = repository;
      _repository.Debug("Constructor");
    }

    protected override void OnActionExecuting(ActionExecutingContext filterContext)
    {
      _repository.Debug("OnActionExecuting");
      base.OnActionExecuting(filterContext);
    }

    protected override void OnActionExecuted(ActionExecutedContext filterContext)
    {
      _repository.Debug("OnActionExecuted");
      base.OnActionExecuted(filterContext);
    }

    public ActionResult Index()
    {
      var httpApp = HttpContext.ApplicationInstance as IUnityContainerAccessor;
      new NorthwindRepository(
        (NorthwindEntities)httpApp.Container.Resolve(typeof(NorthwindEntities))
      ).Debug("Other-1");

      var list = _repository.All<Product>();

      new NorthwindRepository(
        (NorthwindEntities)httpApp.Container.Resolve(typeof(NorthwindEntities))
      ).Debug("Other-2");

      return View(list);
    }

Debug内ではObjectContext.GetHashCode()を出力するようにしてます。コンストラクターで渡されるRepositoryはUnityに任せたもので、OnActionExecutingとOnActionExecutedではコンストラクタで渡されたものを出力し、Index内でそれぞれ(Other-1/2)新しくリポジトリのインスタンスを作成(Resolve)して出力させてます。

TransientLifetimeManagerを使った出力。

Constructor:6658142
OnActionExecuting:6658142
Other-1:5603269
Other-2:50559794
OnActionExecuted:6658142

Otherはそれぞれ違うインスタンスが生成されてますね。

HttpContextLifetimeManagerを使った出力。Other-1/2は全然違うのが出力されますね。

Constructor:60183783
OnActionExecuting:60183783
Other-1:60183783
Other-2:60183783
OnActionExecuted:60183783

Otherも同じインスタンスです。お利口さんです。Global.asaxに以下のコードを追加してUnityに登録してます。

    void InitializeContainer()
    {
      if (_container == null)
        _container = new UnityContainer();

      IControllerFactory controllerFactory = new UnityControllerFactory(_container);
      ControllerBuilder.Current.SetControllerFactory(controllerFactory);

      // Register
      _container.RegisterType<NorthwindEntities>(
        /*
         * ContainerControlledLifetimeManagerを使うとSingleton。
         * デフォルトはTransientLifetimeManager
         */
          new HttpContextLifetimeManager<NorthwindEntities>()
                )
                .Configure<InjectedMembers>()
                .ConfigureInjectionFor<NorthwindEntities>(
                  new InjectionConstructor(
                    ConfigurationManager.ConnectionStrings["NorthwindEntities"].ConnectionString
                    )
                );
      _container.RegisterType<INorthwindRepository, NorthwindRepository>();
    }

とりあえず、今回はProductデータだけを利用したけど、他にもいろいろなテーブルを同じコードで簡単に処理できるように「An Irishman Down Under - Polymorphic Repository for ADO.Net Entity Framework - Keith Patton's blog」に書かれてるようなジェネリックなリポジトリを書いてみようかな~と試してみたんですが、どうですかね。あんまり便利な気がしない。簡単な機能実装ならいいかもしれないけど、この辺は機能ドメイン毎にちゃんと書いた方がいい気がする。ところで、EntitySet名ってエンティティクラスから簡単に取得する方法ってないんですかね。EntityKeyには入ってるみたいだけど、アタッチして無いとnullで取り出せないじゃないですか。今回はずるっこして、エンティティクラス名+"s"として生成してます。

んん~。今回もプロジェクト添付しておくので、動かしてみたい方はどーぞー。あ、データベースファイルは添付してないです。

2009年10月7日水曜日

V2のFuturesにViewStateが!?

Exploring the ASP.NET MVC 2 futures assemby

小野さんに振られたので↑こちらのエントリに書かれてるViewStateについての調べてみました。こういうきっかけが無いとソースを追いかけない自分に少し反省。

まさかホントにASP.NET MVCにViewStateを持ち込むのか?と、疑いたくなるようなエントリだけど何となくサンプルとして提示されてるコードが怪しい。そのまま転載させてもらうと↓こうですよ。

<% using (Html.BeginForm()) {%>
    <%Html.Serialize("person", Model); %>
    <fieldset>
        <legend>Edit person</legend>
        <p>
            <%=Html.DisplayFor(p => Model.FirstName)%>
        </p>
        <p>
            <%=Html.DisplayFor(p => Model.LastName)%>
        </p>
        <p>
            <label for="Email">Email:</label>
            <%= Html.TextBox("Email", Model.Email) %>
            <%= Html.ValidationMessage("Email", "*") %>
        </p>
        <p>
            <input type="submit" value="Save" />
        </p>
    </fieldset>
<% } %>

Html.Serialize(“person”,Model)ってなんか怪しいですよね。おまえ、ホントにViewStateを吐いてくれるのか?と。1つのViewに何個も書いたらどうなるんだよ、とか、ポストバックしたControllerでコントロールツリーを構築するのか、とかなんやかんやデス。

で、考えててもラチがあかないので、サンプル書いて試してみました。

  [Serializable]
  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; }
  }

まずは、前回も使ったDrinkクラスにSerializable属性を追加。これつけないとそもそもシリアライズ出来ないです。どこでシリアライズしてるのかFuturesのソースを追いかけると、MvcSerializerクラスで実装してます。ちなみにシリアライズの方式としてPlaintext、Encrypted、Signed、EncryptedAndSignedの4種類があり、初期値はPlaintext。これはSystem.Web.UI.ObjectStateFormatterを使ってシリアライズしてて。って、おや?マジViewStateなのか?まぁ、いいや。

なのでSerialize属性をつけるわけですが、上記クラスに値を入れてサンプル通りにView書いて実行しても何も出力されない...。

<% Html.Serialize("drink",Model); %>

サンプルだからなんかおかしいのかな~。気になるので更にソースを読み進めると、Html.SerializeはそもそもMvcHtmlStringクラスを返してきます。これは、あれですよね、ASP.NET 4で導入される<%: …%>出力に向けた実装ですよ。IHtmlStringってヤツですよ。でも、ASP.NET 3.5にはそんなもの無いので、Futuresの実装はインターフェースは無しバージョン。ってことは、単純に実行したらレンダリングされるわけじゃ無くて、ToString()かToHtmlString()で取得して、それをレンダリングするようにしないとちゃんと出力されないわけですね。

そうとわかれば、以下のように変更。

<% = Html.Serialize("drink",Model).ToHtmlString() %>

これでちゃんと出力されました。以下のようなModelを渡して結果を見てみます。

    public ActionResult Drinks()
    {
      var model = new Drink {Name = "Cola", CheckName = "Pepsi", Size = 30};
      return View(model);
    }

↑これが↓こうなります。

<input name="drink" type="hidden" value="/wEy7AEAAQAAAP////8BAAAAAAAAAAw
CAAAARk12Y0FwcGxpY2F0aW9uMSwgVmVyc2lvbj0xLjAuMC4wLCBDdWx0dXJlPW5ldXRyYWw
sIFB1YmxpY0tleVRva2VuPW51bGwFAQAAABxNdmNBcHBsaWNhdGlvbjEuTW9kZWxzLkRyaW5
rAwAAABU8TmFtZT5rX19CYWNraW5nRmllbGQaPENoZWNrTmFtZT5rX19CYWNraW5nRmllbGQ
VPFNpemU+a19fQmFja2luZ0ZpZWxkAQEACAIAAAAGAwAAAARDb2xhBgQAAAAFUGVwc2keAAA
ACw==" />

バリバリBase64エンコードされてViewStateっぽいです。でも、これを復元させるコードがどうなるかと言うと、↓こうなります。

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

このDeserialize属性クラスが何をしてるか、ってことデスよね。またしてもソースを確認すると、そこには...。

        private sealed class DeserializingModelBinder : IModelBinder {

            private readonly SerializationMode _mode;

            public DeserializingModelBinder(SerializationMode mode) {
                _mode = mode;
            }

            public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
                if (bindingContext == null) {
                    throw new ArgumentNullException("bindingContext");
                }

                ValueProviderResult vpResult;
                bindingContext.ValueProvider.TryGetValue(bindingContext.ModelName, out vpResult);
                if (vpResult == null) {
                    // nothing found
                    return null;
                }

                MvcSerializer serializer = new MvcSerializer();
                string serializedValue = (string)vpResult.ConvertTo(typeof(string));
                return serializer.Deserialize(serializedValue, _mode);
            }

        }

単なるModelBinder...。Page.ViewStateなわけじゃないですね。単にシリアライズするためにViewStateと同じものを利用してるだけです。最初の方にも書いたけど、ViewStateと同じようにシリアライズ出来るようにするためにEncryptedやSignedが指定出来るようになってるって事です。ちなみにソースを見てみると、暗号化ViewStateを生成するためにprivate sealed class TokenPersister : PageStatePersisterっていうクラスを定義してて、その中でPageクラスのインスタンスを生成し処理させてます。なんか強引。でも、AntiForgeryDataSerializerでも同じ事してたりしてちょいビックリ。

そんなことはいいとして、これが完全に独自のModelBinderになってしまってるので、これを使う場合にはDataAnnotationsが効かない。ので、hidden書き換えを抑制したいときにはEncryptedやSigned、EncryptedAndSignedを指定しておくようにしないとね!

なぜASP.NET MVCにViewStateを持ち込むんだ~、と怒り心頭な方!ご心配なく。WebFormsでいうところのViewStateでは無かったです。ホッとした。

Futuresのソースみてて気がついたんだけど、AsyncControllerがFuturesに戻されてる。Expression系のユーティリティクラスがたんまり入ってて何やら面白そうな予感がしますが、それはまた今度ってことで。

2009年2月26日木曜日

ModelBinderでLINQ to SQLのモデルをそのまま使う

ASP.NET MVC Tip #49 - Use the LinqBinaryModelBinder in your Edit Actions

ステファン君それは無理じゃない?更新以前にバインドした時点で例外でるじゃん。

と、思ってたのは今は昔。RCでは何の問題もなくバインド出来るみたい。試してみたら、普通に出来た。ずいぶん前に試したときはデータベースコンテキストがスタティックじゃないと例外でたり(ModelBinderに気をつけねば)、そもそも直接DBO使うのはうんぬんかんぬん。綺麗な設計どうのじゃなくて、純粋に少ないコード量でどこまで出来るか、っていうのを考えたら直接使う事もあったりするのかもね。

これまでも使ってたサンプルはDBを使ってなかったので、改めてDBを用意して、LINQ to SQLのモデルを作成。

db1

これを利用するためのコントローラも定義。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Mvc.Ajax;

using Mvc.RC.Models;

namespace Mvc.RC.Controllers
{
public class OnePeaceController : Controller
{
  OnePeaceDataContext _context = null;

  public OnePeaceController()
  {
    _context = new OnePeaceDataContext();
  }

  public ActionResult Index()
  {
    return List();
  }

  public ActionResult List()
  {
    var people = _context.Persons;

    return View("List", people);
  }

  public ActionResult Details(int id)
  {
    var people = _context.Persons;

    return View(people.First(p => p.Id == id));
  }

  [AcceptVerbs(HttpVerbs.Get)]
  public ActionResult Edit(int id)
  {
    var people = _context.Persons;

    return View(people.First(p => p.Id == id));
  }

  [AcceptVerbs(HttpVerbs.Post), ValidateInput(false)]
  public ActionResult Edit(int id, Person person)
  {
    try
    {
      _context.Persons.Attach(person, true);
      _context.SubmitChanges();

      return RedirectToAction("Index");
    }
    catch (Exception ex)
    {
      return View(person);
    }
  }
}
}

List/Detail/Editの各ViewはRCで追加されたAdd Viewコマンドでサラッと作成。こういう時にとても便利ですね。Attachのところでブレークポイントをセットして、まずはこのまま動かしてみたところ、DB使っててもホントに動く。信じてなかったわけじゃないけど、こうもあっさりと動くとちょっと感動する。

bind

↑Listページ。

bind2

↑Editページ。

bind4

↑普通にバインドされてる様子。

でも、このままステップ実行すると、確かに例外が発生。

bind3

エンティティは、バージョン メンバを宣言するか、または更新チェック ポリシーを 含まない場合にのみ、元の状態なしに変更したものをアタッチできます。

System.Exception {System.InvalidOperationException}

と、言うことで、簡単に更新チェックポリシーをオフにしてしまおうかとも思ったけど、それよりちゃんと競合チェックするようにtimestamp型の列をテーブルに追加して動かしてみます。

db2

↑ChangeStampという名前のtimestamp型の列を追加したモデル。

でも、timestamp型はSystem.Data.Linq.Binary型としてクラスが生成されるので、このままだとちゃんとモデルが復元されません。なによりViewにちゃんと出力してないし。まずはEditのViewにChangeStampをhiddenで展開するコードを追加。

            <p>
              <%= Html.Hidden("ChangeStamp",
                              Convert.ToBase64String(Model.ChangeStamp.ToArray())) %>
              <input type="submit" value="Save" />
          </p>
      </fieldset>

  <% } %>

  <div>
      <%=Html.ActionLink("Back to List", "Index") %>
  </div>

</asp:Content>

submitの手前に入れてます。Base64エンコードしてHiddenフィールドに。このままだとDefaultModelBinderが復元してくれくれないので、Global.asaxのApplication_Start時にASP.NET MVC Futuresに入ってるLinqBinaryModelBinderを登録。

    protected void Application_Start()
  {
    RegisterRoutes(RouteTable.Routes);
    ModelBinders.Binders.Add(typeof(System.Data.Linq.Binary),
                              new LinqBinaryModelBinder());
  }

もう、何もかもステファンさんのいいなりです。

この状態で動かしてみて、Viewが出力するHTMLソースのChangeStamp部分を確認してみたのが↓これ。

<input id="ChangeStamp" name="ChangeStamp" type="hidden" value="AAAAAAAAB9Q=" />

ちゃんと、エンコードされて出力されてます。あたりまえだっちゅーの。

bind5

これを更新するためにsubmitして、ブレークポイントでモデルの中身を確認。ちゃんとLinqBinaryModelBinderでBinary型も復元されてます。そのまま実行を続けても例外は出なくて、テーブルも更新されてました。で、更新された後のViewのCangeStampを確認してみる。

<input id="ChangeStamp" name="ChangeStamp" type="hidden" value="AAAAAAAAB9U=" />

ちゃんと、最初とは違う値が入ってますね~。

と、いうことで、ステファンさんのやったことをそのまま試したみたわけですが、これが出来るって事はLINQ to SQL+スキャッフォールディングで凄く簡単にDBを使ったアプリケーションを作成出来る事になりますね。Repositoryは作るにしても、ViewModelを作らずシンプルなコードで開発することが出来るのでWebForms並の生産性(ViewStateとデータバインディング)を実現できてるんじゃないかと思う次第です。

※ViewStateはModelStateが各inputフィールドの値を保持しつつHTMLにレンダリングしてくれたりするので。

2009年2月23日月曜日

DataAnnotationsだけでの入力検証の盲点

以前の投稿(ASP.NET MVC RCの入力検証)で、如何にASP.NET MVCのDefaultModelBinder(IModelBinder)が汎用的になったかを取り上げましたね。

で、以前の投稿で見事に見逃してたのが「必須入力ではないけど、入力形式の不正メッセージを表示したいな」というところ。分かりにくいですね。例えば日付フィールドがあってモデルのプロパティはDateTimeなら当然日付形式の文字列じゃないとキャスト出来ないから、入力エラー。数値型ならintで当然アルファベットとか勘弁してくれよ、と。

必須フィールドならRequire属性でいいですよね。キャストに失敗した場合、何もセットされず型初期値(default(T))が入ったままだから、エラーメッセージに「ちゃんと入力してね(ハート)」って表示すれば。それでも、初期値が数値で0だと困る!って時にはNullable<Int32>とかでnullにしとけば、初期値のまま処理が進んじゃうって事も防げますから。

だけど、必須じゃない場合に「キャスト出来ませんでした」なんていうシステム固定のメッセージを出すのは、どうなのよ、なんて時があるもん。社内システムとかならそういうモンだから、で済むかもしれないけど、ネットに公開するならそういうメッセージはダサイ。いや、社内システムでもダサイけど、対象ユーザー層を考えれば、それでもまぁいいじゃん、っていうかね。

ちなみに、DataAnnotationsを使って、入力検証を実装した以前の実装だと、キャストエラー表示してくれないもんね。ModelStateDictionaryには(モデルを復元したタイミングで)ちゃんとエラーとして入ってるんだけど、ModelErrorクラスのErrorMessageにはメッセージが入って無くて、Exceptionプロパティに例外情報として入ってるから展開されないんです。

以前のテストプロジェクトにここで登場してもらいましょう。で、1箇所変更点として、PersonViewModelクラスのAgeプロパティについてるRequire属性を削除して、Range属性だけにしてみます。クラス定義は以下の通り。

public class PersonViewModel : BaseViewModel
{
 public int Id { get; set; }

 [Required(ErrorMessage="名前は?")]
 public string FirstName { get; set; }

 [Required(ErrorMessage="名字は?")]
 public string LastName { get; set; }

    [Range(0,150,ErrorMessage="0歳から150歳で")]
    public int? Age { get; set; }

 public List Weapons { get; set; }

 public PersonViewModel()
 {
   Weapons = new List();
 }

 public override string Error
 {
   get
   {
     if (Weapons == null || Weapons.Count == 0)
       return "武器、っていうか必殺技は?";

     return null;
   }
 }
}

太字のところですね。 これで、入力値に整数以外を入れて、ポストしたときのスクリーンショットが↓これです。

modelbind

ViewがRenderされるときに、ModelStateDictionaryがどうなってるかをブレークポイントをセットして確認してみましょう。

modelbind2

クリックすると大きく見れます。 Render時には、ModelState内のModelErrorは存在してるけど、ErrorMessageは""空文字で、ExceptionにInvalidOperationExceptionが入ってるのが分かります。

で、このExceptionをエラーメッセージとして表示するなら、そのまま取り出して、ErrorMessageに入れてしまうようなコードを書いてしまえばいいですね。ただ、エラーメッセージが今回の場合だと「17a は Int32 の有効な値ではありません。」と、出ちゃうんですよね。Int32って...。そんなこと表示されても普通の人は理解できないし。そもそもどのタイミングでメッセージの取り出し処理をすればいいでしょうね、って展開になります。

そこで、登場するのがIModelBinderのオーバーライド出来るメソッド群。これまたオレルールの方のエントリーの一番最後に書いてるIModelBinderのイベント発生順がキーになります。

結論から言うと、OnModelUpdatedのタイミング(モデルの復元完了時)に、ModelStateDictionary内のModelErrorに上記例外が含まれてるかチェックしてしまえばいい、という事になります。 ドンドン意味の分かりにくいエントリーになってきてますね~。

で、コードとしてはシンプルに↓こんな感じで動きます。LINQ部がかなり適当...。

public class ValidateModelBinder : DefaultModelBinder
{
 protected override void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)
 {
   base.OnModelUpdated(controllerContext, bindingContext);

   var modelStates = bindingContext.ModelState;


   // ここでModelStateDirctionaryにInvalidCastExceptionを含んだエラーが
   // ないかチェックして、あれば入力エラーメッセージをここでいれる。
   // ※表示するときに空メッセージ(InvalidCastException)は除外するから、
   //   コレクションには入れたままにしておく。
   // ModelErrorを消さずにそのまま残しておくことで、ViewPageでCSSクラス名
   // が追加されてどのフォームエレメントがエラーなのかは視覚的に
   // 判断できる。
   var key = "__InvalidOperationException__";
   if (!modelStates.Keys.Contains(key))
   {
     var isInvalidCast = (
                           from ms in modelStates
                           from err in ms.Value.Errors
                           where err.Exception is InvalidOperationException
                           select err
                         ).Count() > 0;
     if (isInvalidCast)
       modelStates.AddModelError(key, "入力形式の間違った項目があります。");
   }
 }
} 

こんな感じで、DefaultModelBinderを派生させたクラスを作成し、このModelBinderをDefaultBinderにしちゃいます。もう、これだけでいいっす。 なので、Global.asaxのApprication_Startの所に以下のコードを追加。

protected void Application_Start() { RegisterRoutes(RouteTable.Routes); ModelBinders.Binders.DefaultBinder = new ValidateModelBinder(); }

特定のViewModelでしか使わないよ!っていうならViewModelクラスの宣言時に

[ModelBinder(typeof(ValidateModelBinder))] public class モデルクラス {…}

って、書きましょう。

この状態で、もう一度動かしてみます。

modelbind3

今度はちゃんと、ValidationSummary()で表示されるようになりました。 今回のコードは無理矢理エラー項目のキーに"__InvalidOperationException__"と入れて、複数のエラーメッセージが出ないようにしてますが、もちろん項目毎に表示してもいいですよね。

若干気になるのは、なんでInvalidOperationExceptionを発生させるようにしてるのかな~、というところ。なんとなくだけど、InvalidCastExceptionのほうがシックリ来ないですかね~。そういうもんなんですか? ちなみにどこでInvalidOperationExceptionを発生させてるかというと、ASP.NET MVCに含まれるValueProviderResultクラスのConvertSimpleTypeメソッド。この中でTypeDescriptor.GetConverter()で取得したコンバーターのConvertToを呼び出すところらへん。

ココまで書いて思ったんだけど、ASP.NET MVCに関するエントリーだけでも全部こっちに持ってくればいいのかな。今はそれ以外の事を書く事もほとんどないし。そうしちゃおっかな。

ASP.NET MVCのエントリは全部移行しました。手作業で...。

2008年8月30日土曜日

ModelBinderが素敵過ぎる

ASP.NET MVC Preview5の続き。

SingingEels : Model Binders in ASP.NET MVC

↑ここでサンプルダウンロードできるけど、DefaultModelBinderを派生させて独自Binderを定義しておくことで、Actionのパラメータをクラスの実体に置き換えることができるというもの。

とにかくダウンロードすればすぐわかるんだけど、少し解説。

まずはBinderのクラスを作成。これはDefaultModelBinderクラスを派生させましょう。 で、ConvertTypeメソッドをオーバーライドして、valueに入ってる値をdestinationTypeに変換してあげる。 例えば、ここのサンプルだとCustomersテーブルのID値をBase64にエンコードしたものをtargetCustomerという名前の QueryStringにしてActionLinkでリンクを生成して、そのリンクをクリックしたときのActionでCustomerクラスの実体が渡されるっていうものになってる。

this.Writer.Write(this.Html.ActionLink<HomeController>(c => c.Details(customer), customer.FullName));

↑これをIndex.aspx内で書いてて、リンク(aタグ)を出力してるんだけど、これの出力結果がたとえばID=1なら1をBase64エンコード('='は'_'に置換)して↓こうなる。

<a href="/Home/Details?targetCustomer=AQAAAA__" >Ivan Buckley</a>

これのアクション定義は↓。

public ActionResult Details(Customer targetCustomer){...}

んだけど、これはつまりデフォルトのルーティングをそのまま使うように変更してidという名前で渡すようにしてみるとですね、↓こうなるわけです。

アクション:public ActionResult Details(Customer id){...} リンク:<a href="/Home/Details/AQAAAA__" >Ivan Buckley</a>

これは、分かりやすくていいですよね。 型付きActionLinkで生成してるからIndex.aspxのほうは変更の必要なし。 わざわざアクションの中でIDからデータを取得するコードを書かなくても、MVCのハンドラがモデルに変換する処理をはさんでくれるというすぐれもの。 ただ、勝手に変換はしてくれないので、Global.asaxでModelBindersにどの型の変換をどのクラスで実行するかを登録しておく。

ModelBinders.Binders.Add(typeof(Customer), new MyCustomerBinder());

もう、楽しくてしょうがないね!

2012年2月25日土曜日

ControllerとApiController

ASP.NET MVC4 beta面白いですねー。

特にRESTfulなサービス実装を容易に実現するために導入されたApiControllerは強烈。

WCFのプロダクトとして開発が進められていたWCF Web APIが、名前を変えてASP.NET Web APIとして生まれ変わったのがその実態です。

そもそもWeb APIの出所がWCFで、その系譜もなかなか歴史のあるものだったわけですが、RESTfulならMVC、SOAPならWCFという住み分けを明確にするという意味も込めてのASP.NET Web APIなんじゃないかと勘ぐってるところです。

ホントのところは本国の開発チームしか知らないんだろーし、そんな理由はDeveloperには関係なかったりもするんだけど、気になって仕方ない。その辺はこっちに書いてみました。まるで根拠のない話ですから!フィクションですからね!

で、ですね、ApiControllerですけど。これまでのMVCだとIController実装のControllerBase、それを派生させたControllerを利用してましたね。そう、この名前が重要で、ApiControllerもControllerという名前。でも、その実態はIControlerではなくIHttpController実装。

はい~?何が違うの~?と、なりましょうね。そりゃーそうです。どっちも名前はControllerってなってるし、使い方もだいたい一緒で、見た目の違いはRouteCollectionへの登録時にMapRouteなのかMapHttpRouteなのか、くらいですからね。

だけど、だからといって、ApiControllerがIControllerの仲間だと思うのは大間違い!MapHttpRouteと、あえて違う拡張メソッドでの登録にしてるのには訳がある。

今のところソースも無いし、詳細を確認するにはなかなか厳しいところです。が、そんな時はJustDecompile!ベータも取れて若干の不具合はあるものの、大変便利なデコンパイラ。困ったらコレ。リバースエンジニアリングで黒判定?でも、知りたいし~。

まずはMVCの仕組み。

An Introduction to ASP.NET MVC Extensibility

↑こちらのPDFがかなり正確な感じです。

mvc_pipeline

PDFをダウンロードしてみてみてください。

スタートはRouteの登録からはじまり、Controller→ModelBind→Action Invoke→ActionResult→Result Invokeとひと通りの流れと、Resolver使ってる部分が書かれてます。素晴らしいですね。

コレとMVC3のソース(公開されてるから見ましょう!)を、見つつApiControllerを使った場合の比較をしてみます。分かってる範囲で。

Route

MVC

System.Web.Mvc.RouteCollectionExtensions.MapRoute(System.Web.Mvc.dll)
    RouteCollection.Add(
        name,
        System.Web.Routing.Route(MvcRouteHandler : IRouteHandler)
    )

どうってこと無いですね。MvcRouteHandlerをRouteクラスに渡してます。MvcRouteHandlerにはMvcHandlerを渡します。

Web API

System.Web.Http.RouteCollectionExtensions.MapHttpRoute(System.Web.Http.WebHost.dll)
    RouteCollection.Add(
        name,
        System.Web.Http.WebHost.Routing.HttpWebRoute(HttpControllerRouteHandler.Instance : IRouteHandler)
    )

こっちはいきなり違います。HttpWebRouteクラスを登録です。そこへはHttpControllerHandlerを渡すんです。

パンチ効いてますねー。

IRouteHandler.GetHttpHandlerがRouteに紐づいてるIHttpHandlerを取得するんですけど、ココからすでに別物です。つまり、Web APIで提供されるApiControllerっていうのはMVCで提供されているControllerとはまるで別物ということです!

それが、何を意味するかというと、ActionSelector、ModelBinder、ActionFilterなどなど、MVCで提供されていたものがすべてWeb APIでは別のアセンブリとして提供されてるということです!!

というのも、そもそもWCF Web APIで実装を進めていたものだしね。今のところ、双方で依存関係もないです。どこかで一本化するのかどうかは微妙ですね。

System.Web.HttpとSystem.Web.Mvcはそれぞれの道を歩みそうな気もする。もちろんSystem.Web.RoutingやSystem.ComponentModel.DataAnnotationsなんかは共通なんだけど。それらを利用する部分はまるで別物。

それを踏まえて、いま、出回ってる情報を見ると、なんでDataAnnotationsを使ったバインドやAuthorizationFilterとかのFilterが使えるんだよ、とことさら強調してるのかがわかると思います。新たに実装したんだから、自慢したい!っていうね。違うか...。

リクエストとレスポンス

MVC

Requestは基本的にASP.NETの仕組みと同じで最終的にActionResultを返す。

Web API

RequestはHttpMessageHandlerが仲介してHttpRequestMessageになって、HttpResponseMessageを返す。

普通の事書いてる感じがするけど、大違いなんです。Web APIではこのMessageっていうのが重要でそれに対して各種Pipelineが介入していく。この設計、まさにWCFって感じですね。よくできてるなー、と感心せざるを得ない。

ここで分離し、かつWebサーバーの存在をHttpServerというクラスで抽象化することで、APIの提供をWeb サーバーに限定せず、Self Hostへとつなげることが、あたかも自明な流れとして受け入れられる。ん?そんなことない?

HTTP Message Handlers: The Official Microsoft ASP.NET Site

IHttpHandler

MVC

ControllerFactoryから対象Controllerインスタンスを取得してExecute。Controller.ExecuteCoreに入った時の流れ。

PossiblyLoadTempData
IActionInvoker.InvokeAction(ControllerActionInvoker)
    ControllerDescriptor <= ReflectedControllerDescriptor
    ActionDesctiptor <= ControllerDescriptor.FindAction
    FilterInfo <= GetFilters
    InvokeAuthorizationFilters - IList<IAuthorizationFilter>
    AuthorizationContext
    ? InvokeAuthorizationFilters
    : GetParameterValues
        IModelBinder.BindModel : ParameterDescriptor
            DefaultModelBinder.SetProperty
                ModelValidator <= ModelValidatorProviders.Providers.GetValidators
                    DataAnnotationsModelValidatorProvider
                    DataErrorInfoModelValidatorProvider
                    ClientDataTypeModelValidatorProvider
      InvokeActionMethodWithFilters : IList<IActionFilter>
        InvokeActionMethod
            IActionDescriptor.Execute
      InvokeActionResultWithFilters : IList<IResultFilter>
        InvokeActionResult
            ActionResult.ExecuteResult
PossiblySaveTempData

わかりにく!メモってことで。そもそも素直な実装なので、コード見たほうが早い。

Web API

RequestからHttpRequestMessageを作成、ConfigurationとDispatcherを指定してHttpServeを作成。

HttpServer.SubmitRequestAsyncにHttpRequestMessageを渡して処理開始!HttpMessageHandler、HttpControllerDispatcherが大活躍になるのがここから。

HttpControllerDispatcher
SendAsync
SendAsyncInternal
    Initialize
       
        IHttpControllerActivator <= ServiceResolver.GetService || Activator.CreateInstance
        IHttpActionSelector <= ServiceResolver.GetService || Activator.CreateInstance
        IHttpActionInvoker <= ServiceResolver.GetService || Activator.CreateInstance
       
    IHttpControllerFactory <= ServiceResolver.GetControllerFactory
    IHttpControllerFactory.CreateController
        IHttpController <= DefaultHttpContollerFactory.CreateInstance
            ControllerDescriptor.HttpControllerActivator.Create
                TypeActivator.Create
                    Expression.New
               
    IHttpController.ExecuteAsync
   
        HttpControllerDescriptor <= HttpControllerContext.Descriptor
        HttpActionDescriptor <= HttpControllerDescriptor.HttpActionSelector.SelectAction
            IHttpActionSelector : ApiControllerActionSelector
        HttpActionContext
        FilterInfo <= HttpActionDescriptor.GetFilterPipeline
        IEnumerable<IActionFilter> <= FilterInfo.ActionFilters
        IEnumerable<IAuthorizationFilter> <= FilterInfo.AuthorizationFilters
        IEnumerable<IExceptionFilter> <= FilterInfo.ExceptionFilters

        InvokeActionWithExceptionFilters(taks)
            InvokeActionWithAuthorizationFilters
                IActionValueBinder <= ServiceResolver.GetActionValueBinder : DefaultActionValueBinder
                       
                IActionValueBinder.BindValueAsync
                    HttpActionBinding                           
                        CreateParameterBindings
                            BindToBody
                                ValidationModelBinder.BindModel
                                ModelValidationNode.Validate
                    DefaultActionValueBinder.BindParameterValue
                    IModelBinder.BindModel

                    MutableObjectModelBinder.SetProperty?
                    HttpActionContextExtensions.GetValidators?
                        DataAnnotationsModelValidatorProvider
                        ClientDataTypeModelValidatorProvider

                InvokeActionWithActionFilters
                    IHttpActionInvoker.HttpActionInvoker.InvokeActionAcyns : ApiControllerActionInvoker
                        ActionDescriptor.Execute(ControllerContext,ActionArguments)
            HttpActionExecuteContext

動かしながらの確認じゃないのでイイカゲン。だし、相変わらずメモで読みにくし。どんまい。Resolverがいろんな所で使われてるのと、ほとんどがTaskになってる。

今のところよくわかってないのがMutableObjectModelBinderがValidationするModelBinderなんだけど、こいつが発動するのはいつなのかというところ。この中のSetPropertyでModelValidatorがワサワサ動いてるっぽいから、コレを利用したバインドにならないと検証が動かないじゃないっすか。動いてることは間違いないんだけど(DataAnnotatinosでの検証がかかってるのはサンプルで確認できてるし)なー。今後の調査課題。

ここでMVCと明確に違うのがDataErrorInfoModelValidatorProviderが存在してないところ。なくてもいいと思うけど、IDataErrorInfo使ってるのを動かすのは自分でしこまないとダメってことですね。こういう所ではちょっと差がある。

Validating your models in ASP.NET Web API - Pablo M. Cibraro (aka Cibrax) ASP.NET MVC 4 public beta including ASP.NET Web API

↑この書き方のサンプルをオンラインでよく見かけますよね。MVCならAction内でModelState.IsValid見てたと思うけど、Web APIだとFilterにしちゃうのがいいの?ときになるところだと思いますが、これはAPIが返す結果が何なのかを考えれば妥当というかコレしかないね、と思えるところです。

どういうことかというとHTMLを返して、その中にエラーメッセージを含んでたりModelStateによる入力値を復元させることを前提にしてるアプリケーションとしてのMVC(どこのViewにModelを渡すのかをControllerが指定)。それに対してエラーですよ、ということを返せばいいだけのAPI(ControllerがModelを返すけど、APIから返るデータをViewというならViewはModelをFormatしたXML/JSON固定で不変)。何がどのようにエラーだったのかを判定して表示を制御するのはAPIを利用したアプリケーションの責任で表示としてのViewを制御してるのはAPIじゃない。なので、一律Filterでエラーの時の処理を決めてレスポンスすれば良い。MVCでも同じようにFilterでModelStateをみてエラーをレスポンスすることは可能ですが、その時のViewをFilterが判断することになって、う~ん、まいった、ですね。

んじゃ、ApiControllerのActionの中でModelStateにエラーを入れることができないの?っていうとそんなことはなくて、入れればいいです。で、OnActionExecutingじゃなくてOnActionExecutedでResponse変えるとか、HttpMessageHandlerで書き換えるとか、事後にエラーとしてしまう方法はイロイロです。

DefaultServiceResolver

ちょっと脱線。最初から登録されてるインスタンスたちはコレらでした。

  • IBuildManager
        DefaultBuildManager
  • IHttpControllerFactory
        DefaultHttpControllerFactory
  • IHttpControllerActivator
        DefaultHttpControllerActivator
  • IHttpActionSelector
        ApiControllerActionSelector
  • IHttpActionInvoker
        ApiControllerActionInvoker
  • ModelMetadataProvider
        EmptyModelMetadataProvider
  • IFormatterSelector
        FormatterSelector
  • IActionValueBinder
        DefaultActionValueBinder
  • IRequestContentReadPolicy
        DefaultRequestContentReadPolicy
  • IFilterProvider
        ConfigurationFilterProvider
        ActionDescriptorFilterProvider
        EnumerableEvaluatorFilterProvider
        QueryCompositionFilterProvider
  • ModelBinderProvider
        TypeMatchModelBinderProvider
        BinaryDataModelBinderProvider
        KeyValuePairModelBinderProvider
        ComplexModelDtoModelBinderProvider
        ArrayModelBinderProvider
        DictionaryModelBinderProvider
        CollectionModelBinderProvider
        TypeConverterModelBinderProvider
        MutableObjectModelBinderProvider
        CompositeModelBinderProvider
  • ModelValidatorProvider
        DataAnnotationsModelValidatorProvider
        ClientDataTypeModelValidatorProvider
  • ValueProviderFactory
        RouteDataValueProviderFactory
        QueryStringValueProviderFactory
  • ModelMetadataProvider
        CachedDataAnnotationsModelMetadataProvider
  • ILogger
        DiagnosticLogger

いっぱいあるねー。

resolvers

System.Web.Http.Services.DefaultServiceResolver。System.Web.Http.GlobalConfiguration.Configuration.ServiceResolverらへんです。

MVCそのもの(System.Web.Mvc)も楽しいんだけど、今後はASP.NET Web API(System.Web.Http)に対しても、目を光らせておこうと思う次第です。

dotnetConf2015 Japan

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