2008年10月21日火曜日

ベータになって

ソースも公開されましたね~。

ASP.NET - Release: ASP.NET MVC Beta Source Code Release

これで少し救われた...。

今回の変更でかなりリファクタリングが進んで、いい感じですね。 だいたいの変更点はコンパイルエラーで判断できるので、まぁいいでしょう。 ちょっとビックリするのはMvcFuturesが一緒に配布されなくなったところ。 AntiForgeryToken/ValidateAntiForgeryTokenとFileResultを使ってたのでどうしようと悩む。 HtmlHelperが凄く整理(使う分には関係ないかもしんないけど)されてて、SubmitButtonが無くなったね。これまた使いたければ直に<input type="submit" />を書く。

と、思いきやMvcFuturesのbetaアセンブリがちゃんとダウンロード出来るようになってましたね。 ASP.NET - Release: ASP.NET MVC Beta Futures ※ダウンロード数が異様に少ない気がするのは気のせいか。あんまり誰も使わない?

がMvcFuturesにもFileResultが無く...(Html.SubmitButtonはこっちに入ってました)。 代わりにBinaryStreamResultっていうのがあって、これを使うことで対応出来る事が分かりました。 Streamをコンストラクタに渡さなきゃいけない(ファイル実体じゃなくてファイルのパスでもテスト出来ると思うけど、それじゃイカン!ヤカン!って事なんでしょうか)のが、変わったところ。

var fileStream = new System.IO.FileStream(filePath, System.IO.FileMode.Open);
var result = new BinaryStreamResult(fileStream) {
 ContentType = mimeType,       // MIMEタイプ
 FileDownloadName = fileName // Content-Dispositionのファイル名
}; 

Form の値を取得する部分が見た目それほど変化が無い(namespaceの変更とオーバーロードの変更)にも関わらず、内部では大幅な変更が入っている模様。それぞれのInputExtentions(CheckBox,Hidden,Password,RadioButton,TextBox)では privateのInputHelperを呼び出してinputタグを生成してるんだけど、その中でGetModelAttemptedValueを呼び出す。必ず呼び出す。これが曲者(ってわけじゃないけど)。 ModelState.AddModelErrorなんかが、今回の変更で入力値をあえて渡さなくていいようになってますよね。結局表示の段階でHtmlHelperのInputExtentionsを呼び出すから、それで前回入力値を取得して表示出来るっていう寸法で便利っちゃ便利なんです。が、しかしですよ、ということはInputExtentionsを使う限りは、 ViewData.ModelStateに入っている値が最優先で表示されるってことデスよ。いいじゃん、それで、と思う事なかれ。ModelState の値はPost時に確かに取得するけど、だからといってそれを表示したいわけじゃないってときもあるじゃないですか!ないですか!そうですか! UpdateModelを使って、Formの値を取得した場合、ModelStateにも同じ値が入るんで↓こうなる感じ。

public class Val
{
 public string MyValue {get;set;}
}

var  val = new Val();
UpdateMode(val); 

↑このとき、val.MyValueに"123"って入ってたとします。

Viewで

<%= Html.TextBox("MyValue", "456") %>

って書いててもデスよ、展開されるinputタグのvalueには"123"デスよ! 回避するにはどうするか。いまいち答えが見つけられず、とりあえずinputタグを直接書く(それだとエラーの時に自動でCssClassが追加されないし値も表示されない)...、か、ModelStateを消す? 泣けるっす...。 これだけじゃなく、配列(List<T>でも)の取得が出来るようになってたりするのに、いまいち書き方が分からずなところで、ソース公開だったので助かりました。以下にサンプルを。 まずはモデルクラスを定義。

using System; using System.Collections.Generic; using System.Linq; using System.Web; namespace MvcApplication.B1.Models { public class Person { public string Name { get; set; } public int? Age { get; set; } } public class MyViewData { public List People { get; set; } public MyViewData() { People = new List(); } } }

