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

2009年1月29日木曜日

ASP.NET MVC RCでIDataErrorInfoの使い方

ソース出てきたので早速IDataErrorInfoのチェック。 普通にソース見れるって幸せだね。

まずはIDataErrorInfoをどこで使ってるのか検索。 そしたらDefaultModelBinderでしか使ってないのが判明。 しかもDefaultModelBinderでは、OnModelUpdatedとOnPropertyValidatedの2箇所だけ。 なるほど。モデル全体の更新完了タイミングと、個々のプロパティ検証時に実行されるわけね。 こういう設計にしたいからModelBinderが大きく変わったんだね。 WPF的な?

IDataErrorInfoだとErrorプロパティとItem(this[string columnName])を実装するんだけど、その中に検証コードを書いてしまうと。んで、入力検証処理はココに集約しましょうと。 ってことは、アレだね、結局DataAnnotationsを使うのも同じだね。DataAnnotationsだとエラー情報を集約して ModelState.AddModelErrorを呼び出すコードは自分で書かなきゃいけないけど検証処理自体は属性ベースで簡単にできる。 IDataErrorInfoだと検証コードは自分で書かなきゃいけないけど、エラーはModelStateに自動で入れてくれる。

どっちもどっちだね。 試しにコードを書いてみた。 Scaffoldingとかも試してみたかったしね!

ビューモデルの定義

ModelsフォルダにPersonViewModelクラスを作成。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ComponentModel;

namespace Mvc.RC.Models
{
 public class WeaponViewModel
 {
   public string Type { get; set; }
   public string Name { get; set; }
 }

 public class PersonViewModel : IDataErrorInfo
 {
   public int Id { get; set; }
   public string FirstName { get; set; }
   public string LastName { get; set; }
   public int Age { get; set; }
   public List Weapons;

   public string Error
   {
     get {
       if (Id == 0 ||
           string.IsNullOrEmpty(FirstName) ||
           string.IsNullOrEmpty(LastName) ||
           Age == 0)
         return "ちゃんと全部の項目入れてね";

       return null;
     }
   }

   public string this[string columnName]
   {
     get {
       string error = null;
       switch (columnName)
       {
         case "FirstName":
           if (FirstName == "チョッパー")
             error = "禁句";
           break;
         case "LastName":
           if (LastName == "トニートニー")
             error = "禁句";
           break;
         case "Age":
           if (Age < 0)
             error = "0歳以上で";
           break;
       }

       return error;
     }
   }
 }
} 
※全体入力チェックでは未入力許すまじなもの。 ※項目入力チェックではチョッパーを入れたらエラーになるようなもの。

コントローラの追加

ControllersフォルダにPeopleControllerを作成。

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 PeopleController : Controller
 {
   //
   // GET: /People/
   private List _people;

   public PeopleController()
   {
     _people = new List() {
    
new PersonViewModel{Id = 1, FirstName = "ルフィー", LastName = "モンキー", Age
= 17, Weapons = new List(){
         new WeaponViewModel{Type="ゴムゴム", Name="ガトリング"},
         new WeaponViewModel{Type="ゴムゴム", Name="鞭"}
       }},
       new PersonViewModel{Id = 2, FirstName = "ゾロ", LastName = "ロロノア", Age = 19, Weapons = new List(){
         new WeaponViewModel{Type="三刀流", Name="鬼斬り"},
         new WeaponViewModel{Type="一刀流居合", Name="獅子歌歌"}
       }},
       new PersonViewModel{Id = 3, FirstName = "ロビン", LastName = "ニコ", Age = 28, Weapons = new List(){
         new WeaponViewModel{Type="ハナハナ", Name="トレスフルール"},
         new WeaponViewModel{Type="ハナハナ", Name="シンコフルール"}
       }}
     };
   }

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

   public ActionResult List()
   {
     return View("List", _people);
   }

   public ActionResult Details(int id)
   {
     return View(_people.First(p => p.Id == id));
   }

   [AcceptVerbs(HttpVerbs.Get)]
   public ActionResult Edit(int id)
   {
     return View(_people.First(p => p.Id == id));
   }

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

     return View(person);
   }
 }
}

※最初にデータをゴッソリ作ってるけど、毎回作成されるから更新しても意味なし! ※最後のEditアクションだけがポイント。

Viewの追加

Controllerの各アクションでAdd Viewを実行してみました。 List/Details/Editとそのままやってみた。 img.aspx

まずはList。 Add Viewから型を指定して作成するんだけど、型って単一モデルじゃないっすか。

img.aspx2

自分でListとかにするのかなとも思ったけどそのままモデルを選んで実行すると、賢 く 「System.Web.Mvc.ViewPage>」っ ていうinherits指定に。

img.aspx3

続いてDetail。 これは特に。そのまんま。

img.aspx4

最後にEdit。ここではWeaponsがどうなるんだろう?と思いつつ実行。 案の定、Weaponsに関しては生成されませんでした。 何でかというとAddView\List.ttですよ。T4ですよ! ウキウキしながらファイルを見てみると、FilterProperties(tt内で定義してる)でモデルからプロパティ一覧を取得。その処理はどうなってるかっていうとコレは単純で、IsBindableType(これまたtt内で定義)でプロパティを展開するかどうかチェック。

そのチェック方法が

if (type.IsPrimitive || type.Equals(typeof(string)) || type.Equals(typeof(DateTime)) || type.Equals(typeof(decimal)) || type.Equals(typeof(Guid)) || type.Equals(typeof(DateTimeOffset)) || type.Equals(typeof(TimeSpan)))

というわけでListやArray、Collectionやらは展開されないってことデス。 なので、自分でWeapons部分は書きます。

            <p>
             <% foreach(var weapon in Model.Weapons){
                  var index = Model.Weapons.IndexOf(weapon);
                  %>
               <%= Html.TextBox(string.Format("Weapons[{0}].Type", index), weapon.Type)%>
               <%= Html.TextBox(string.Format("Weapons[{0}].Name", index), weapon.Name)%>
               <%= Html.Hidden("Weapons.Index", index) %>
               <br />
             <% } %>
           </p>

書くって言っても、ココだけだし。 ※プレフィックス+Indexの名前でhidden作成するっていうのは前と変わらず。 ちなみにFuturesに入ってるHtmlHelperを使うと、TextBoxを以下のように書けます。

            <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>

Html.TextBoxForの部分ね。ValidationMessageが文字列じゃねーか、っていう突っ込みがもちろんあるよね。でも、ValidationMessageについては特にそういう拡張用意されてません。ちょっと中途半端。 コントローラのEditアクション(Post)のところで、ValidateAntiForgeryTokenを指定してるからHtml.Submit付近に<% = Html.AntiForgeryToken() %>を書いておきましょう。表示とPOST送信したのが同じユーザーエージェントかをCookieと hidden(__RequestVerificationToken)で勝手に確認してくれます。エラーならもちろんストップTHE処理。 POST 版Editの定義で引数がPersonViewMode personになってるけど、コレで動かすと残念ながらWeaponsは復元されませんでした(他の項目はOKね)。なので、上記のコントローラコードのままではダメですたい。コレについてはベータと変わらずだね。

なのでEditを変更。

    public ActionResult Edit(int id, PersonViewModel person)
   {
      person.Weapons = new List();
     if (ModelState.IsValid)
       UpdateModel(person.Weapons, "Weapons");
      if (ModelState.IsValid)
       return RedirectToAction("Index");

     return View(person);
   } 

