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年2月4日水曜日

ASP.NET MVCでLambda使ったRepeaterヘルパー

Bug squash: Repeater with separator for ASP.NET MVC

やってることは、Actionデリゲートを受け取るRepeaterヘルパーを書いて、その中でイテレータでデータを取り出して、Actionデリゲート(アイテム描画と区切り描画)を実行するものだけど、初回実行と2回目以降の描画を判定するイテレータの書き方がオシャレ。

1.itemsをSelectでアイテム描画FuncのIEnumerableに変換。 2.Intersperseで最初のイテレータなら、アイテム描画のFuncをyield return、2回目以降は区切り描画Funcをyield return後アイテム描画Funcをyield returnで新たなIEnumerableに変換。 3.最後に、2のFuncのIEnumerableをSelectして実行させる。

元はPhilさんの↓このエントリー。

Code Based Repeater for ASP.NET MVC

ちょっと、古いじゃないですか!と、思うところですが、気になったんですよね。 そういえば、このLambda(Actionデリゲート)には何が渡されてるんだ?と。 WebFormViewEngineなんだから、aspxをc#(VB.NETかもしれないけど)に変換してるのは分かるけど、そういえば最近変換内容を確認してないな~、なんて。

  <p>
 <% Html.Repeat(new []{"ルフィー","ゾロ","ウソップ"}, val => {%>
   <% = Html.Encode(val)%>
 <% }, ()=>{ %>
   <br />
 <% }); %>
 </p>

↑こんな感じで書いたときに、どういうcsが出力されてるのか。 WebDev.WebServerで動かしてる時って、どこにいるんだべか。 悩む必要なんか無いことに気がついた。VSの出力ウィンドウに出てるじゃん。

img.aspx

小さすぎて見えないね。

'WebDev.WebServer.EXE' (マネージ): 'C:\Windows\Microsoft.NET\Framework\v2.0.50727\Temporary ASP.NET Files\root\c83bad6c\726bdcee\App_Web_index.aspx.a8d08dba.52iuaodz.dll' が読み込まれました。シンボルが読み込まれました。

↑こう。 ってことで、上記フォルダを見てみる。それっぽい拡張子csのファイルを開く。 コメントが多いから消したのが↓これ。

            @__w.Write("\r\n  <p>\r\n  ");
    Html.Repeat(new []{"ルフィー","ゾロ","ウソップ"}, val => {
           @__w.Write( Html.Encode(val));
           @__w.Write("\r\n  ");
    }, ()=>{
           @__w.Write("\r\n    <br />\r\n  ");
    });
           @__w.Write("\r\n  </p>\r\n\r\n");

※@__writeはHtmlTextWriter。

そっか。普通に<%= ~ %>はHtmlTextWriter.Writeになるだけで、<% ~ %>はそのままのコードだもんね。 スッキリ。

2026年06月17日の記事一覧

(全 13 件) Foundry Toolkit for Visual Studio Code 自分のPCで「とりあえず動く」ではなく「一番賢く使える」ローカルLLMを教えてくれる「whichllm」、実測ベンチでランク付け(生成AIクローズアップ) | ...