2009年1月31日土曜日

ASP.NET MVC RC Refresh?

エントリーはなぜか無くなってるけど、Philさんとこでアナウンスがあった。 ダウンロードは生きてるし、実際ダウンロード出来るんだけど、なんでエントリーが消えてんだろう。 まだまだ、変更するのかな。 ASP.NET MVC RC Refresh

復活! Philさんところからのリンクで直接ダウンロードしましょう。 あ、いや、ここね。

img.aspx

ちなみにUrl生成に関する処理が、ベータの時と同じ動きになるようにソースを戻したらしい。って、ことは、ソースも新しくダウンロード出来るようになるのかな。なんかバタバタしてきたな~。 BeginForm now has not qualified action... - ASP.NET Forums まずは↑この問題。

Home/Index.aspxに以下のコードを書いてみた。

    <% using (Html.BeginForm("Home","Index")){ %>
   <% } %>

   <% using (Html.BeginRouteForm("Default")){ %>
   <% } %>

   <% using (Ajax.BeginForm(new AjaxOptions())){ %>
   <% } %>

↑こうかくとRCでは↓こう出力される。

    <form action="Index/Home" method="post"></form>
   <form action="./" method="post"></form>
   <form action="http://localhost:17595/" method="post"
         onsubmit="Sys.Mvc.AsyncForm.handleSubmit(this,
                   new Sys.UI.DomEvent(event), {
                    insertionMode: Sys.Mvc.InsertionMode.replace });">
     System.Web.Mvc.Html.MvcForm
   </form>

RC Refreshでは↓こう出力される。

    <form action="/Index/Home" method="post"></form>
   <form action="/" method="post"></form>
   <form action="http://localhost:17595/" method="post"
         onsubmit="Sys.Mvc.AsyncForm.handleSubmit(this,
                   new Sys.UI.DomEvent(event), {
                    insertionMode: Sys.Mvc.InsertionMode.replace });">
     System.Web.Mvc.Html.MvcForm
   </form>

ちなみにUrl.Contentが生成するURLも元に戻った。 なんかへんてこな相対アドレスを生成してくれてたし。

あと、Route名を指定してURL生成するヘルパー群で生成されるURLが空になってしまうって言うバグがあったみたい。そこまで確認とれてなくて残念。 とりあえず、RCはRefreshを使っときましょう。

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年1月11日日曜日

xVal

まだバージョンも0.5だし、これからなプロジェクトなんだろうけど、これはちょっと目が離せないかも。

ASP.NET MVCでサーバーサイドのバリデーション(入力検証)を行うとき、DynamicDataで導入されたDataAnnotationsを使うのが、現時点ではベストな選択なんじゃないかと思うんだけど、いかんせんサーバーサイドでの属性ベースのテクノロジなので、クライアントサイドでの検証は別途実装しなきゃなところ。 クライアントサイドでの検証があるだけで、入力をいちいちサーバーにポストしなくても何がエラーなのか、入力と同時に分かるから便利だけど、その為に同じルールを何度もコーディングするのは面倒だよね。面倒だけど仕方なく実装する感じ。

そこを橋渡しするのがこのxVal。

xVal – Home

DataAnnotationsで属性に指定した検証ルールをJson形式に変換して、クライアントに書き出すことで、ルール定義は1箇所で済ませようという、楽したいがタメに生まれたナイスなプロジェクト。 サーバーサイドの属性の抽出用プロバイダとして、System.ComponentModel.DataAnnotationsだけじゃなく、Castle.Components.Validatorも実装。

クライアントサイドの検証にはjQuery.validateの他にも、ASP.NET標準の検証コントロールを使った実装もある(けど、こっちはあんまり興味なし)。xVal.ClientSidePluginsに入ってるこの2つのクライアントサイドの実装は、たぶんこれ以外にも例えば prototype.js版とか作れるってことだよね。AllPossibleRulesを全部書けば...。誰か作ってくれるんじゃないかな~。作ってくれないかな~。

xVal - a validation framework for ASP.NET MVC « Steve Sanderson’s blog ↑ここで、どんな物なのか書かれてる。

サンプルプロジェクトもあるので、実際に動かしてみるのが分かりやすいね。 って、ことで、早速ダウンロード。

BookingsDemoにはxValのアセンブリは含まれてるけど、ソースは含まれてないのでソースはCodePlexから。

xVal - Source Code

なるほど~。すばらしいくらいリフレクション。 DataTypeRule/RangeRule/RegularExpressionRule/RequiredRule/StringLengthRuleの5つのルールに属性を変換するんだね。