先にnewしておいてUpdateModelでWeaponsを復元。その他の部分についてはそのまま。 コレで復元出来るんだけど、UpdateModelでブレークポイント書けるとちょっと面白い。 img.aspx5

まだUpdateModelを実行してないからWeaponsが復元されてないのはいいとして。 Id に値が入ってるよね!ね!引数idにももちろん入ってる(BeginFormのAction先のURIに含まれてるからルーティングでちゃんと入れてくれる)んだけどpersonクラスのIdも復元されてるっていうのが、今回改善された[Bind(Prefix=""])無しでも復元してくれるっていうヤツ。

肝心のIDataErrorInfoはどうなってるんだってとこですね。 Errorとthisの最初の部分にブレークポイント入れてみる。

img.aspx6

いつ止まるのか気になりますね! なりませんか?そうですか。

最初に書いたとおり、DefaultModelBinderのOnModelUpdatedとOnPropertyValidatedでIDataErrorInfoを使ってると書いたとおり、Editの最初(personを復元するタイミング)で止まります。 最初にOnPropertyValidatedがプロパティの数だけ呼び出されて、最後にOnModelUpdatedが呼び出される。順当。

他にも今回の改訂でCreateModel/OnModelUpdating/OnPropertyValidating/SetPropertyなんかが IModelBinderで定義されてるから、自分でModelBinderを作成するときにはそのタイミングで処理を入れたりも出来たりするみたいよ。 WPFみたいなノリで!

img.aspx7

で、Bindでエラーが発生すると↑こんな感じで。 Model.IsValidがエラーになる(ModelStateにエラー情報が入ってる)から、Weaponsも復元しないしリダイレクトもしない。ちょっと手抜きだけどエラーになるのが分かればよし! ※「ちゃんと全部の項目入れてね」のエラーはIDataErrorInfo.Errorプロパティが出力してる。

こんな感じなんで、DataAnnotationsでもまぁいいかな、なんて気がしなくもなく。データベースに問い合わせてビジネスロジック的にOKかどうかの検証とかココに入れるのはどうなんだ、って思えなくもないし。

2009年1月28日水曜日

待ちに待ったASP.NET MVC RCリリース

なんかもうお祭り状態でフィードの嵐。海外ばっかりだけど 国内だとナオキさん小野さんくらい?

ASP.NET MVC 1.0 Release Candidate Now Available - ScottGu's Blog

とにもかくにもスコガルブログ。

ASP.NET MVC Release Candidate Controls Collection Cannot Be Modified Issue with ASP.NET MVC RC1

Philさんとこ2つ。

Download details: ASP.NET MVC RC 1

何はともあれダウンロードしてインストール! リリースノートも忘れずに。

細かい話はリリースノートに全部書いてる。 スクリプトが用意されててIIS6/7Classicで動かすときの設定が自動化されてるとか。 で、とりあえずスコガルブログの面白ポイントピックアップ。

便利なAdd Controller/Add Viewコマンド

それぞれT4テンプレートエンジンで生成。 マシンレベルならC:\Program Files\Microsoft Visual Studio 9.0\Common7\IDE\ItemTemplates\CSharp\Web\MVC\CodeTemplatesフォルダを書き換え。 プロジェクトレベルならプロジェクト直下にCodeTemplatesフォルダを作ってそのなかに入れる。ControllerテンプレートはAddController、ViewテンプレートはAddView。 ・Add Controllerの内容 単純にIndexアクションを定義するのみ。 ・AddViewの内容 こっちは登録テンプレートリストから生成(Scaffolding)するViewの選択が出来る。 型付けViewPageを選択しないとモデルが分からないから生成する項目が不明になるから、ちゃんと型を指定するんだけど、POCO(Plain Old CLR Object:単純なモデルクラス)を推奨。もちろんいろんな(LINQ to SQLもLINQ to Entityも)のが使えるけど、基本的にはView専用のモデル(ViewData.Modelに入れるモデル)を定義してそっちを使いましょう。

デフォルトで生成出来るタイプは以下の5通り。 ・Create(新規登録) ・Details(詳細表示) ・Edit(編集) ・List(一覧):普通にモデルを指定するとIEnumerableとして生成。賢い。 ・Empty(なんも無し)

便利なGo to Controller/Go to View

アクションのViewResultを返すのところでコンテキストメニューから対象ビューを開いたり、ViewのASPXからコントローラを開いたり。

自動コンパイル

コンパイル時にデフォルトではコードしかコンパイルしないから、View(ASPX)内のインラインコードのエラーは実行時にしか分からなかった (MSBuildの設定変えればBetaでもViewのコンパイルは可能)けど、RCでは最初からそれが出来るコンパイルようなオプションあり。 もちろん毎回そんなことしたらコンパイルに時間がかかり過ぎちゃうから普段使いではViewのコンパイルはしない方がいいかもね。 設定はcsprojの以下の箇所をtrueすればよろし。テキストエディタでよろしく!

<PropertyGroup>
 <MvcBuildViews>true</MvcBuildViews>
</PropertyGroup>

地味にリファクタリング機能追加

Controllerのクラス名を変えるとViewのフォルダ名が変わって、ControllerのActionメソッド名を変えるとViewのファイル名が変わるようにリファクタリング機能も追加。けっこう便利なはず。

これは強烈、Viewのaspxにコードビハインドファイルを全く無しに出来る。

事前アナウンスがあった待望の機能。 型付けViewPageのタメだけのコードビハインドなんていらない! そんな思いを実現するために、Viewフォルダ内のweb.configに不思議な設定が追加されて、Pageディレクティブのinheritsに直接ジェネリックViewPageを定義可能に。 これはもう最高だよね!

ページでのModel参照

ViewPage内でViewData.Modelを参照するときはViewPage.ViewDataプロパティを参照するためにViewDataから書く必要があったけど、これからはViewPage.Modelを参照するからModelから書けばいいさ。 ViewData.Model.HogeをModel.Hogeって書ける。少しだけどコード量が減るね。 もちろん今まで通りViewDataから参照することも可能。

ページタイトルの変更も簡単に

Site.Masterも変更になって、ページタイトル(head内のtitleタグ)がページから簡単 に変更できるようにContentPlaceHolderを2つ(headとMainContent)用意。これで、 ViewData["PageTitle"]とかに入れて、Site.Masterでtitleにセットする必要が無くなってちょっと綺麗に書けそう。 なにげにページタイトルって面倒だもんね。 共通処理にしようと思うと、ページタイトルを取得するためにController/Actionの全組み合わせのデータを内部で保持とかしなきゃだし。

ただPhilさんも指摘の通り、Head自体は相変わらずrunat="server"がついてるからSite.Masterのhead内に直接<%= ~ %>を書くとエラーになりんす。 回避するために<%= ~ %>はContentPlaceHolderをもう一つ作ってその中に入れましょう。 これまでは速攻でrunat="server"を消してたんだけど、ContentPlaceHolderを使いたい場合はでもなくていいんだから、パスの解決にUrl.Contentヘルパーとか使えば、サーバーコントロールにする必要全く無いし。

Futuresの機能だけどテキストフォーム拡張

HtmlHelperを拡張してフォームタグ出力の時に、テキストで項目名を指定(この名前をさらに内部でViewData.ModelやModelStateのリフレクションに利用)してたのを、ラムダ渡しで型付プロパティを直接渡せるようになりました。 ただ、コレクションや配列でプレフィックス使いたいときの指定はどうするのか気になるところ。プレフィックスを指定できるオーバーロードはなさげ。

Bind[Prefix=""]でもインスタンス化

