2009年3月8日日曜日
第2戦(Brass Division)
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も)を配布すればよくなりました。グッジョブ!
2009年3月2日月曜日
ASP.NET MVCの新しいサンプル&チュートリアル
ASP.NET MVC Sample Application at www.ASP.net/MVC
ASP.NET MVCでの新しいサンプル。チュートリアルにそって順に良くなっていく。
これはMVCerはみとくべき。
イテレ-ション1 Entity Frameworkをモデルにして、アドレス帳サンプルを作成。この手順はオーソドックスな最初の作り方。これでMVCでの作成ってこれまでとどう違うのかが分かるはず。
イテレーション2 デザインギャラリーからデザインテンプレートを取得したり、自分でデザインしたものを1で作ったものに適用させて、見た目を綺麗にしましょう。ヘルパーなんかも書いたりしてデザインしやすくしておきましょう。
イテレ-ション3 フォームの入力検証を実装。シンプルな検証方法で、DataAnnotationsなどを使うわけではないです。どこで検証を実施すればいいのか、検証結果がどうViewに反映されるのかをしっかり押さえておきます。
イテレーション4 機能を疎結合にしましょう。モデル層はRepositoryに任せ、ロジック層はServiceに移動させる。それぞれインターフェイスと実装を分けて書きましょう。
イテレーション5 ユニットテスト。これまで書いたコードにたいするテストコードを書きましょう。モックを使うとテストコードを楽にかけるのでお気に入りのモックを使いましょう。ここではMoqを使います。他にもRhino MocksやTypeMockなんかもあるよ。テストの初期化時にインターフェースのインスタンスを生成し、コンストラクタインジェクション。 Service層だけじゃなく、コントローラもテストコードを書きましょう。
イテレーション6 5でテストコードを書いたけど、ここからはテストドリブンな開発にしましょう。Group機能を追加する様子をテストを書くところから始めます。その後でテストをパスするコントローラコードを書き、繰り返して機能を実装していく。一通り出来たら、テストコードもあることだし、リファクタリングしてコードをService層に移動したり、Repository層に移動して、テストをパスしないコードが発生しないようにしましょう。
イテレーション7 最後にアプリケーションをAjax化して、使いやすさも向上させましょう。ページの部分更新をしたりするので、共通部分はコントロール化して、Ajaxでの更新時にPartialViewでページ全体じゃなく、必要な部分だけを返すようなコードにしましょう。
テーブルも2個しかないし、規模としては凄く小さいけど、これがASP.NET MVCの開発スタイルの基本になると思います。しっかり身につけて、テストもはしょったりしないようにしましょう(自分で言っときながら耳が痛い...)。
カスタムViewEngineを試す
ASP.NET MVCに限った話ではないんですが、JavaScriptをページで使う場合、外部ファイル(scriptタグのsrc属性で指定する)にするか、インラインでページに直接書く(scriptタグ内にコードを書く)かどっちかになる。
今時の作り方ならJavaScriptはページの最後で外部ファイルを取り込むのが、パフォーマンス的にもよろし、ということになってますね(ハイパフォーマンスWebサイト)。
ASP.NET MVCでも、もちろん外部JSファイルを使ったアプリケーションを作るわけですが、ここで少し悩みが出てきました。
処理コード(静的)としてのJavaScriptは簡単に外部に出せるから問題にはならないんですが、サーバーサイドで生成したデータを元に処理するコードの場合、そのデータをどうやって外部ファイルで使えばいいでしょうか。分かりにくい説明ですが、例えばサーバー上でデータベースから名前一覧を取得してViewDataに入れておいたとします(こればっかりは動的)。そのViewDataを外部JSファイルではもちろんそのまま参照できません。と、いうのもscriptタグで取り込んだ外部JSファイルとページそのもののリクエスト(MvcHandler)は別物だから、いくらコントローラのアクションでViewDataに値を入れたとしても外部JSファイルを取得したリクエスト(StaticFileHandler)では参照できないというのと、そもそもJSファイルが<%=~%>を解析して処理してくれないから。あたりまえですね~。
じゃぁ、どうすればデータ(サーバーサイドで動的生成)をコード(JavaScriptを静的に取得)に簡単に渡せるんだろう。
ページ専用のJavaScriptコードはどうしても、アクションで生成されるデータを使った動的コードにしたくなってしまう。でも、それだと外部ファイルじゃなくViewにscriptタグを書いてしまうことになって、なんかスッキリしない。
- 外部JSファイルを拡張子JSじゃなくASPXで作成し、JavaScript用のコントローラを作成(レスポンスのコンテンツタイプをapplication/x-javascriptに変更)し、動的にJSコードを生成するようにして(ViewファイルにJavaScriptコードを書く)、scriptタグでsrc指定。
- 外部JSファイルにはコードだけを書き、Viewに出力されるHTMLにscriptタグを書いておいて適当な変数にデータ(JSON)で入れておいて、外部JSファイルからはこれを参照する。
1の方法をとる場合、凄く分かりにくくなるのがコントローラが違うからViewとJavaScriptそれぞれでデータ取得のロジック(もちろんJavaScript用のコントローラではクライアントで必要なデータのみですが)を書かなきゃいけないからコードが散らばるうえに、VSで開発してるにも関わらずコードハイライトもインテリセンスも効かなくなる。
例えば...
// CharaControllerのViewアクション public ActionResult Character() { return View(); } // JsControllerのJavaScriptアクション public ActionResult Character() { ViewData["chara"] = ToJson(new {firstName="ルフィ",lastName="モンキー"}); return View(); } // ビュー <body> <!-- viewの定義 --> </body> <script type="text/javascript" src="JsController/Character"></script> // Js/Character.aspx(JavaScript) var chara = <% = ViewData["chara"] %>; // 以降charaを使った処理これだと2つのコントローラが必要になるし、ViewDataの生成と利用が離れすぎ。
綺麗にコードとデータを分離するなら、2の方法が正解ですよね。その場合Viewページにscriptタグを書く必要がありますが、あくまで処理コードは外部に分離できる。
例えば...
// CharaControllerのViewアクション public ActionResult Character() { ViewData["chara"] = ToJson(new {firstName="ルフィ",lastName="モンキー"}); return View(); } // ビュー <body> <!-- viewの定義 --> </body> <script type="text/javascript"> var viewData = <% = ViewData["chara"] %>; </script> <script type="text/javascript" src="chara.js"></script> // chara.js var chara = viewData; // 以降charaを使った処理
Viewアクション内でJavaScriptで利用するデータを生成しておき、外部スクリプトでのデータ参照はグローバル(この場合ならviewData)を見る。
どうも1の方法に固執しすぎてて、これを解決するためにカスタムのViewEngineを作ればいいんじゃないの?というおかしな路線に走ってしまって...。結局は2の方法にすることでViewEngine作る必要は無かったことに気がついたんだけど、その過程で今ネットで見つかるViewEngineの作り方が少し古いやり方な事に気がついたので、無駄にしないために、ココにメモとして残しておきます。
ASP.NET MVC Tip #25 – Unit Test Your Views without a Web Server Maarten Balliauw {blog} - Creating a custom ViewEngine for the ASP.NET MVC framework SingingEels : Creating a Custom View Engine in ASP.NET MVC Brad Wilson: Partial Rendering View Engines in ASP.NET MVC
ViewEngineを作るといっても、単にViewパスの検索場所を変更するためだけの目的もあれば、テンプレートエンジンを置き換えてしまう目的もあると思います。
パスを変えるだけならIViewEngineの実装はせず、PhilさんのAreasデモソース(Grouping Controllers with ASP.NET MVC)のようにWebFormViewEngineを派生させてViewLocationFormatsとMasterLocationFormatsをセットして、IViewは標準のWebFormViewを使えばいいですね。
テンプレートエンジンを変えてしまいたい場合は、IViewEngineとIViewそれぞれを実装することになりますが、今回は少し楽をしてIViewEngineにはVirtualPathProviderViewEngineを使うことにします。
何を作るかというと、拡張子jsの中に/$Key$/という形でテンプレートを入れとくと、ViewDataCollection内の同名Key値を埋め込むというテンプレート。これならJavaScriptのインテリセンスもコードハイライトも有効。
IViewEngineの実装。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Web.Mvc; using System.Web.Hosting; namespace Sample.Libraries { public class JavaScriptViewEngine : VirtualPathProviderViewEngine { public JavaScriptViewEngine() { MasterLocationFormats = new string[0]; ViewLocationFormats = new[]{ "~/ViewScripts/{0}.js", "~/ViewScripts/Shared/{0}.js" }; PartialViewLocationFormats = ViewLocationFormats; } protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath) { return CreateView(controllerContext, partialPath, null); } protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath) { return new JavaScriptView(viewPath); } } }
IViewの実装。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Web.Mvc; using System.IO; using System.Web; using System.Text.RegularExpressions; namespace Sample.Libraries { public class JavaScriptView : IView { private string _templatePath; public JavaScriptView(string templatePath) { _templatePath = templatePath; } public void Render(ViewContext viewContext, System.IO.TextWriter writer) { var appPath = viewContext.HttpContext.Request.PhysicalApplicationPath; var filePath = VirtualPathUtility.ToAbsolute(_templatePath).Substring(1).Replace("/", "\\"); var fullPath = Path.Combine(appPath, filePath); if (!File.Exists(fullPath)) throw new InvalidOperationException("not exits javascript template file."); var template = File.ReadAllText(fullPath); writer.Write(Parse(template, viewContext.ViewData)); } public string Parse(string contents, ViewDataDictionary viewData) { return Regex.Replace(contents, @"\$\/(.+)\/\$", m => GetMatch(m, viewData)); } protected virtual string GetMatch(Match m, ViewDataDictionary viewData) { if (m.Success) { string key = m.Result("$1"); if (viewData.ContainsKey(key)) return viewData[key].ToString(); } return String.Empty; } } }
置換部分の処理はまるっきりStephenさんのコードです...。
拡張子jsのファイルはViewScriptsフォルダに入れておくようにしたものです。コントローラ名もフォマットに含めようと思ったんですが、そこはルーティングの登録を以下のようにしておくことでとりあえず必要無いな、と。でも、ViewScriptsフォルダ内はコントローラ名フォルダ/スクリプト名.jsでファイルを入れておきます。
routes.MapRoute( "ViewScripts", "ViewScripts/{*path}", new { controller = "ViewScripts", action = "Index" } );
ViewEngineの登録も忘れずに。
ViewEngines.Engines.Add(new JavaScriptViewEngine());
あとはViewScriptsControllerを書くだけ。
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using System.Web.Mvc.Ajax; namespace Sample.Controllers { public class ViewScriptsController : Controller { // // GET: /ViewScripts/ public ActionResult Index(string path) { ViewData["test"] = "{data:'sample'}"; return View(path); } } }
まずは動くコードを、という簡単なサンプルです。
テスト用にViewScripts/Test.jsを作成。
// test.js var viewData = /$Test$/; alert(viewData);
あとは、通常のViewでこれをインクルード。
<script type="text/javascript" src="/ViewScripts/Test.js"></script>
これで一応動くものが出来たわけですが、ここで2の方法でいいじゃん、と思い直してコードを破棄...。しかもこの方法だとViewScriptsControllerでいろんなViewDataを入れるためのコードが必要になって、面倒なことに。もっと早い段階で気がつけば良かったけど、カスタムViewEngineを書いてみる勉強になったから良しとします。
ちなみにカスタムViewEngineで最高のサンプルは今書いたこんな中途半端なコードじゃなくてStringTemplate Template Engineじゃないかと思われます。作ってみたい方は是非そちらを参照してみてください。
2009年3月1日日曜日
Crystalized Intelligence
2009年2月28日土曜日
Base64でエンコード
Url encoded slash in URL - Stack Overflow
この投稿から始まって、以下のエントリにたどり着く。
Allowing special characters (forward slash, hash, asterisk etc) in ASP.Net MVC URL parameters
まさに、同じ問題に直面した内容。
Double/incomplete Parameter Url Encoding - Stack Overflow
↑ここへのリンクもあったから覗いてみたら、Uri.EscapeDataString メソッド (System)を使ったエンコーディングがあるということを初めて知る。なにこれ~、と思って試したけど、Server.UrlEncodeと同じだった。ガッカリ。いや、違う方が驚きか。
結局、Philさんの投稿のように"/"か他の文字に置換して区切ってしまうか、Base64エンコードで渡すのがいいのかな。Base64といえばFuturesに入ってるLinqBinaryModelBinderが使えるんだもんね。
というわけで試してみた。
登録するルートは以下の通り。
routes.MapRoute(null, "Proxy/{*base64url}", new { controller = "Images", action = "Proxy" } );
Controllerはシンプルに以下。LinqBinaryModelBinderを使うので、参照設定にFuturesのアセンブリを含めるのを忘れずに(単純に文字列で渡しておいて、Binary型にしなくてもConvert.FromBase64Stringでも可)。
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using System.Web.Mvc.Ajax; using System.Net; using System.Text; using System.Data.Linq; namespace Mvc.RC.Controllers { public class ImagesController : Controller { public ActionResult Proxy(Binary base64url) { var web = new WebClient(); var url = Encoding.UTF8.GetString(base64url.ToArray()); var bytes = web.DownloadData(url); return File(bytes, web.ResponseHeaders["Content-Type"]); } } }Viewでリンクを作る時にBase64エンコードしたパラメータを渡す。
<% = Html.ActionLink("いぬ", "Proxy", "Images", new { base64url = Convert.ToBase64String(Encoding.UTF8.GetBytes( @"http://farm1.static.flickr.com/131/353753310_1ed04f694c_m.jpg" )) }, null)%>
これが出力されると以下のHTML。
<a href="/Proxy/aHR0cDovL2Zhcm0xLnN0YXRpYy5mbGlja3IuY29tLzEzMS8zNTM3NTMzMTBfMWVkMDRmNjk0Y19tLmpwZw==">いぬ</a>
リンクをクリックする。
ちゃんと犬が表示されました。一応、ブレークポイントセットして変数の中身を確認。ちょと見にくいけど、引数のbase64UrlにはBase64エンコードされたBinary型の値が入ってて、変数urlには元のURLがデコードされてる。
問題はぱっと見どこのURLを参照してるのかを判断出来ないところ。フレンドリURLとは言い難し。
ところで、Windows Live Writerから投稿すると、毎回無駄な改行が入るのはなんでなんだろう。
2009年2月26日木曜日
ModelBinderでLINQ to SQLのモデルをそのまま使う
ASP.NET MVC Tip #49 - Use the LinqBinaryModelBinder in your Edit Actions
ステファン君それは無理じゃない?更新以前にバインドした時点で例外でるじゃん。
と、思ってたのは今は昔。RCでは何の問題もなくバインド出来るみたい。試してみたら、普通に出来た。ずいぶん前に試したときはデータベースコンテキストがスタティックじゃないと例外でたり(ModelBinderに気をつけねば)、そもそも直接DBO使うのはうんぬんかんぬん。綺麗な設計どうのじゃなくて、純粋に少ないコード量でどこまで出来るか、っていうのを考えたら直接使う事もあったりするのかもね。
これまでも使ってたサンプルはDBを使ってなかったので、改めてDBを用意して、LINQ to SQLのモデルを作成。
これを利用するためのコントローラも定義。
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 OnePeaceController : Controller { OnePeaceDataContext _context = null; public OnePeaceController() { _context = new OnePeaceDataContext(); } public ActionResult Index() { return List(); } public ActionResult List() { var people = _context.Persons; return View("List", people); } public ActionResult Details(int id) { var people = _context.Persons; return View(people.First(p => p.Id == id)); } [AcceptVerbs(HttpVerbs.Get)] public ActionResult Edit(int id) { var people = _context.Persons; return View(people.First(p => p.Id == id)); } [AcceptVerbs(HttpVerbs.Post), ValidateInput(false)] public ActionResult Edit(int id, Person person) { try { _context.Persons.Attach(person, true); _context.SubmitChanges(); return RedirectToAction("Index"); } catch (Exception ex) { return View(person); } } } }
List/Detail/Editの各ViewはRCで追加されたAdd Viewコマンドでサラッと作成。こういう時にとても便利ですね。Attachのところでブレークポイントをセットして、まずはこのまま動かしてみたところ、DB使っててもホントに動く。信じてなかったわけじゃないけど、こうもあっさりと動くとちょっと感動する。
↑Listページ。
↑Editページ。
↑普通にバインドされてる様子。
でも、このままステップ実行すると、確かに例外が発生。
エンティティは、バージョン メンバを宣言するか、または更新チェック ポリシーを 含まない場合にのみ、元の状態なしに変更したものをアタッチできます。
System.Exception {System.InvalidOperationException}
と、言うことで、簡単に更新チェックポリシーをオフにしてしまおうかとも思ったけど、それよりちゃんと競合チェックするようにtimestamp型の列をテーブルに追加して動かしてみます。
↑ChangeStampという名前のtimestamp型の列を追加したモデル。
でも、timestamp型はSystem.Data.Linq.Binary型としてクラスが生成されるので、このままだとちゃんとモデルが復元されません。なによりViewにちゃんと出力してないし。まずはEditのViewにChangeStampをhiddenで展開するコードを追加。
<p> <%= Html.Hidden("ChangeStamp", Convert.ToBase64String(Model.ChangeStamp.ToArray())) %> <input type="submit" value="Save" /> </p> </fieldset> <% } %> <div> <%=Html.ActionLink("Back to List", "Index") %> </div> </asp:Content>
submitの手前に入れてます。Base64エンコードしてHiddenフィールドに。このままだとDefaultModelBinderが復元してくれくれないので、Global.asaxのApplication_Start時にASP.NET MVC Futuresに入ってるLinqBinaryModelBinderを登録。
protected void Application_Start() { RegisterRoutes(RouteTable.Routes); ModelBinders.Binders.Add(typeof(System.Data.Linq.Binary), new LinqBinaryModelBinder()); }
もう、何もかもステファンさんのいいなりです。
この状態で動かしてみて、Viewが出力するHTMLソースのChangeStamp部分を確認してみたのが↓これ。
<input id="ChangeStamp" name="ChangeStamp" type="hidden" value="AAAAAAAAB9Q=" />
ちゃんと、エンコードされて出力されてます。あたりまえだっちゅーの。
これを更新するためにsubmitして、ブレークポイントでモデルの中身を確認。ちゃんとLinqBinaryModelBinderでBinary型も復元されてます。そのまま実行を続けても例外は出なくて、テーブルも更新されてました。で、更新された後のViewのCangeStampを確認してみる。
<input id="ChangeStamp" name="ChangeStamp" type="hidden" value="AAAAAAAAB9U=" />
ちゃんと、最初とは違う値が入ってますね~。
と、いうことで、ステファンさんのやったことをそのまま試したみたわけですが、これが出来るって事はLINQ to SQL+スキャッフォールディングで凄く簡単にDBを使ったアプリケーションを作成出来る事になりますね。Repositoryは作るにしても、ViewModelを作らずシンプルなコードで開発することが出来るのでWebForms並の生産性(ViewStateとデータバインディング)を実現できてるんじゃないかと思う次第です。
※ViewStateはModelStateが各inputフィールドの値を保持しつつHTMLにレンダリングしてくれたりするので。
dotnetConf2015 Japan
https://github.com/takepara/MvcVpl ↑こちらにいろいろ置いときました。 参加してくださった方々の温かい対応に感謝感謝です。
-
Working with SSL at Development Time is easier with IISExpress - Scott Hanselman Hanselmanさんのエントリに書かれてる通りデス! IIS ExpressでSSLを有効にしたデバッグだと無効...
-
How to iterate through objects in ViewData via javascript on the page/view? - Stack Overflow この質問の回答としては、ページにscriptタグを書き、その中でJavaScriptを書き...
-
すでにコロケーションサービスを利用したりして、データセンターには自前のサーバーがいて。でも、今のデータセンターの利用を拡張していったり、するのもどうかなー、資産化するのもなー。という時にはパブリッククラウドにVPN接続させて、オンプレミスとのハイブリッド。 ハイブリッドクラウド...