2008年10月30日木曜日

関連情報も続々登場

ASP.NET MVCベータの気になる関連情報。


その1.ASP.NET - Release: ASP.NET MVC Beta Source Code Release
まだそれほどダウンロードされてない気がするけど、これを見とかないことにはドキュメントが無い状態では効率よく開発するのは難しいよね。

その2.ASP.NET - Release: ASP.NET Dynamic Data 4.0 Preview 1
ASP.NET MVCとDynamic Dataの融合。
サンプルのブログが参考になります。

その3.SubSonic MVC Addin Updated for Beta 1 : Rob Conery
これまたスゴイんだけど、ScaffoldできるSubSonic。
リポジトリパターンのデータアクセスと、コントローラ、ビューまでをLINQ to SQLのモデルから生成。生成されるビューを_Templatesフォルダに作っておくことで、カスタマイズ可能。
Dynamic Dataでのお手軽作成に負けないほどの凄さ。

その4.Cloudy in Seattle : ASP.Net MVC Projects running on Windows Azure
これはまだ試せてないけど、Windows Azure上にASP.NET MVCをデプロイする方法。
衝撃的...。出来るかどうか、調べようと思ってた矢先のエントリでビックリ。
データアクセスがSQL Servicesだろうから、そっちの使い方も調べる必要があるけど、まずは動くっていうのが分かっただけでワクワクする。

2008年10月28日火曜日

UpdateModelのタイプ指定

ASP.NET MVCベータになってUpdateModelで「強く型付けされたホワイトリストフィルター」がありますね。これが悩ましくてですね。結局スマートな使い方はどうした物なのか未だに答えを見つけられないでいます。 そもそもViewDataをViewに渡す時ってどういう内容で渡しますか?

入力される項目と、表示しかしない項目があると思いますが、それってクラスを分けるもの? 単純に考えたら入力も表示も同じクラス内に混在させていいんじゃないの、って感じに実装すると思うんだけど、そうするとUpdateModelでちょっと面倒な事が起きたり起きなかったり。

例えば、表示専用のプロパティとしてLINQ to SQLのモデルクラスを持ってたりした場合なんて、切なくなりますよね。だってね、DefaultModelBinderでインスタンス作られると最悪Webサーバーがダウン。 その為にstring配列で復元対象のプロパティを指定したり、除外したりすることになります。ただ、単純に文字列をコードの中に埋め込むっていうのもシャキっとしない気がするんですよね(リフレクションでプロパティリストを自動取得するっていうやり方がシンプルで簡単だけど)。

だから、スコットさんのエントリに書いてあるように型指定して値を復元しようかな、と思い至る。 で、今度は保持してるプロパティにList<T>とかがあるとどうなるんだ、って話です。 前回までのエントリで配列なりListの取得が出来るのは分かってるんだけど、それを「強い型付け」で取得したいときどうしましょうかと。

  public interface IPerson
  {
    string Name { get; set; }
    int? Age { get; set; }
  }

  public class Person : IPerson
  {
    public string Name { get; set; }
    public int? Age { get; set; }
    public string Nickname { get; set; }
  }

  public interface IMyViewData
  {
    string TeamName { get; set; }
    int Level { get; set; }
  }

  public class MyViewData : IMyViewData
  {
    public string TeamName { get; set; }
    public int Level { get; set; }
    public List People { get; set; }

    public MyViewData()
    {
      People = new List();
    }
  } 

試しに↑こんなクラスを書いて。

var formData = new MyViewData();
UpdateModel(formData); 

と、実行するとですね、Peopleプロパティは復元されませんよね。 インターフェースにPeopleが無いから。 ※インターフェースに含めてプロパティの型をList<IPerson>にしても上手く行かない。 Peopleを強い型付けで復元したいときって、Prefixを指定してループで回して取得とか?

      var index = Request.Form["People.index"].Split(',').Length;
      for (var i = 0; i < index; i++)
      {
        var person = new Person();
        UpdateModel(person, string.Format("People[{0}]", i));
        formData.People.Add(person);
      } 