フォームポスト時にBind属性を指定しなくても、ちゃんとデシリアライズしてオブジェクトが復元出来るようになりました。ポストする項目をちゃんと Domain Objectだけにしておけばわざわざプレフィックスをつける必要無いと。ModelBinder関係は凄く変わってるみたい。リリースノートにもなんか書いてある。

IDataErrorInfo

Validationがらみで追加されたのがIDataErrorInfo(もともとSystem.ComponentModelに持ってる)。いまいちよくわかんない。自動でリフレクションでチェックしていってくれるのかな~?だとしたらどのタイミングで?? Item実装時に項目毎のチェック処理を書くのかな~。 それだけだとDataAnnotationsとなにが違うんだろうってことになるから、やっぱりソース見ていつ実行されるのかが分かれば、見えてくる気がする。

Controller.ControllerContextがRequestContextから派生しなくなった

これまで、コントローラのテストが結構面倒だった。 それもこれもControllerContextが今までRequestContextから派生してたからなんだけど、今回から派生しなくなった(プロパティで保持)。 Moqは使ったことない(RhinoMock)んだけど、サンプルの通りこれだけのコードで済むなら乗り換えしようと思えるね。AccountControllerのテストが実際に見れるけど、Moq(じゃなくてもいいけど)使えばもっと簡単にできるような内容だね。

AntiForgeryの標準取り込み

CSRF(Cross Site Request Forgery)を防ぐための、AntiForgeryToken()とValidateAntiForgeryTokenフィルターがとうとう組み込まれた。これまでFuturesで別アセンブリだったのがこれからは標準装備だね。

FileResultクラスとController.File()メソッドでファイルの出力とダウンロードが簡単に

これまたFuturesだったBinaryResultとBinaryStreamResultを一個にまとめて、簡単に処理出来るようになってる。ファイル名を渡さなければ、バイナリストリームを直接レスポンスするし、渡せばattachmentで保存できるように。 と、簡単に書かれてるけど、中身を見てみるとController.File()は用途に合わせて FileContentResult/FileStreamResult/FilePathResult(いずれもFileResultの派生クラス)と3つのActionResultを返すようになってる。FileContentResultがbyte配列、FileStreamResultが Stream、FilePathResultがファイルのパスを指定。 これらはController.File()のオーバーロードだからあんまり意識しなくてもいいのかな。FilePathResultがResponse.TransmitFileで、他2つはResponse.OutputStream.Write。 グッジョブ!

アップロードも簡単に

ファイル出力だけじゃなくてアップロードも簡単にできるようにHttpPostedFileBaseに直接バインドされるようになってる。Request.Filesから取らなくても良くなりました。 ちなみにFuturesには複数ファイルアップロード時のBindも定義されてたよ。 非同期アップロード(iframe/Flash/Silverlight)なら個別アップロードでもいいから使わないかもしれないね。

Ajaxも改善。jQueryのインテリセンスがデフォで

1.3系じゃなくて1.2.6のまま。 なにより、これまでのIsMvcAjaxRequestプロパティはX-Requested-Withヘッダを見るだけだったけど、 IsAjaxRequestプロパティに名前が変わって各種ライブラリ(Prototype.jsも!!あとjQueryとDojo)で使ってるヘッダの解析も実装したので、自分で解析を実装しなくて良くなりました。超嬉しい!! JavaScriptResultの追加も。JavaScriptをそのままレスポンスするものみたいね。 サーバーサイドにJavaScriptコード書くのはちょっと...。

Futuresの中身を見てみた。 ソースがまだ公開されてないけど、そこはそれReflectorで。 なんと待望の非同期コントローラが!! 非同期で処理するには、IHttpHandlerじゃなくIAsyncHttpHandlerが必要になるんで、MvcAsyncHandlerももちろん含まれる。一式ちゃんとある感じ。 もちろんルートに登録するためのMapAsyncRouteもあるよ! Improve scalability in ASP.NET MVC using Asynchronous requests « Steve Sanderson’s blog ↑こんな感じなのかな? 使い方は...。ちょっと試す。 LinqBinaryModelBinderなんてものも。Base64のデータをSystem.Data.Linq.Binaryクラスにバインド。 キャッシュされてても強制書き換えが出来るHtml.Substitute(httpContext => ~)が入ってる。前からあった??

もちろんリリースノートもチェケラッチョ

・これまでformタグを出力するときにHtml.BegineForm/BeginAjaxFormだったからルート名を指定したアクションにポストするときなんかは直接formタグを書かなきゃいけなかったけど、BeginRouteFromが追加されて、そんな苦労ともおさらばだ! ・DropDownList/ListBoxが変更になったみたい。 今まではSelectList/MultiSelectListをViewDataに入れてたけど、これからはIEnumerable<ListItem>でいいんだって。

・沢山のバグフィックス。 "Html.BeginForm and Ajax.BeginForm have been fixed to not render a fully qualified URL." ここが凄く気になる。 Adding HTTPS/SSL support to ASP.NET MVC routing « Steve Sanderson’s blog ↑関係ないかもしれないけど、ここの問題ってコレ系だったりするんじゃないのかと。違うかもしれないけど要チェックや。 ・ベータのコードをRCに移行する方法 1.まずはアセンブリ参照の変更。 2.コンパイルが通るまでがんばってコード直す。 3.Viewsフォルダのweb.configを書き換え。

        <pages
           validateRequest="false"
           pageParserFilterType="System.Web.Mvc.ViewTypeParserFilter, System.Web.Mvc, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"
           pageBaseType="System.Web.Mvc.ViewPage, System.Web.Mvc, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"
           userControlBaseType="System.Web.Mvc.ViewUserControl, System.Web.Mvc, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
         <controls>
           <add assembly="System.Web.Mvc, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" namespace="System.Web.Mvc" tagPrefix="mvc" />
         </controls>
       </pages>

※ViewTypeParserFilter入れとくと、型付ViewPageをinherits指定だけで出来るようになる。 That's it! って、Viewのinherits書き換えたいから、やっぱゴッソリ書き直したくなるね。 最終リリースが来月と思いのほか早い展開になってきた楽しみのつきないASP.NET MVC。

2009年8月2日日曜日

MVC V2のチラ見

やっと、ため込んだFeedの処理に追いついて、ビデオの確認ができた。

Hanselminutes on 9 - ASP.NET MVC 2 Preview 1 with Phil Haack and Virtual Scott | Scott Hanselman | Channel 9

内容も15分と短いので、是非。

ってことで、このビデオ見つつ少しだけv2のソースも少しだけ確認。Editor/EditorFor(EditorTemplateフォルダ)、Display/DisplayFor(DisplayTemplateフォルダ)がそれぞれあって、なおかつLabelForがFuturesから標準に入ってきてる。ここでTextBoxForを作ろうとして、これは拡張性に問題がありそうだと気がついて、Dynamic Dataで使われてるTemplateベースのレンダリングを参考に拡張方法を変えたのかな。前のFuturesには入ってたモンね、 TextBoxFor。

なので、TemplateHelperがかなり重要なポジションをしめる気がする。さらにModelBinderの時と同じようにDefaultDisplayTemplate/DefaultEditorTemplateを用意して、ある程度は自動テンプレート出力をしてくれる模様。すばらしい。

v2 のリリースノートをチラ見した時には「えぇ~、そっちかよ~!」と思ったけど、中身を見ると素敵かも。ITemplateでのテンプレート処理じゃなくてあくまで、HTMLをベースにascxでのテンプレート処理にこだわることで、ViewStateを取り込まないし、クリーンHTMLをはき出せるしで、 MVCらしさをそのまま引き継いだ上での拡張っす!System.Web.UI.WebControls.DataBoundControlModeがInitializeDataCellDataControlRowStateを使わなくて良くしてくれてるんでしょう。