で、コントローラにアクション実装。

    public ActionResult People()
   {
     var viewData = new MyViewData();
     viewData.People = new List() {
       new Person() { Name = "たけはら", Age = 33 },
       new Person() { Name = "まうり", Age = 26 },
       new Person() { Name = "しんたろ", Age = 21 }
     };
     return View(viewData);
   }

   [ActionName("People"), AcceptVerbs(HttpVerbs.Post)]
   public ActionResult PeoplePost()
   {
     var formData = new MyViewData();

     UpdateModel(formData.People, "People.Person");

     formData.People.RemoveAt(1);

     return View(formData);
   } 

※これをHomeControllerに追加。 最後にViewを。Viewはコントローラと同じフォルダなので、今回はHomeに。

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" AutoEventWireup="true" CodeBehind="People.aspx.cs" Inherits="MvcApplication.B1.Views.Home.People" %>
<asp:Content ID="Content1" ContentPlaceHolderID="MainContent" runat="server">

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

<table>
<% var row = 0; %>
<% foreach(var person in ViewData.Model.People) { %>
<tr>
 <th>なまえ</th>
 <td>
 <% = Html.TextBox("People.Person[" + row + "].Name", person.Name)%>
 <input type="hidden" name="People.Person.index" value="<%= row %>" />
 </td>
</tr>
<tr>
 <th>年齢</th>
 <td><% = Html.TextBox("People.Person[" + row + "].Age", person.Age)%></td>
</tr>
<%
 row++;
 }
%>
</table>

<input type="submit" value="送信" />

<% } %>
</asp:Content>

で、一発目動かして、/Home/Peopleにアクセス。 img.aspx

わかりやすいよね~。3人分の名前と年齢が出てきました。 で、今度はそのまま送信を押す。と、Postなんで違うアクション(PeoplePost)が実行されます。 コードを見て分かるとおり、UpdateModelで復元したあと、2番目のデータを削除して再表示。 このとき配列を復元するためにFormに"People.Person.index"っていうhiddenの値を埋め込むのがコツみたいですよ。なんでHtml.Hiddenを使わないのかっていうのは、以下の続きを。 で、この場合、「まうり」が消えた状態の2件が表示されるはずよね。 動かしてみましょうか?

img.aspx2

なんとまぁ「しんたろ」が消えてるじゃないっすか。 でも、ちょっと待てと。データは確かに「まうり」が消えてる。

img.aspx3

ね。これがGetModelAttemptedValueの値が優先されるときに起きる現象。 InputExtensionsのInputHelper関数の以下の行。

tagBuilder.MergeAttribute("value", attemptedValue ?? ((useViewData) ? htmlHelper.EvalString(name) : Convert.ToString(value, CultureInfo.CurrentUICulture)))

attemptedValueにはModelStateの値が入ってる(UpdateModelした時とか)から、Viewで引数に渡した値を全然無視。なもんで、Html.Hiddenでindexを入れてしまうと、カンマ区切りのpost値が勝手に埋め込まれてNG。 こういう使い方を実際にしちゃってるもんだから、設計を変えるのにどうしようかと悩み中デス。 今回の大きな変更部はやっぱりModelBinder周り。

ちゃんとアクションのパラメータに対するBindも実装(ガスブロ参照)されてるみたいだし。UpdateModel/TryUpdateModelなんかも併せて大幅改修な感じ?keyを渡すのがホワイトリスト(取得対象)/ブラックリスト(拒否対象)だったり、指定すら必要無くなってたり(これが嬉しいんだけど、値の保持し過ぎ問題を含む)、インターフェースで取得項目を指定する感じだったり。 ViewDataのクラスを作る場合、表示のタメだけに使いたいフィールドの定義なんかもするけど、それを UpdateModelで取得する必要が無いって時に、いろんな方法で対応出来るようになったのはいいことですね。今までのコードも今まで通り動く(ホワイトリスト方式)し。 今回のリリースで手痛い思いをするような作りをしてる人は少ないのかなと思いますが、個人的には設計を見直す必要があるので、ダサイ作り方をしてしまった(自分のプロジェクトがって意味です)と割り切って、考え直します...。