配列を保持するときに、コレまで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> <% } %>
こんな感じの単純な物なんだけど。 例えばコレで、テキストエリアに"<script />"なんて入れて送信すると...。
見慣れたエラーが出るね。 だけど、アクションに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するので、その違いがこんな所に出てきてます。まぁ、デフォルト安全動作っていうのはいいことだね。ベータ以前から移行の場合は修正箇所は増えるけど。
肝心のファイルアップロードといえば、以下の通り。普通に入ってるね(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は書き換えてないです。
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"を入れる。 これだけなんだけど、なかなか気がつかないよね。
今度はuploadFileがnullじゃな~い。 HttpPostedFileBase bug when binding - ASP.NET Forums