こうやって書けば、そりゃもちろん取得できるけど...。ダサい気がする。 IPersonじゃなくてPersonBaseクラスを定義して、PersonをインターフェースじゃなくてPersonBaseから派生させて保持する方法を考えてみた。

  public class PersonBase : IPerson
  {
    public string Name { get; set; }
    public int? Age { get; set; }
  }

  public class Person : PersonBase
  {
    public string Nickname { get; set; }
  }

...

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

だけど、これってInvalidCastException。 Listの場合ってどうするのさ! コード量は少なくしたいっす。

次にListで取得して、それをListにキャストすればいいのかと。

var people = new List();
UpdateModel>(people, "People");
formData.People.AddRange(people.Cast()); 

これまたInvalidCastException。ダウンキャスト出来ないんだね~。そもそもCast()は何を使ってキャストしてるんですか。Reflectorで見た感じだとキャスト演算子(T)してるだけっぽいけど。 PersonBaseクラスにexplicitでPersonを返すメソッド書いたけど、派生クラスはダメよ!って怒られてダメだし...。 なので、PersonクラスのコンストラクタにPersonBaseを渡すほうほうに変えてみた。


public class Person : IPerson { public string Name { get; set; } public int? Age { get; set; } public string Nickname { get; set; } public Person() { } public Person(PersonBase person) { Name = person.Name; Age = person.Age; } }

Personクラスは↑こう変更。 で、UpdateModelの後のコードを↓こう変更。 formData.People.AddRange(people.Select(p=>new Person(p)); これは上手く動きますね。ViewDataのクラスのコンストラクタを追加する必要はあるけど、Controllerでは短いコードですむし。

Personクラスのコンストラクタはいじらずに、ConvertAllでConverterを渡しても結果同じだけど、コードはちょっと見づらい。Controllerに書くのもどうかと思うよね。

formData.People.AddRange(people.ConvertAll(
  new Converter(s => new Person() {
    Name = s.Name,
    Age = s.Age
  })
)); 

クラスの中に、違うクラスのコレクションや配列、リストをプロパティに持ってる場合の、UpdateModelのベストな使い方ってどう書くんですか...。 CustomMobelBinderを沢山作るのはちょっとヤダ(コード増えすぎ)。 教えて偉い人!

2008年10月22日水曜日

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

昨日のASP.NET MVC ベータの悩みのシンプルな解決方法はこれかな~。

UpdateModel(TryUpdateModel)で取得したForm値は、ModelStateDictionaryの値が優先されて、2回目以降のViewでInputExtentionsの引数の値を無視する、のこと。 昨日と同じようにまずはモデルクラス。

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 string TeamName { get; set; }
   public int Level { get; set; }
   public List People { get; set; }

   public MyViewData()
   {
     People = new List();
   }
 }
} 

変更する必要も無かったけど、配列の時の使い方とそうじゃない場合を分かりやすくMyViewDataクラスのプロパティを追加。 今回もコントローラはHomeControllerってことで、以下のコードを追加。

    public ActionResult People()
   {
     var viewData = new MyViewData() {
       TeamName = "チーム座布団",
       Level = 11,
       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();

     if(TryUpdateModel(formData))
       formData.People.RemoveAt(1);

      ViewData.ModelState.Remove("People.index");
     if (ViewData.ModelState.IsValid)
       foreach (var ms in ViewData.ModelState.Where(ms => ms.Key.StartsWith("People")))
         ms.Value.AttemptedValue = null;

     return View(formData);
   } 

最後に、Views/Homeの下にPeople.aspxを追加。

<%@ 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()) { %>

<label>チーム名</label>
<% = Html.TextBox("TeamName") %>
<label>レベル</label>
<% = Html.TextBox("Level") %>
<table>
<%
 foreach(var person in ViewData.Model.People) {
   var row = ViewData.Model.People.IndexOf(person);
%>
<tr>
 <th>なまえ</th>
 <td>
 <% = Html.Hidden("People.index", row)%>
 <% = Html.TextBox("People[" + row + "].Name", person.Name)%>
 </td>
</tr>
<tr>
 <th>年齢</th>
 <td><% = Html.TextBox("People[" + row + "].Age", person.Age)%></td>
</tr>
<% } %>
</table>

<input type="submit" value="送信" />
<% } %>
</asp:Content>