Scott Hanselman's Computer Zen - Hanselminutes on 9 - ASP.NET MVC 2 Preview 1 Released

↑ここにも書かれてる通り、Preview1ではそれほど大量の機能追加はなくて、Preview2が本気リリースっぽい。なので、V2に関しても順を追って追いかけて行くことで、一度に沢山の機能を調べなくて済む感じです。

DataAnnotationsの取り込みがどの程度のものなのか。その片鱗をこのビデオで垣間見れた気がします。Dynamic Dataと同じくらい取り込もうとしてますね。DataType、DisypayNameにUIHint、それらもしっかりとUIの出力(TemplateHelperとDefaultEditorTemplate)に反映されてる。前まではDataAnnotaionModelBinderを使って、システムに対して入力方向での連携で入力値検証でしか利用してなかったのが、V2からは出力方向にもしっかり活用。なので、Templateベースの出力になってるのもうなずける。

大枠はいつものごとく、ガスリーさんところで書かれてるから、そこで確認。

ASP.NET MVC V2 Preview 1 Released - ScottGu's Blog

まだちゃんと見てないけどね!

すでにフィルさんところでAreaに関する投稿(Single Project Areas With ASP.NET MVC 2 Preview 1)もあるし、Maartenさんとこ(ASP.NET MVC 2 Preview 1 released!)にも簡単な説明が出てるので、これから少しずつ見ていこうと思います。

つか、この週末でちゃんと調べる予定だったのに、全然観れなくてちょっと残念...。

2010年2月24日水曜日

ダイヤモンドは砕けない

ゆりかもめに初めて乗りました。

techdays2010 

わーい、自由の女神だー。あははー。あはははー。はぁ...。

台場でTechDays2010のBoFに出てきたんです。

4 の時代の Web アプリケーションを語ってみよう

小野さんとナオキさんと三人でのおしゃべりはTechEd 2009の時とあわせて2度目です。相変わらず打ち合わせとかしないのにはヒヤヒヤです。

ASP.NET 4になっていろいろすごいじゃないですか。その辺の話をしましょうよ、ということだったので、たけはら担当はもちろんASP.NET MVC 2。一応事前に書いておいたメモをブログにも載せておきます。

メジャーな部分

  • データ検証の方法がDataAnnotationsを基本にしたものになりました。

これまで通りIDataErrorInfoを実装したものも有効ですが、ValidationAttributeをモデルまたはモデルプロパティに指定(ModelMetadataで外部に定義したものでも可能)して、DataAnnotationsModelValidatorを使用するようDefaultModelBinderが変更になっています。
LinqBinaryModelBinderも標準実装に組み込まれたので、System.Data.Linq.Binary(SqlDbType.Timestampなど)をbase64でhiddenからポストしたときも、自動で復元されます!
またLinqBinaryModelBinder、基底クラスのByteArrayModelBinder(byte[]を復元)もFuturesから昇格です。

  • ASP.NET 4で組み込まれるSystem.Web.IHtmlStringを使えるようにMvcHtmlStringクラスが導入されました。

MvcHtmlStringのソースファイルを確認するととても勉強になります。まさに黒魔術。
すべてのヘルパーはstringではなく、このMvcHtmlStringを返すよう変更されています。
<%: expression %>としている場合、自動的にHtmlEncodeした結果がレンダリングされるので、今後はこれが主流になります(きっと)。
MVC2の実装でもそうですが、2.0ベースのSystem.Webにはそんなものないので、なので、ASP.NET 4じゃない場合はこれまで通り、Html.Encode(string)を使いましょう。
IHtmlStringの場合、<%: ihtmlstring %>となっていても、HtmlEncodeをかけずに出力するので、Partial HTMLをレンダリングする場合(ヘルパーのレンダリングとかも)は、IHtmlStringとして渡しておきましょう。

  • AcceptVerbsAttributeがHttpVerbs.Get/Post以外に、Put/Delete/Headにも対応するようになっています。

Get/Post以外は通常、ブラウザからは送信されないですが、HtmlHelper.HttpMethodOverride()をformタグ内で呼んでおくことで、hiddenにX-HTTP-Method-Overrideという名前で、メソッドオーバーライドを保持するようになり、POSTを使ったHttpVerbsの上書きができるようになります。
この辺の実装はHttpRequestExtensionsに用意されてるHttpRequestBaseに拡張メソッドとして実装しているGetHttpMethodOverride()がまるっと処理してくれるようになっています。
もちろんここも、AcceptVerbsAttributeの判定メソッドがoverride可能になっているので、独自のAcceptVerbsAttributeを実装することで、Railsライクに"_method"というオーバーライドを使うように変更することも可能です!
これは今までのバージョンではとても面倒な実装 にしないとできない部分でした。
ちなみに、RESTfulなController実装をするときに、これができないと、ブラウザからのリクエストと、その他のクライアントからのリクエストの処理を簡単に切り分けできなくてとても不便です。

  • JsonResultを返すActionの場合、HttpVerbsがPOSTであることが基本条件になりました。

セキュリティ的にゴニョゴニョらしいです。

  public ActionResult Json()
  {
    //var json = Json(new {result = "json!"});
    //json.JsonRequestBehavior = JsonRequestBehavior.AllowGet;
    //return json;
return Json(new {result = "json!"},JsonRequestBehavior.AllowGet); }

Getなら↑これで大丈夫。

修正:2010年3月9日

  • DefaultControllerFactoryからRequestContextプロパティが無くなりました。

依存性をなくすのはいいことです。その代わり、Controllerのインスタンスを生成するためのCreateControllerにRequestContextをパラメータとして渡すようになってます。
Mockを作る時が楽ちんです。

  • AreaをサポートするためにRouteData.Valuesで"area"が予約されるようになりました。

自分仕様でこの名前の値を使ってる場合は変更しておきましょう。

  • Html.Substituteは残念ながら使えなくなりました。
  • クライアントサイド検証が標準で組み込まれてます。

このクライアントサイドコードを生成するために、C#からJavaScriptを吐き出す
ScriptSharpが使われています。
コード生成に興味がある方は、参照してみるといいんじゃないかと思います。

Script#
http://projects.nikhilk.net/ScriptSharp

ボスが最近似たようなのを見つけてはしゃいでました。

SharpKit - Write C# instead of JavaScript
http://sharpkit.net/

  • Templateベース

DynamicDataと同じようにTemplateベースのモデルレンダリングを実装したHtml.EditorForやDisplayFor、プロパティベースのレンダリングを行うTextBoxForやLabelForなんかも目が離せない便利機能です。
System.ComponentModel.DisplayNameAttributeやUIHintAttributeを使ってカスタマイズもしやすくていいですね。

注目機能

  • ChildActionOnly属性

HtmlHelperのRenderAction/Actionからの要求しか受け付けないようにするActionFilter。

  • UrlParameter.Optional

Route登録時にRouteData.Valuesにキーすら含ませないようにするオプション。

Deep Dive!

一番注目したいのはASP.NETらしさをしっかり継承したProviderモデルへのリファクタリングです。マニアックな部分ですが、拡張性を考慮して、より自由どの高い開発を行えるよう沢山のフックポイントを提供するために実装されています。処理の流として入力→検証(Model)→処理(Controller & Model)→出力(View)
というフローになるそれぞれのつなぎ部分で拡張できる感じです。