サンプルでレンダリングされるJsonは↓。

<script type="text/javascript">
 xVal.AttachValidator("booking",
   {"Fields":[
       {"FieldName":"ClientName","FieldRules":[
           {"RuleName":"StringLength","RuleParameters":{"MaxLength":"15"}},
           {"RuleName":"Required","RuleParameters":{}}]
       },
       {"FieldName":"NumberOfGuests","FieldRules":[
           {"RuleName":"Range","RuleParameters":{"Min":"1","Max":"20","Type":"decimal"}},
           {"RuleName":"DataType","RuleParameters":{"Type":"Integer"}}]
       },
       {"FieldName":"ArrivalDate","FieldRules":[
           {"RuleName":"DataType","RuleParameters":{"Type":"Date"}},
           {"RuleName":"Required","RuleParameters":{}}]
       }]
   })
</script>

見ての通り、入力値の単純な検証は変換するけど、ビジネスルールはサーバーサイドのみで実行。 それが、どこにあるのか探してみると、BookingManager.PlaceBooking。 その中で、属性ベースの検証の実行と、ビジネスルールの検証の両方を実装。

アクションはどうやってるのか見てみると

        [AcceptVerbs(HttpVerbs.Post)]
       public ActionResult CreateBooking(Booking booking)
       {
           try {
               BookingManager.PlaceBooking(booking);              
           }
           catch(RulesException ex) {
               ex.AddModelStateErrors(ModelState, "booking");
           }

           return ModelState.IsValid ? RedirectToAction("Completed")
                                     : (ActionResult) View();
       } 

RuleException が発生(PlaceBooking内でエラーがあったらスロー)したら、ModelStateにエラーを入れる。これで、JavaScriptオフでも入力検証はきちんと実行されるて、エラーフィールドにはinput-validation-error、エラーメッセージも表示(<%= Html.ValidationMessage(モデル名)%>)されるっていうすばらしさ(ModelState.AddModelErrorでエラーを入れれば、Htmlヘルパー経由の場合、ちゃんと反映されるっていうASP.NET MVCの設計がこういうとき威力を発揮するね)。 RCがなかなかリリースされないけど、この辺見てるだけでも楽しいかも~。 ※RCでは動かなかったりするかもしれないけどね。

2009年1月2日金曜日

ちょっとしたTips

ASP.NET MVCの情報源として外せないStackOverflowで↓こんな質問があがってたんです。

Compile Views in ASP.NET MVC - Stack Overflow

ViewPageのコンパイルって先送りされるじゃないですか。 んで、ページを表示させたときに、エラーが出て「あ、そっか」って気付くことも多いと思うんですよね。 対応として、プロジェクトファイルに以下の設定を書いておけばいいって、ことなんだけど面倒じゃないですか。

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <Target Name="PrecompileWeb">
       <AspNetCompiler
           VirtualPath="/MyWebSite"
           PhysicalPath="c:\inetpub\wwwroot\MyWebSite\"
           TargetPath="c:\precompiledweb\MyWebSite\"
           Force="true"
           Debug="true"
       />
   </Target>
</Project>

もっと簡単にできる方法があってですね。 Code rant: Compiling aspx templates using aspnet_compiler

1.ウソコード書いてみる。

img.aspx

ViewData[a+b]なんて書いてるけど、そんな変数宣言もしてないし。 でも、ビルドしてもステータスバーみて分かるとおり「ビルド正常終了」する。

2.プロジェクトプロパティで設定する。

「ビルド後に実行するコマンドライン」に以下を書いておく。 C:\Windows\Microsoft.NET\Framework\v2.0.50727\aspnet_compiler.exe -v / -p "$(ProjectDir)\" ※環境によっては違うよ~。

img.aspx2

3.もう一度ビルドする。

img.aspx3

ちゃんと「ビルド失敗」するよ~。 ViewPage多いとちょっと時間かかるけど、ページみていきなりビックリしたり、ページ見るまでエラーに気がつかないなんてことが無くて、安心だね!

ちなみに、ASP.NET MVCの次のリリース(RC)では「ASP.NET MVC RC版では、プロジェクトのコンパイル・プロセスの一部としてビューを含めたい場合に使用できるビルトインのMSBuildタスクがあります。これにより、アプリケーションにあるすべてのビューやマスターページに含まれる構文やコードが検証され、もし問題があればビルド・エラーが発生します。」(ASP.NET MVCデザイン・ギャラリーとASP.NET MVC RC版におけるビューの改善 - @IT)ってことで、こんな事しなくてもいいみたいだけどね。