Person クラスの配列になるPeopleプロパティの表示と取得をメインにしてるんで、hiddenでPeople.indexという名前で配列のインデックスを入れるのは昨日と同じだけど、Html.Hiddenヘルパーを使うのが違うところ。これはPostしたときに、必ずModelStateから削除するようにすることで値がCSVになっちゃわないように対応。これに関しては、UpdateModelでエラーが起きても関係なく削除しちゃって問題ないもんね。 で、Post時のコントローラでUpdateModelのエラーが無ければ、Peopleで始まるキーを持つModelStateの AtteptedValueにnullを入れることで、2回目以降のViewでも値がそのまま表示されるようにしてます。エラーの時には入力値がそのまま表示されるようにして欲しいから、エラー時にはnullを入れない。 これで、配列(List<T>だけど)のプロパティを持つクラスでも、UpdateModelで取得したり表示したり簡単にできるようになりました。

初回表示は↓。

img.aspx

送信ボタンを押した2回目の表示↓。 ちゃんと"まうり"が消えます。

img.aspx2

1回目の送信で入力エラーがあった場合の表示↓。

img.aspx3

やったね!

あと、Evalが拡張されて↓こう書くことで書式化が簡単になりました。

<% = ViewData.Eval("Age","{0:D4}") %>

※"0034"みたいに4桁0埋めの書き方。

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で取得する必要が無いって時に、いろんな方法で対応出来るようになったのはいいことですね。今までのコードも今まで通り動く(ホワイトリスト方式)し。 今回のリリースで手痛い思いをするような作りをしてる人は少ないのかなと思いますが、個人的には設計を見直す必要があるので、ダサイ作り方をしてしまった(自分のプロジェクトがって意味です)と割り切って、考え直します...。

2008年10月16日木曜日

ASP.NET MVC Betaリリース

Download details: MVC Beta


codeplexにはまだ出てないんだけど、ASP.NET MVC Betaが出てましたね。
早速ナオキさんところで、取り上げられててビックリした。

ナオキにASP.NET(仮) : ASP.NET MVC Beta のインストール方法と面白い機能1つ紹介

Viewの追加が簡単になってるのと標準でJavaScriptの場所が/Scriptsになってるんですね。
最後の一文がプレッシャー...。
↓ここにサラッと一覧に書かれてるの発見。

Microsoft ASP.NET MVC Beta Released! - John Mandia's Points of Interest

でも、これだけだとやっぱり細かいと所は分からないので、リリースノートを読むことにします。
Preview 5からの変更点はそんなに多くないみたいな事が書かれてて、ちょっと一安心。
ただ、ソース(出るよね?)もドキュメントもこれからみたいだし、全然わかんない...。

スコット君の言ってた、アクションパラメータへの直接Bind指定の実装はされてるんだろうか(ASP.NET MVC Preview 5 and Form Posting Scenarios - ScottGu's Blog)?


Developer @ ADJUST : ASP.NET MVC でリダイレクト先アクションのURLにアンカーを指定したい - よりスマートな別解

↑こちらのサイトで話題になったUrlの取得もControllerにUrlという名前でUrlHelperが入ったことで、お手軽に出来るようになりましたね。

AcceptVerbs の判定に、独自の条件をつけれるようになったのかな~?作ってみないとよくわかんないですね。ModelStateDictionaryの AddModelErrorがキーとメッセージの2つの引数だけで良くなったみたいで、デフォルトのAccountControllerがそういう実装になってるのが、優しさを感じます(_FORMっていうキー値は適当につけただけなんだろうか。Html.FormがBeginFormに変更になってるのも優しさ?)。
いろいろありそうなので、とにかくいじってみます!