MVCソースから"Provider"と付いているクラスを検索!

  • ModelMetadataProviders
    • ModelMetadataProvider
      • AssociatedMetadataProvider
        • EmptryModelValidatorProvider
        • DataAnnotationsModelValidatorProvider

ModelMetadataを取得するためのプロバイダ。
モデルやモデルプロパティに関するすべてのメタ情報。
ValidationAttributeの定義や、型情報、出力方法など。

  • ModelValidatorProviders
    • ModelValidatornProvider
      • AssociatedValidatorProvider
        標準のValidationAttribute用のAttributeAdopterクラスとDataAnnotationsModelValidatorを管理。
        • EmptryModelValidatorProvider
        • DataAnnotationsModelValidatorProvider
      • DataErrorInfoModelValidatorProvider
        IDataErrorInfoの実装に対するDataErrorInfoPropertyModelValidatorとDataErrorInfoClassModelValidatorを使って、検証の結果を取得する。
      • ClientDataTypeModelValidatorProvider
        数値型に関するNumericModelValidator。
    • DataAnnotationsModelValidatorProvider
    • DataErrorInfoModelValidatorProvider
    • ClientDataTypeModelValidatorProvider
  • IValueProvider
    • NameValueCollectionValueProvider
      • FormValueProvider
      • RouteDataValueProvider
      • QueryStringValueProvider
    • HttpFileCollectionValueProvider

ModelBinderがモデルにデータをバインドするさいに、値を取得する際に利用する。データの出所がどこなのかをModelBinderは意識しなくてもいいんです。

次に"Factory"とついたクラスを検索!

  • DefaultControllerFactory
  • ValueProviderFactories
    • ValueProviderFactory
      • FormValueProviderFactory
      • RouteDataValueProviderFactory
      • QueryStringValueProviderFactory
      • HttpFileCollectionValueProviderFactory

コントローラの生成用のControllerFactoryと、IValueProviderを実装したValueProviderFactoryたちの2種類です。これがリファクタリングした超重要な部分になります。

で、後は、これらを利用したサンプルの紹介という流れです。

MVC Presenter

↑これね。このサンプルそのものがWebアプリケーションになっていて、サンプルの実行そのものがプレゼンスライドの表示になるという仕掛けだったんですけど...。

プロバイダモデルがうんぬんくらいからちょっとツマラナイ話になってしまいましたね。今回はコードに関する説明がごっそりできてないので、MVC2の良くなった部分がサンプルのどこにどう使われているのか、全然伝えられませんでした。反省してます。Zipで圧縮したのをアップロードしてどうのこうのとか、実はパワポじゃないんだよとか、それ以前に何を伝えようとしてるのかが、伝えられてなかったです。ドン引きってこんな時に使うのかな。

心が折れて、帰りのゆりかもめでは夕日が目に染みた。泣いてなんかないやい。

JOJO'S BIZARRE ADVENTURE Part4 Diamond is not Crash

けど、魂を砕くことはできないぜ!また機会があったら、今回の教訓を生かし、もっと参加者に楽しんでもらえるよう的を絞ったプレゼン+コードにしようと思います。

サンプルの説明を近いうちここにエントリしようと思います(ここ知らない参加者の方には申し訳ないですが)。

2009年3月4日水曜日

ASP.NET MVC RC2リリース

when its going to be released - ASP.NET Forums

↑ここで発見。最後の発言が...。まぁ、そうですけど...みたいな。

いろいろ気になるところがあったんでしょうね。安易にRTMを出さず、品質上げるためにRC2リリースの決断に敬意を表します。

ASP.NET MVC Release Candidate 2

とはいえ、RC1との違いが単にインストーラだけなわけ無いですよね。しっかりとリリースノートを確認しましょう。

CodePlexにソース/Futuresも上がってるんですが、その下にDataAnnotationsを使ったModelBinderのサンプルが...。もっと早く出してくれれば、自分でサンプル書かなくても良かったのに...。

このDataAnnotationsModelBinderの実装を確認してみると、その内容がまた超かっこいいです。IDataErrorInfo使わずに、DefaultModelBinderを派生させて、OnPropertyValidatingのオーバーライドメソッド内でValidationAttribute属性の取得・実行とModelStateへのエラー投入を行ってる。入力検証の集約というより、DataAnnotationsはそれだけで検証を完結させておき、モデル自体の検証や、ビジネスルールの検証は切り離して行うという設計。IDataErrorInfoならErrorのオーバーライドでモデル検証も集約できるから、どっちが分かりやすいコード(このサンプルだとモデル検証は別途モデル層に実装しましょうということになる)になるかは、アプリケーション設計者の好みでどうぞ、ってことですかね。

リリースノートに書かれてる変更点(それほど多くない)をさらっと確認してみると、以下のような感じです。

  • .NET Framework 3.5SP1必須です。入れといてね。
  • サーバーインストールモードを用意しました。VS関係のインストールがないのでデブロイ環境へのインストールはこれを使いましょう。 msiexec /i AspNetMVC1-RC2.msi /l*v .\mvc.log MVC_SERVER_INSTALL="YES"
  • GACに入れるよ。
  • AntiForgeryのCookie出力時にパスを指定できるようにしました。なので、デプロイ環境(ルートからとかサブフォルダあるとか)に合わせてパスを調整できます。
  • DefaultModelBinderが出力するエラーメッセージをローカライズできるようにしました。resxファイルを独自に定義して分かりやすいエラーメッセージを登録しておきましょう(InvalidPropertyValueとPropertyValueRequiredの2個だけ)。
  • ValidationSummaryのオーバーロードが増えたよ。メッセージリストをul/liタグで展開する前にspanタグでのタイトル表示できるなり。
  • jQuery1.3.1にしたよ。
  • あとバグフィックス。DropDownListの例外。web.configのauthenticationの値をLogOnに。Site.Masterとかで使ってるheadタグがrunat="server"をちゃんと動くように。checkboxとradiobuttonをModelStateからちゃんと復元。ルートのDefault.aspx(ルーティングでコントローラ+アクション指定してるとルートアクセスでデフォルトのコントローラ+アクションが実行されるけど、これにOutputCacheを指定しても効かない)でOutputCacheがちゃんと効くように。

基本的にRC1のコードならそのまま動くっぽいですね。

追記:2009/03/04 16:50

System.Web.AbstractionsとSystem.Web.Routingの2つのアセンブリがSP1のものを利用するように変わったんですね。なので、System.Web.Mvcだけ(Futuresを利用するならMicrosoft.Web.Mvcも)を配布すればよくなりました。グッジョブ!

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年8月30日土曜日

まさかまさかのPreview5

早いタイミングで出てきたのはいいけどPreview5とは。 実際、Controller変ったりViewEngine変ったりしてて大きな変更だからPreviewのままなんだろうね。 ※ViewEngineを作ったりはしんどそうだから、特に興味ないぜ!

img.aspx

ばびゅ~んとインストール。 後先考えずにインストール。 今までのプロジェクト動かないのはRelease Noteみたら書いてるから覚悟はしてたけど、ここまでダメだとは...。しかも、Sourceはまだ公開されてなくて、どうやって追っかけるんですか...。またしても見切り発車で先走り過ぎた。まぁ、いいや。

HtmlHelperが大きく変わって全く動かなくなりますね。 追加されたものとして↓。 HtmlHelper.RenderAction HtmlHelper.RenderRoute HtmlHelper.RenderPartial

今までのRenderUserControlが無くなって、これらが追加されてます。 これはViewDataの継承とかが全然変わるんじゃ...? と、思って簡単なテストプログラムを書いてみました。

まずはPreview 5で新規プロジェクト作成。

1.ModelsにUserViewData.cs(クラス)を追加。

namespace MvcApplication1.Models
{
public class UserViewData
{
  public string UserName { get; set; }
  public string ViewName { get; set; }
  public string Message { get; set; }
}
} 

2.Views/HomeにUsers.aspx(MVC View Content Page)を追加。

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" AutoEventWireup="true" CodeBehind="Users.aspx.cs" Inherits="MvcApplication1.Views.Home.Users" %>
<asp:Content ID="Content1" ContentPlaceHolderID="MainContent" runat="server">
<h2>ユーザー</h2>

<h3>ページで表示するメッセージ:<% = Html.Encode(ViewData["ViewMessage"]) %></h3>

<%
 foreach(var user in ViewData.Model)
   Html.RenderPartial("~/Views/UserControls/User.ascx", user);
%>

</asp:Content>

コードビハインドで型指定

namespace MvcApplication1.Views.Home
{
 public partial class Users : ViewPage<List<Models.UserViewData>>
 {
 }
}

3.Viewsに"UserControls"フォルダ作成。 4.Views/UserControlsにUser.ascx(MVC View User Control)を追加。

<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="User.ascx.cs" Inherits="MvcApplication1.Views.UserControls.User" %>

<h5>コントロールで表示するメッセージ:<% = Html.Encode(ViewData["ViewMessage"]) %></h5>

<dl>
 <dt>ユーザー名</dt><dd><%= Html.Encode(ViewData.Model.UserName) %></dd>
 <dt>表示名</dt><dd><%= Html.Encode(ViewData.Model.ViewName) %></dd>
 <dt>メッセージ</dt><dd><%= Html.Encode(ViewData.Model.Message) %></dd>
</dl>

コードビハインドで型指定

namespace MvcApplication1.Views.UserControls
{
 public partial class User : System.Web.Mvc.ViewUserControl<Models.UserViewData>
 {
 }
}

5.HomeControllerにUsersアクション追加。

    public ActionResult Users()
  {
    ViewData["ViewMessage"] = "今日も雨が降ったり止んだりだね。";
    var users = new List()
    {
      new Models.UserViewData(){UserName = "takehara", ViewName="たけはら", Message="運動不足"},
      new Models.UserViewData(){UserName = "mauri", ViewName="マウリ", Message="ホッケーばっかり"},
      new Models.UserViewData(){UserName = "suzuki", ViewName="すずき", Message="ホッケーのみ"}
    };
 
    return View(users);
  }

6.Home/Index.aspxにリンク作成。

<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" AutoEventWireup="true" CodeBehind="Index.aspx.cs" Inherits="MvcApplication1.Views.Home.Index" %>

<asp:Content ID="indexContent" ContentPlaceHolderID="MainContent" runat="server">
   <h2><%= Html.Encode(ViewData["Message"]) %></h2>
   <p>
       To learn more about ASP.NET MVC visit <a href="http://asp.net/mvc" title="ASP.NET MVC Website">http://asp.net/mvc</a>.
   </p>

   <%= Html.ActionLink("ユーザーページ", "Users") %>  
</asp:Content>

で、これを実行した結果の画面が↓これ。 img.aspx2

相変わらず、ViewDataが参照できてないですね。 が!!

RenderPartialには違うオーバーロードがあってですね、User.aspxのコードを以下のように変更。

<%
 foreach(var user in ViewData.Model)
   Html.RenderPartial("~/Views/UserControls/User.ascx", user, ViewData);
%>

すると...。 img.aspx3

あびりーばぼー!!

ViewDataDictionaryの中身をUserControlの中から参照できるようになりました。 今までのRenderUserControlだと型指定してViewDataを渡すとディクショナリの中身は見れなくなってたんだけど、これでそのページで利用したい情報はすべてViewDataDictionaryに入れておくことで、全部のUserControlから参照できるようになりますね。 最高っす!!

ここで、RenderPartialが直接結果を返さないのがミソ。実行はギリギリまで遅らせてTestしやすくってことですな。モック使ってね。

次に変わってんのがHtmlHelper.ActionLink/RouteLinkのオーバーロード。object型の引数の解釈が先にHtmlAttributes。ちゃんとUrlが生成されなくてビビるので気をつけましょう。

今までのコードをなるべくそのまま使うなら↓こんな感じでnull渡しましょう。

Html.RouteLink("リンク", "RouteName", new {val1="1", val2="2"},null)

HtmlHelperはいろいろ変更が多そうだけど、ここら辺押さえておけば以降は大丈夫そう。 次はControllerでの変更点。 ビビるのはBindingHelperExtensionsが無くなってるとこ。 BindingHelperExtensions.UpdateFromが...。これがないとRequest.Formの値をまとめて取り出せないじゃないか。と、みんな同じようなことを思ったみたいでForum確認してみたらちゃんと答えが。 Controller.UpdateModelを使えと。なるほど。確かに取得できる。Request.Formを指定しなくても良くなってたり、TryUpdateModelを使えば例外も起きないっぽい。

さらに今まで、Executeの前後の処理をController.Executeをoverrideしてそこに実装してたけど、これがなんとinternalになって、overrideできなくなりました。 が、しかし、これはちゃんとRelease Noteに書いてるから、すんなりとExecuteCoreのoverrideへ切り替え。でも、Initializeのoverrideでいいような処理だから、こっちにしよ。 ActionFilterAttributeもなんか結構変わってて。

ActionMethod.Name何処にいったんですかね。リフレクションでどうのこうのって書いてるけど、RouteDataから引っ張ってくればいいんですかね? とりあえずはfilterContext.RouteData.Values["action"]でAction名はとれる。 Cancelもなくなってるし、アクションが実行されなかったかどうかはどこでセットすればいいんですかね? どうしよ的な変更箇所が多かったりする中で、RenderPartialヘルパーが実装されてたり、AcceptVerbでRESTfulっぽくアクションを書けたり(PUT/DELETEはどうすんの?_methodで書き換えれるのがいいんだけど.NET Reflectorで確認した限りではHttpMethodをそのまま判定に使ってるっぽいから、自分で書いたRESTfulAttributeクラスからの乗り換えはないかな)、AjaxHelperがSystem.Web.Mvc.Ajaxに移動してたり、よさげなこともあるから(他にも FileResultクラスがあったり)しっかりチェックしていこうと思うところです。 How to use the ASP.NET MVC ModelBinder - Melvyn Harbour ↑ModelBinderの使い方サンプル。

Maarten Balliauw {blog} - Form validation with ASP.NET MVC preview 5 ↑ModelStateを使った入力検証のサンプル。

これViewDataに入れたりして、エラー項目を保持するのを自分で実装しなきゃいけなかったりしてたけど、超いい感じ!

2010年8月29日日曜日

MVC Graffiti

宇宙兄弟面白いよね!ムッタと日々人の宇宙飛行士を目指す兄弟の話。

...。

去年に引き続き、今年もTechEdでBoFに呼ばれたので行ってきました。TechEd自体は3日間の開催期間があるんですけど、月曜から水曜まで会社で合宿状態で、初日は参加できず。2日目は現地入りしたものの、ずっと障害調査でパシフィコにWiFi借りに行っただけみたいな状態。ちなみにこの時点でBoFで使う予定のデモプログラムはまだちゃんと動かず。貝になりたい。

IMG_0057

去年はオープンしてなかったけど、今年はオープンしてるクリスピー・クリームにココロオドル。

今回のデモのテーマは「人類の進歩と調和」です。ウソですね。はいはい。テーマなんて特に無いです。

bof1

↑これです。多人数で同じキャンバスに絵を書くアプリで、一般的にはなんていうのかな、お絵描きチャットとでも言うんでしょうか。そんなようなものです。

仕組みとしてはいたってシンプルです。

WebアプリケーションとしてASP.NET MVC 3 Preview1をベースにプロジェクトを作成。もちろんViewはRazorです。ここは実質ほとんど処理もなく、Webアプリケーションの構造を制御するためと、サーバーサイドでサムネイル画像を生成するくらいのシンプルな構造です。

データ通信はWCF Data Servicesを使ってます。XMLではなくクライアントサイドのJavaScriptからJSONでデータの取得・更新・削除をRESTで行うのに、これより簡単に実装する方法はないんじゃないでしょうか。実質EFでモデルを定義し、標準テンプレートにジェネリッククラスに指定するくらいしか作業はない感じですよね。今回は少しWebGetで機能を追加してます。

残るはクライアントサイドのJavaScriptで、HTML5のCanvas要素にレンダリングしたりXHRを使った通信部の実装です。

「マイクロソフトのWeb テクノロジ最前線と現実解を語ろう」

が、BoFのタイトルでしたよね。覚えてますか?

ということで、今回のデモで使ったテクノロジは以下の通り。

  • ASP.NET MVC 3 Preview1(Razor)
  • WCF Data Services
  • HTML5(Canvas)
  • CSS3
  • jQuery

MVCの中ではViewに対してデータをほとんど送ってないです。これはつまりMVCでデータ制御をほとんど行ってないからです。データに関してはすべてWCF Data Servicesで通信してるので、クライアント部をSilverlightにしたとしても、そのままデータは利用できるので、もっとリッチなキャンバスを作るのも面白いかもです。

四の五の言わずにソースコードですよね。今回はCodePlexにTFSでアップしようと思って、いろいろ試したんですが...。ちょっとまだうまくできてないです。なのでとりあえずSkyDriveに置いておきます。

↑ここからダウンロードして展開してソース見てみてね!データベースのセットアップはスクリプトになってるので、MvcGraffitiという名前でデータベースを作成後(なんでもいいです)Databases\MvcGraffiti.sqlを実行してテーブルを作成してください。BoFで使ったデータもそれぞれスクリプトにしています。作ったデータベースに合わせてWeb.configのconnectionStringを適当にいじってください。これで動くはず。うまくいかない場合は連絡いただければできる範囲で対応していきます。このエントリのコメント欄によろしくです。

今回のサンプルはwebkit系ブラウザにのみ最適化して作ってます。なので、HTML5対応してるとうたっている他の実装ではちゃんと動きません。そこは手抜きです。座標の計算とかの部分がちょっとへんちくりん。PCでのお勧めはSafariではなくChrome。なぜかというとSafariではWheelイベントが発生しなくてマウスのコロコロをつかったズームが効かない(Zoom自体はCSSのZoomを使ってます)から。後はMobile SafariとしてiPhone4とiPadで動作確認してます。こちらはWheelイベントではなくTouch系イベントをハンドリングしてます。なのでJSのソースを見てもらうとわかるんですが、PC向けのMouseInputクラスとMobile向けのTouchPanelInputという2種類のクラスを用意して入力デバイスごとの処理を記述してます。といっても、イベントのハンドリングと座標の取得方法が違うくらいですが(そこが一番面倒...)。

Mobile Safariを使った場合、指2本でズームしたり、キャンバスをドラッグできるようになってるので試してみてください。この辺、意外と知られてないと思いますが、イベントハンドリングして自分ですべて実装する必要があります。ただのHTMLに対してはそんなことしなくてもいいんですけど、今回Canvasお絵かきなので楽はできないってことですね。

MVC3になって目に見えて違うのはViewDataのほかにViewModelプロパティ経由でViewにデータを渡せるようになってるところでしょうか。ModelBinderは今回のデモで使ってないので。ViewModelはdynamic型なので、データだけじゃなくFunc<T>渡せます。今回のデモの中でIPアドレスをViewで表示してるところが無駄にこの機能を使って、IPアドレスそのものではなく、IPアドレスを返すデリゲート経由でViewは表示してます。GraffitisControllerの36行目と、List.cshtmlの31行目。可能性は無限ですね。使い過ぎ注意ですけど(特にデータをどうのこうのするようなものには適用しないほうがいいかも)。

VS2010だとしても、まだRazorに対する対応はできていないので、コードハイライトもインテリセンスも効きません。なにも設定しないと、VS2010がただのメモ帳です。なので、コードハイライトだけでも行いたい場合は拡張しcshtmlにたいしてHTMLエディタを関連付けしてみましょう。

bof2 bof3

そうすると↑こんな感じにはなります。あとは拡張機能マネージャで"Razor Syntax Highlighter"を入れるとかでしょうか。黒バックの場合切ない表示になるので個人的には使ってないですが、白バックならきれいに表示されます。

あと、Razorの構文については英語の資料になりますが、以下のサイトからダウンロードできます。

Download details: ASP.NET Web Pages with Razor Syntax

200ページ越えです。読み応え抜群です。後はASP.NET MVC3 Preview1のソースをダウンロードするとRazorのパーサーとWebPage関連一式のソースも含まれてる(MVC専用ソースなのがPreview1のかわいげのあるところです)ので、パーサーマニアにはたまらないでしょう。

Tipsとしては困ったときの<text/>。これで囲むといったんパースのコンテキストがCodeParserからMarkupParserに切り替わるので、c#の終わりがわからない!と怒られた時には試してみましょう。

残るはWCF Data Services。かつてADO.NET Data Servicesと言われていたものです。大きなところではAtomPub/JSONだったのがAtomPub改であるODataに対応してるところですね。それがどううれしいかというと会場でも話しましたが、PowerPivotによるセルフサービスBIの促進による無駄エネルギー使用(レポート作成のエネルギーは顧客が使えばよろし)の撤廃です。この辺話し出すと長くなるのではしょります。デジタルナーバスシステムです。ね!ゲイツさん!

Microsoft Developer Network Weekly News Japan = Vol.46 = 2/ 25/98

ふるっ!

WCFのレイヤで介入するIDispatchMessageInspectorを使うと、Data Servicesの処理に到達する直前に介入できるのと、Data Servicesのレイヤで介入するQueryInterceptorやChangeInterceptorなんかを利用するとスマートにデータアクセスに対して機能拡張が施せます。

今回はInspectorの使用例としてJSONP and URL-controlled format support for ADO.NET Data Services - Homeで公開されているQueryStringによるformat指定Behaviorを使ってみました。プログラム中ではまったく有効利用しません。これってどういうことかというと、WCF Data Services自体が無効なQueryStringは例外を出してくるので、好き勝手なQueryStringを使うことはできない仕様なんですが、InspectorでData Servicesの処理が始まる前に$format指定をリクエストヘッダ"accept"に移動させるということをしています。そうするとData Servicesからすれば普通にContentTypeを指定してリクエストされたものだと判断して処理を進めてくれるので、自分でそれ以上の処理をする必要がなくなるわけですね。今のところAtom/JSONしかないですけど、多分うまいこと介入すればCSVをレンダリングとかもできるんじゃないかとにらんでます。どこかのProviderなんじゃないかな~。適当ですいません。

デモアプリケーションのJavaScriptの構造としては以下のような感じです。

bof5

network部はtimerでsend/receiveをくるくる回してます。あんまりリアルタイムにはしてないです。

サーバーサイドの構造としては以下のような感じです。

bof6

雑にもほどがある...。

あれですよ、デベロッパーたるもの「コードで語ってくれ」ですよ。九十九も「拳で語ってくれ」って言ってたじゃないですか。

BoF後に「WebSocketとかもいいですよね!」と声を掛けてくれた方がいましたが、まさにその通りで、今回のようなアプリケーションの場合XHRで処理するよりWebSocketを使ったほうがより効率のいい通信ができるでしょう。あと、コードの9割がJavaScriptだし、iPad持ち出してデモしたりとMSのテクノロジがあまり使われてないじゃないか!なんて野暮なことは言いっこなしです。ちゃんと使ってるんですよ。ただコード量が少なくて済むというだけです。なので全体比率としては少ないです。localStorageやsessionStorageなんかを使うと完全にスタンドアロンで動かすこともできるようになるので面白そうですね。

それと、今回EFを使ってWCF Data Servicesでアクセスするようにしてますが、リードオンリーなデータ公開ならLINQ to SQLをベースにするかPOCOテンプレートを使うほうが使い勝手はいいと思ってます。更新系はMVCでバリデーションかけてそっちから更新する感じです(更新はUIにフィードバックしやすいほうが開発が楽だと思いますがどうでしょう?)。バランスですね。

Ask the Speakerのほうでも話題になりましたが、SQLをジェネレート任せにせず、自分でチューニングしたものをEFなりLINQ to SQLで使いたい場合はどうするのさ、についてはストアドで書いて、DataContext/ObjectContextにはやす感じですね。カスタムなエンティティとして返すもよし、他のテーブルと同じ定義を戻すもよし、です。その後動的に条件を付ける場合はIQueryableにどんどこ追加してしまいましょう。最初のクエリが効率よく絞れて抽出できてればあとは、オンメモリのLINQ to Objectで!

だいたい、こんな感じです。わからない・わかりにくい点など多々あると思うので、そんな時には質問いただければと思います。

いきなり話は変わりますが、WebMatrixなどの他のツールについての話が出てましたが、最近読んだ本で「Amazon.co.jp: 続・ハイパフォーマンスWebサイト ―ウェブ高速化のベストプラクティス: Steve Souders, 武舎 広幸, 福地 太郎, 武舎 るみ: 本」っていうのがあるんですが、ちょっといい感じの記述があるので紹介。付録Bの"Yahoo!JAPANが実践するWebの高速化"のB.5.1役割分担。

大きく分けて3つの開発フェーズがあり

  • コンテンツの情報を設計する「インタラクションデザイン」フェーズ
  • コンテンツの見た目をデザインする「ビジュアルデザイン」フェーズ
  • コンテンツのHTML/CSSを実装する「ウェブデベロップメント」フェーズ

となります。

ここでWD(ウェブデベロッパー)とFEE(フロントエンドエンジニア)という役割がありまして、それぞれ担当するフェーズも違うわけですね。ここが難しいところなんですが、「デザインはデザイナー」というのが口癖のこの業界、デザインとは何を指しているのでしょう。見た目だけ?動きのある場合は動きもデザインするのがデザイナー?そもそもの情報設計はデザイナー関与せず?HTML/CSSの実装は?その辺のふわっとした垣根のおかげで、お互いが密に連携できず、特に会社が別々だったりすると誰がどこまで担当してるのかで、ごにょごにょな状態になりやすかったりね。WebMatrixはフェーズの違う部分の発生するロスを減らしていくためにフェーズをまたいだツールとしての意味もあったりするんじゃないかと勝手に妄想してます。

最後に、BoFに参加してくれたたくさんの開発者の方々、いきなりキャンバス作りまくったり削除しまくったりと、想定外の操作で会場を盛り上げてくれてありがとうございました!質疑応答があまりできなくてすいませんでした。

小野さん、ナオキさん、今回も声掛けてくれてありがとうございます。Techdaysのトラウマがはれそうです。

それとチャック。急きょWiFiルーターを貸してくれてほんとにありがとう。あれがなかったら会場に来てくれた方々は見るだけで、実際にキャンバスに描くこともできず、企画倒れになるところでした。

なんのその、男は裸百貫の波に立つ獅子であれ

2011年6月30日木曜日

全角数値を半角数値に変換するModelBinder

拝啓、まゆきっつぁん

いつも、The Shodoにて筆の練習をさせていただいてます。ただ、いつまで経っても筆ぺんでは上手にかけるようになれません。本物の習字道具を使わないと練習の成果が出ないのでしょうか?The Fudepenで練習すると効果が期待できるかもしれないですね。是非、考慮いただければと思います。

先日のmvcConf @:Japan懇親会での一件について。ふと、思い出したので書いてみました。あの時はValueProviderがどうのこうのという話になったような気がしないでもないですが、ValueProviderではレイヤ低すぎて型は意識されてないのダメですね。

こんな感じでいかがでしょうか?整数に限定してますが、応用すると他にもいろいろできると思います。

public class IntegralModelBinder : DefaultModelBinder
{
  private readonly List<Type> _integralTypes = new List<Type>
  {
    typeof (sbyte),
    typeof (byte),
    typeof (char),
    typeof (short),
    typeof (ushort),
    typeof (int),
    typeof (uint),
    typeof (long),
    typeof (ulong)
  };
  private const string WideIntegrals = "1234567890一二三四五六七八九零壱弐参肆伍陸柒捌玖零";
  private const string NarrowIntegrals = "123456789012345678901234567890";

  protected override void SetProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value)
  {
    if (!_integralTypes.Contains(propertyDescriptor.PropertyType) || value != null)
    {
      base.SetProperty(controllerContext, bindingContext, propertyDescriptor, value);
      return;
    }

    var providerResult = bindingContext.ValueProvider.GetValue(propertyDescriptor.Name);
    value = providerResult.AttemptedValue;

    var narrow = string.Join("",(value + "").Select(c => WideIntegrals.Contains(c) ? NarrowIntegrals[WideIntegrals.IndexOf(c)] : c));
    var converter = TypeDescriptor.GetConverter(propertyDescriptor.PropertyType);
    try
    {
      value = converter.ConvertFrom(narrow);
      bindingContext.ModelState.Remove(propertyDescriptor.Name);
    }
    catch{}

    base.SetProperty(controllerContext, bindingContext, propertyDescriptor, value);
  }
}

試しに以下のようなモデルを定義してみました。

    public class Person
    {
        public string Name { get; set; }
        public int Age { get; set; }
        public DateTime? Birthday { get; set; }
        public byte Rank { get; set; }
    }

AgeとRankが整数型なので処理対象となります。Global.asaxでDefaultBinder(ModelBinders.Binders.DefaultBinder = new IntegralModelBinder();)を差し替えて実行した結果は↓こんな感じになります。

mb1

@model ZenBinder.Models.Person
@{
    ViewBag.Title = "ホーム ページ";
}

<h2>@ViewBag.Message</h2>

@using (Html.BeginForm())
{
    @Html.EditorForModel()

    <button type="submit">送信</button>
}

まずはフォームを出すでしょう。簡単にEditorForModelを使います。

mb2

普通に半角だけで試して送信してみると、ちゃんと動きます。同じものを全角にしてみても結果は同じになります。

mb3

変換出来ない場合はDefaultModelBinderの挙動になります。十とか百とか千も変換するためのマッピングを用意すれば、もう少しオシャレさが増すかもしれないです。

mb4

いかがでしょう。ケータイでの入力に是非応用してみてください。

dotnetConf2015 Japan

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