2008年12月31日水曜日

Blogsvc.net

これまたASP.NET MVCをつかったCMS。バージョンが0.8でずいぶん進んでるのに今まで全然知らなかったです。

BlogService – Home

同じジャンル・ターゲットのプロダクトとして最近Oxiteが出てきたけど、OxiteがMetaWeblogAPIなのに対して、こっちはAtomPub API。

機能的にはほぼ互角な感じだけど、動かすと最初にウィザード形式で設定する分、使う人にとってはすこしとっつきやすいかも。

API 実装してるからか、編集機能はWebアプリケーションとして実装しない(してても適当、Oxiteもそうだけど)で、Live Writerとかの外部エディタに任せるスタイル。これはコレでいい割り切りだと思うな。プロバイダサービスみたいに沢山の人にたいして、使ってもらうんじゃなくて会社のサイトや個人のサイトをターゲットプラットフォームとして考えた場合、妥当な選択。編集機能にリソースをさくより、API実装に専念しておけばいいよね。

初期のデータプロバイダがDBじゃなくてファイルベースのXMLだったりする。でも、もちろんLINQ to SQLのプロバイダもあって、テーブル構築用のSQLも用意されてます。でも、コードを追っかけるとRepositoryを直接インスタンス化してる箇所がWebMvcプロジェクト(Webアプリ)にもあるから、ファイルストレージを前提として開発してるんだろうね。開発チーム内で分業されてるからかな?

ファイルストレージで気になるのはファイルフォーマット。どういう形式でファイルに保持してるのかなと思ってみてみたら、Atom形式のXml。全部が全部 Atom。で、Atom関係のモデルクラスはDomainプロジェクトに集約されてて、そのモデルを使うので、DBのモデルを直接使うなんて事はしてないんですな。AtomPub APIをとことん使いこなす設計ですばらしい。

んで、ファイルだからIO負荷が気になるところだけど、そこはシンプルにRepository実装内で、staticなDictionaryを持ってて、そこで保持。ファイルが書き換わったときにDictionary内のデータを破棄するために、ちょっとオシャレな作り方してて参考になるので、ここで引用。

RepositoryプロジェクトのFile/XmlCache.csの最後の所。

        public void CacheItem(string filename, T item)
        {
            if (!objs.ContainsKey(filename)) objs.Add(filename, item);
            //items aren't actually put in http cache, but an object handle is
            HttpRuntime.Cache.Insert(filename, new object(), new CacheDependency(filename),
                Cache.NoAbsoluteExpiration, TimeSpan.FromMinutes(30) /*TODO: make configurable*/, CacheItemPriority.Normal,
                //remove from cache
                new CacheItemRemovedCallback((key, value, reason) =>  objs.Remove(key))
            );
        } 

実体をキャッシュに入れちゃえばいいじゃんと思うけど、なんかあるのかな。CacheDependencyの削除コールバックがラムダ式で書かれてて、コードがあっちこっち行かなくて素敵でしょ? 中でTidy.NETっていう別のオープンソースプロダクトを使ってて、HTMLの整形をしてるのかな? 同じく別のライブラリでSgmlReaderっていうのも使ってる。

それぞれ用途が違うのかな。どっちかだけでいいって物でもないの?整形するのとパースするのとで使い分けてるのかな。

SourceForge.net: Tidy.NET SgmlReader – Home

WebMvc プロジェクトにASP.NET MVCの実装が入ってるけど、テーマをサポートするために(ASP.NETのテーマとは違う)、独自のViewEngineとして ThemeViewEngineを使ってる。といっても、View/MasterPartialViewそれぞれのLocationFormatをセットして、Find系を実装するのみ。

フォルダ構成のなかにViewがなくて、themesっていうのがあるけど、そこにすべてのViewPageが入ってます。で、defaultとの差分のみをそれぞれのテーマフォルダに入れておけば、無いものはdefaultフォルダのものを使うように LocationFormatに登録されてるから問題なし。 ※default以外にはテーマ毎のイメージとCSS、テーマ説明のAtomEntry XMLとサムネイル画像が入ってるくらい。

URIで各ページの拡張子にフォーマット指定でxhtmlってつけるのをルールにしてるのは、RESTfulを意識してるからなのかな。

ソースをダウンロードして、動かそうとするといきなりコンパイルエラーがでるのはご愛敬。

単純に文字列閉じてないだけ。なのでDomainプロジェクトのAtomText.cs 行50の部分を↓変更。 デフォルトのtrailerがなんなのかオンラインでソースを確認しても化けててわかんないので、とりあえずなんか入れとくってことで"..."を使う。 ※trailerは文字列を指定の長さに切り抜いて、元文字列が指定長以上なら後ろにくっつける文字列。

        public string ToStringPreview(int length)
        {
            return ToString().AbbreviateText(length, "...");
        } 

AtomPub APIを公開するのにあわせて、Atom実装のコードがたくさんあるけど、その他はパッと見シンプルな実装な感じ。

ViewPageも型指定(強い型付け)でViewDataを扱ってるし、コードをほとんど含まないように書かれててOxiteよりも綺麗な印象。好みの問題?

ちょっとOxiteのコードは量が多くて、冗長な感じがしなくもないし...。 このBlogsvc.netもCMS+Blogエンジンとして(Oxiteだけじゃなく)十分いけてる気がするね!

2008年12月30日火曜日

Repositoryで実装してみる

とりあえずデータアクセスはLINQ to SQLのみなんだけど、テストしやすくしたいし、DataContextクラスに大量のコードを書くのもいかがな物かと。で、やっぱりRepositoryパターンだよね!ってことで今更ながらコードを書いてみた次第なんです。

データストレージ層とデータアクセス層、サービス層の分離を明確にして、インターフェースベースの実装にしとくことでテストしやすくなるっていうことなんだけど、さらにデータストレージ層を置き換えも楽ちんとかいうのは、まぁ実際そんなこと滅多にするもんじゃないから普通のプロダクトにおいてそこがメリットになることはほぼなかったり...?

データモデルクラスをLINQ to SQL(L2S)のモデルの他に、純粋なデータクラスとして別途定義するのはコード量が増えるし、ちょっとイヤだな~、と思うところでそこはLINQ to SQLのモデルクラスをそのまま使うってことで勘弁してもらいます。クラスの変換とか余計な処理も必要になるしな~、と。

でね、IRepositoryでインターフェース定義して、その実装をLINQ to SQLを使って実装するんだけど、SubmitChangesってどこに書くのがいいんだか、ちょっと悩ましいところだったりしまして。 きちんとデータモデルを分けて定義しておけば、サービス層からデータアクセス層にデータクラスを渡して、その中でL2Sモデルに値をマッピングするのが王道パターンだとしても、それをやりたくない場合、サービス層でL2Sモデルに直接値を入れたりすることになるじゃないですか。わかりにくい説明でなんだかわかんなくなるな。

テーブルPeopleがあって、L2Sモデルクラスとして

public class Person
{
  public int Id {get;set;}
  public string FirstName {get;set;}
  public string LastName {get;set;}
}

っていうのがあったとしましょうよ(これがdbmlから自動生成されたとしてください)。 んで、そのDataContextクラスをTestDataContextとしましょう。 これに新しいデータを追加するなら

var db = new TestDataContext();
var person = new Person(){Id=1,FirstName="ロビン",LastName="ニコ"};
db.People.InsertOnSubmit(person);
db.SubmitChanges();

なんて書きますよね。 更新や削除なら

var db = new TestDataContext();

var person = db.People.FirstOrDefault(p=>p.Id=1);
if (person != null) {
  person.FirstName = "オルビア";
  db.SubmitChanges();  // ←これで更新確定
}
db.People.DeleteOnSubmit(person);
db.SubmitChanges();  // ←これで削除確定 

みたいな。 追加、更新、削除を持ったRepositoryにするなら

public interface IPeopleRepository {
  void Add(Person model);
  void Update();
  void Delete(Person model);
} 

とか、って書きたくなるけど、Updateは実質SubmitChangesだけじゃん!みたいな。 この例だと引数は最大3個でいいかもしれないけど、クラス次第でスゴイ引数の数になっちゃうから、もう一つ上で直接モデルに値を入れるほうがコード少なくて済むって話しです。 普通にRepositoryの関数内で都度SubmitChangesを呼び出すのが正しい実装、だと思う。

ASP.NET MVC Storefront Part 10: Shopping Cart Refactor and Authorization : The Official Microsoft ASP.NET Site

↑ここでもそういう書き方にリファクタリングしてるし。

そうすると、複数の更新をまとめてSbumitChangesしたい、なんてのはダメにするか、そういう関数をRepositoryに追加するってことになる。どっちもなんかヤダ。 んじゃ、データ操作の確定のためのSubmitChangesもサービス層で呼べばいいのかっていうと、単純にDataContextを上の層で持ちたくない(db.SubmitChanges()なんてコードをサービス層で書きたくない)。 結局、単純にSubmitChangesと同名のメソッドをRepositoryに実装して、それをサービス層で呼び出せばいいのかな?と。そしたらDataContextをサービス層が弄る必要ないし。

var repository = new PeopleRepository();
var p1 = new Person(){...};
var p2 = new Person(){...};
var p3 = new Person(){...};
repository.Add(p1);
repository.Add(p2);
repository.Add(p3);
repository.SubmitChanges(); 

↑こんな感じ?(コレクションにしてない&InsertAllSubmitにしてないっていうのは気にしないでね) で、ふと思い出した。Fluent Programming(Fluent interface - Wikipedia, the free encyclopedia)。 データ操作の戻り値として、IRepositoryを返せば(この場合ならIPeopleRepository)つなげられるな~、と。

public interface IPeopleRepository {
  IPeopleRepository Add(Person model);
  IPeopleRepository Delete(Person model);
  void SubmitChanges();
}

↑インターフェースはこう替える。 んで、実装は済んでるとして、呼び出し側では↓こう書く。

var repository = new PeopleRepository();
var p1 = new Person(){...};
var p2 = new Person(){...};
var p3 = new Person(){...};

repository.Add(p1);
         .Add(p2);
         .Add(p3);
         .SubmitChanges();

これなら間に、Deleteとかも連結出来るもんね。

他のRepositoryをどうするんだよ、って言うのは、まぁ...、そうなんだけど...。 とりあえず、同じRepositoryの場合はコレでいいですかね(結局L2Sモデルクラスをサービス層で使うんなら分離出来てないじゃないか、っていうのはその通りなんだけど...)。

2008年12月22日月曜日

Oxiteのソリューション構成

ちょいちょいソースを見ていこうと思うところで、まずはソリューションの構成を確認して、どんなコードがどのプロジェクトに含まれてるのかを把握しとこうかなと。


ソリューションを開いたところで、表示されるプロジェクトは↓。

img.aspx

それぞれのプロジェクトに何が含まれてるのかを見てみたよ。

1.Oxite


このプロジェクトにOxiteで共通のライブラリコードや、インターフェースが集中。
BackgroundServicesにBackgroundServicesで実装する各種バックグラウンドタスクのベースクラスや、インターフェース。
Configurationにはweb.configのカスタムエレメント用のクラス。クラス自体は大量にあるけど、内容は各設定値を取得するための物だからエレメントの数だけクラスがある感じ。
Dataにはデータベースのモデルクラスのインターフェース。実装はLinqToSqlProviderに入ってる。
HandlersにMetaWeblogAPIとXmlRPCのクラス。コントローラクラスだけじゃなくてこのハンドラもルーティング登録されてるからコントローラを探しても見つからず。
Routingにはそのままルーティングを登録するクラス。ルート登録の詳細がMVCのWebアプリケーションのglobal.asaxじゃなくて、ここにすべてまとめて入ってる。
Searchに検索機能のインターフェース。
あと、ルートに共通関数が入ってる。
で、 StringExtensionsクラスにStringクラスの拡張メソッドが入ってるんだけど、なんとこの中でAntiXssLibraryをラップしてる関数有り。AntiXss.HtmlEncodeとAntiXss.HtmlAttributeEncodeを使って、エンコードするようにしてる。さすが抜け目なし。ただバージョンは先日公開された3.0じゃなくて1.5を使ってる。ちょっと古いのを使ってるのが気になるところだけどリリースの関係でOxiteの先にリリースしてるし、MIXのサイトでも使ってるって事で、そこはやむなしな感じで。

2.Oxite.BackgroundServices

バックグラウンドで動作する機能の実装。
何個かクラスがあるけど、ようはメール送信(SMTP送信)とトラックバック送信(HTTP POST)の2つを処理してる(と思う)。
CreateMessagesFromSubscriptions(BackgroundService)

コメントをメールメッセージとしてMessageRepositoryに登録。

SendMessages(BackgroundService)
MessageRepositoryに登録されてるメッセージをメール送信。

CreateTrackbacks(PostBackgroundServiceAction)
トラックバック要求をBackgroundServiceActionRepositoryに登録。

SendTrackbacks(PostBackgroundServiceAction)
BackgroundServiceActionRepositoryに登録されてる要求を実行。

3.Oxite.LinqToSqlProvider


QxiteプロジェクトのData内に定義されてる、リポジトリの実装。
モデル自体はLINQ to SQL(dbml)で定義されてるのをそのまま。
あとIQueryable<T>の拡張メソッドでページングデータ(PageOfAList<T>)を取得するものや、モデルクラスの拡張機能など。
MembershipRepositoryがoxite_User/oxite_Roleなんかを見るような独自実装なのがちょっと以外。これはGravator対応とかが関係してるんだろうか?関係なさそうだけどなんでASP.NETのMembershipじゃダメなんだろうね~。

4.Oxite.LiveSearchProvider


サイト検索をWindows Live Searchで実行するための実装。
http://soap.search.live.com/webservices.asmx?wsdlへのWeb参照(SOAP)で検索要求を送信して、結果を取得するようになってる~。

5.Oxite.SearchProvider


これも検索用の実装なんだけど、こっちはLINQ to SQLで実行。
Post.SearchPost(TitleとBodyをくっつけたもの)に対してContainsで単純に検索してる。
Like検索だね。

6.Oxite.MVC


このプロジェクトに全部のコントローラクラスが入ってる。
OxiteApplication クラスがHttpApplicationを派生したクラスとして、定義されててこの中のOnStartでバックグラウンドサービスの開始や、ルーティング登録の実行。BeginRequestでリダイレクトがどうのこうのって処理があるけど、なんのタメの機能なのかよくわかんないかも。
ControllerもBaseControllerっていうクラスをベースにして実装。
ルーティングとコントローラのクラス名を見比べると、URLの設計はそれはそれだろ的な思いもちらほらと。


AccountController(Users/{userName}/~,SignIn,SignOut)
AdminController(Admin/~)
ArchiveController(Archive/{*archiveData})
AreaController({areaName}/~)
FileController
HomeController
PageController({*pagePath})
PingbackController({id}/Pingback)
SearchController(OpenSearch.xml,Search/~)
SEOController(Robots.txt,SiteMap/~)
TagController(Tags/{tagName}/~)
TrackbackController({id}/Trackback)


Viewで使うViewPageクラスの代わりにBaseViewPage/BaseViewUserControlなんかを定義。
OxiteSite(Webアプリケーション)で使う共通関数なんかもここで定義。Oxiteでも共通関数があったりするけど、Webアプリケーションでしか使わないのはたぶんここに集中してるんだろね。
ヘルパーの拡張なんかもここ。
FeedResultっていうのがなぜかViewResultから派生させて、Viewとして定義してるのはちょっとダサイ気がしなくもない。
Trackback/Pingbackを属性クラスで定義して、ViewDataにディスカバリー情報を入れるっていうのはカッコイイ。

7.OxiteSite


これがサイトの実体プロジェクト。
でも、実質コントローラもロジックも別のプロジェクトにごっそり入ってて、ここではもっぱらViewに関する物だけがある。
なので、Global.asaxのコードビハインドではOxiteApplicationクラスから派生。
ほとんどのViewにAtom/Rssそれぞれのフィード専用Viewが入ってて冗長な気が...。もう少し綺麗にできるんじゃないのかな~。ダメなのかな~。
特徴的なのはViewDataがすべてDictionaryのままで、View内キャストするようになってるとこ。
ViewPage<T>なんて定義はなく、すべてキャスト。しかもViewに結構なコードが含まれてる気がする。
あと、ルートにLiveWriterManifest.xml(Windows Live Writer Provider Customization API) があって、MetaWeblogAPIに関する設定が書かれてた。こんなファイルがあるのを初めて知りました。


とりあえず、含まれてるプロジェクトはこんな感じってことで。
中身・機能についてもちょいちょい見ていきます。

2008年12月19日金曜日

RCなんですね

ASP.NET MVC Design Gallery and Upcoming View Improvements with the ASP.NET MVC Release Candidate - ScottGu's Blog


デザインギャラリーについての部分も面白そうだけど、それより何よりASP.NET MVCの次のリリースが1月にRCを出すってところ。
IDataErrorInfoっていうのが、どういうインターフェースなのか気になって年越しが落ち着かないじゃないですか!

View生成の時にScaffold出来るとかは、正直あんまり興味なかったりするけど、管理機能を作るのがどれだけ簡単かが、重要なエンタープライズ(社内システム)開発には有効なんだろうね。
でも、Viewのコードビハインドが全く生成されなくなるっていうのはすばらしいかも。

確かに@PageディレクティブのInheritsに指定するならViewDataの型指定のタメだけのコードビハインドファイルはいらない(Strongly-Typed ViewData Without A Codebehind - Tim Barcz)。
ひょっとして、そうすることで、コンパイルしなくてもViewData.Modelにアクセス出来るようになったりするんだろうか。

試してみたら、今のバージョン(ベータ)でも上記サイトのようにInherits="System.Web.Mvc.ViewPage`1[[モデルクラス名,アセンブリ名]]"を書いておけば、コンパイル無しでもViewData.Modelにアクセスできるね(`1ってついたクラスしてだったり面倒だけど、ジェネリックバージョンはこっちなんだもんな~)。
ViewData.ModelじゃなくてModelから書けるようにもなるっていう。

んで、すっごく気になるのがJavaScriptResult。
どういう風に使えるんだろ。

とにかくJavaScript内でViewDataを参照したいときとか、単純に外部jsに出来ないから、aspxにしてるけど、それをscriptタグで読み込むような使い方にしちゃうと、コードの色分けもIntelliSenceも効かなくてちょっと切ない。かといって、ページに直接 JavaScript埋め込みたくないし。
全然JavaScriptResult関係ない気がしてきた...。
1月にRCだと、その次はいつになるやらデス。

Oxite

Oxite – Home


いろんなニュースサイトでも取り上げられてるから知ってる人も多いと思います。
このOxite(おくさいと)、ASP.NET MVCで作られてるってことで楽しそうじゃないですか。

Oxite - Lab - MIX Online

↑ここで紹介ビデオが見れます。

何にせよ、どんなものかを知るには動かしてみるのが一番。
ということで、まずはソースのダウンロード
ファイルを解凍してソリューション(Oxite.sln)を開いて見よう。

img.aspx

あれれ~?怒られちゃった。dbprojが開けないってさ。
まぁいいや。そんな人のタメにもう一つソリューションファイル(Oxite.VWDExpress.sln)が入っててそっちを開きましょう。
データベース関連のプロジェクトはたぶん使う事ないし。
今度は問題なく開けたので早速実行。

img.aspx2

あっさり動いたね。
さて、使い方がサッパリ分からない。
とりあえず、ログインしてみるものの(Admin/pa$$w0rdで)「で?」って感じです。

img.aspx3

"Create Post"がブログエリアの投稿で、"Create Page"がページの投稿みたいだけど、画像ファイルをアップロードするインターフェースもWYSIWYGなエディタがあるわけでもない。素っ気ないな~。
MetaWeblog API対応なOxiteにはWindows Live Writeがピッタリなんじゃん?と、Aboutページにも書いてるしで、早速インストール(使ったことないも~ん)。

img.aspx4

んで、アカウントの設定をして、いざ投稿!

img.aspx5

なるほど。

img.aspx6

おぉ~。ちゃんと投稿できた。スラッグ(ナメクジじゃないよ、URLのページ名だよ)に日本語入れてみるなんていう意地悪してみたら、ちゃんとエンコードされてた。やるな。
※"新しい記事"(ブログ)は投稿出来るけど、"新しいページ"が投稿出来ないのは何でなんすか?

つか、この投稿者の写真の人誰だよ...。替えたいけど替え方が分からない。まぁちょいちょいソース追いかけようかな。

デザインテンプレートも特に気の利いた物もないし、エディタもテキストエリアだしで、普通に使うのには面倒かもしれないけど、バックグラウンドで処理が走るように作られてたり(パッと見た感じではTimerで起動するみたい)、MetaWeblogAPI対応だったり、プロジェクトが細かく分かれてたりで、気になるところ盛りだくさん。

img.aspx7

小さくし過ぎて見にくいけど、コードメトリックス。
OxiteSiteっていうのがASP.NET MVCのWebサイトなんだけど、コード量が異常に少ないのは、Oxite.MVCにすべてのコントローラが入ってるし、Oxiteと Oxite.LinqToSqlDataProviderにRepositoryとロジックが入ってるから。

MVCのバージョンが変わっても、変更箇所をOxiteSiteだけに押さえてしまおうということかな。
にしても、Viewをちらっと見たけど、ViewDataが型無しのディクショナリだったのがちょっと意外。キャストしまくってるし。

しばらくコードを見たり、動かしたりしながら遊んでみようかな~、なんて。
で、ちょいちょい気になった部分をエントリしちゃったりするのも面白そう。

2008年11月22日土曜日

Windows Azureで遊ぼう

とりあえずWebRoleでWebアプリケーションを作ってみたい。
Workerでごにょごにょするのも楽しそうだけどまずはWeb。
で、もちろんWebFormsじゃなくてASP.NET MVCでしょ!


Azure Services Platform

開発に必要なファイル類をダウンロードします。
Resources - Developer SDKs | Azure Services Platform
↑ここからね。

1.Windows Azure SDK。
2.Windows Azure Tools for Visual Studio

いろいろあるけど上記2つがあれば先に進めるよ。
.NET Services SDK(Account Control/Workflow/Service Busを試すときに使う)とSQL Data Services SDK(SDSのみ試したい時に使う)は使わないからほっとこう。
もちろんVisual Studio 2008とASP.NET MVC ベータ/SQL Server Expressは入れてあるって前提でね。
Azure SDKに開発に必要なAzure仮想環境がローカルに構築されます。なので、これが無いと開発できましぇん。Tools for VSが無いとテンプレートとか入らないし、たぶんファブリックとストレージを起動してくれないんだと思われる。入れてない環境で試さない安全主義!


img.aspx

↑これが仮想実行環境。Development Fabric。

img.aspx2

で、↑これが仮想ストレージ環境。Development Storage。
もちろんこれだけでもAzure上の開発は出来るんだけど、これじゃMVCにならないのでさらに追加でダウンロードするものが一つ。

Cloudy in Seattle : ASP.Net MVC on Windows Azure with Providers

↑ここで説明があるけど、MVCCloudServiceっていうテンプレートプロジェクトをダウンロード。

ASP.Net MVC Windows Azure Cloud Service – Home

で、このソリューションを開発を始めるタメのテンプレートに使います(簡単だね)。
ただし、Azure SDKに入ってるsamplesへのプロジェクトが2つあるので、その二つは手動でソリューションに追加しましょう。

対象になるプロジェクトはAspProviders/StorageClientの2つ。
AspProvidersがあればMembershipやSession、RolesのストレージにAzureのストレージが使えるようになるんだけど、その中でStorageClientを使うので(自分で書くプログラムでもね)、この2つは必須。
LINQ to SQLを使ってSqlServerにアクセスしないで、このStorageClientライブラリを使ってAzure上のStorageにアクセスするからね。LINQは使えるから安心だべ~!!

img.aspx3

img.aspx4

ほら、ちゃんと動く。
試しに"takehara"っていうユーザーを登録してみて、データベースを確認してみる。

img.aspx5

ServiceHostingSDKSampleデータベースのMembershipテーブルにユーザーが入ってるね~。あ、Profileがないな。まぁ、いいか。
AspProvidersがこのデータベースを使ってくれます。
Blob/Table/Queueはここじゃなくて、DevelopmentStorageDbっていう名前のデータベース。
ワクワクしてくるね~。

あ、Azureって何?ってことになる人もいるかもしれないけど、そこんところは他のサイトで色々説明があるから検索してみてね。簡単に言うとMicrosoft版Google App Engineね。簡単じゃね~よ!
で、データを保存する場所というか機能としてBlob/Table/Queueの3種類があるんだけど、このうちQueueはWorkerとの連携に使ったりするような簡易メッセージング機能なので今回はスルー。
BlobっていうのがファイルストレージでTableっていうのがデータベース。でもリレーショナルじゃないからSQL Serverみたいな感じで使えると思うと大間違いな設計になっちゃうので気をつけましょう(だってそうじゃないとスケールアウトも出来ないっしょ)。


Table にはStorageClientからアクセスするんだけど、なぜならRESTで自分でコード書くのが面倒だからだね。Blobにアクセスするときもそうだけど基本RESTで(CRUDはPOST/GET/PUT/DELETE)。SOAPもあるんだっけ?まぁ、いいや。

あと一つ、これはあった方がいいっていうツールがあります。

CodePlex.SpaceBlock – Home

これ、何する物かっていうとAzure Blobへのファイル操作を行う物。

img.aspx6

これがないとどうなるかっていうと...。PowerShell上でのコマンド処理ですから!
出来ない分けじゃないけど、面倒だしね。
ちなみにこのツールはAmazon S3にも使える優れものです。
ここまでで、だいたい準備完了。

これだけあれば開発出来るよ。
1.Azure SDKでローカルに仮想環境構築。
2.Tools for VSでVS統合。
3.MVCCloudServiceでASP.NET MVCテンプレートを準備。
4.SpaceBlockでBlobストレージへのGUIアクセス。

OK?

で、これだけだとAzure上にデプロイできません。ので、Azureを使えるようにアカウントを登録しないといけないのでレッツトライ。つか、最初にこれしとかないと...。
早速AzureサイトにアクセスしてSign inします。が、ここで悲しいお知らせが。

img.aspx7

Invitation Code(Resource Token ID)ていうのがメールで送られてこないと先に進めない...。
ぬかった。

Inviteをもらわないといけなかったのか。
そうだったのか。
と、いうわけで、現時点では実際にデプロイは出来ないってことになりまして...。
どうも、すいませんね。期待させちゃって。てへ。
これをcscfgファイル(AccountSharedKey)や、SpaceBlock(Access Key)に設定しとかないと、Azureにアクセス出来ないから面白くないんで、続きはInvite来たらってことで。

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に変更になってるのも優しさ?)。
いろいろありそうなので、とにかくいじってみます!

2008年9月16日火曜日

Feedの配信

もちろんASP.NET MVCで。

Feed自体はXMLなんで、XMLのレスポンスを返すようなPageを書けばそれで完了! でもそんなのかっこよくないよね。

んじゃどうすんだって、もちろんSyndicationFeedクラスでしょ! WCFじゃないと使えないってことはもちろんなくて、普通に使えるっしょ!

    public ActionResult Feed(string id, string format)
    {
      // データのロード
      var data = (LINQでアイテム抽出).Take(20).ToList();
     
      // 全体かタグのFeed
      var feed = new SyndicationFeed("フィードのタイトル", "", new Uri("サイトのURLとか"));
      var items = new List();
      foreach (var post in data)
      {
        items.Add(new SyndicationItem("アイテムのタイトル", "アイテムのコンテンツ", new Uri("アイテムのURL"), "ユニークなID", 更新日時));
      }
      feed.Items = items;

      SyndicationFeedFormatter formatter = null;
      string contentType;
      if (string.IsNullOrEmpty(format) || format.ToLower() == "atom")
      {
        formatter = new Atom10FeedFormatter(feed);
        contentType = "application/atom+xml";
      }
      else
      {
        formatter = new Rss20FeedFormatter(feed);
        contentType = "application/rss+xml";
      }

      var stream = new StringWriter();
      var xml = new XmlTextWriter(stream);
      formatter.WriteTo(xml);

      return Content("" + stream.ToString(), contentType);

    } 

↑こんなアクションを書く。

※XMLのヘッダが出力されないからって固定で書いて追加してるのはかっこわるい...。

これで、Viewなんて定義しなくてもいいよね!

話変わるけど、いつの間にやらStackoverflow.comが公開されてました。 Hottest "asp.net-mvc" Questions - Stack Overflow

↑とりあえずこのカテゴリだけでも超タメになるっす! Is anyone using the ASP.NET MVC Framework on live sites? - Stack Overflow この質問が切ない。

2008年9月9日火曜日

JavaScriptとCSSをまとめて圧縮する

もちろんASP.NET MVCで作るときの話。

ところでタイトル変だね。全部一緒にまとめて圧縮ってわけじゃないですよ。 JavaScriptもCSSも、外部ファイルにするでしょ。で、外部ファイルにする単位ってやっぱり機能や役割で分割すると思うんですよ。 分割したはいいけど、それらを個別にscriptタグやらlinkタグで読み込むとそれはそれはたくさんのHTTP要求が発生しますよ。

サイトのパフォーマンスをチェックするときに、YSlowって使うでしょ。使いませんか?そうですか。 いやいや、使うんですよ。 で、ハイパフォーマンスWebサイトにも書いてるけど ・HTTP要求の回数を減らしましょう。 ・JavaScriptとCSSは圧縮しましょう。 ・CSSはページの上部、JavaScriptは下部でインクルードしましょう。 ・キャッシュの有効期限設定しましょう。 っていうのが、オーソドックスなパフォーマンスを上げる方法になりますよね。 調べてみたんですよ。今作ってるやつどんなかな、って。 ひどいもんですよ。軒並みF判定ですよ。悲しいよね。 でも、そんなの分かってたことなんだよね。だって、最適化なんてしてないし。そろそろこの辺もちゃんと手をつけようと考えますわね。

ASP.NET MVC Action Filter - Caching and Compression - Kazi Manzur Rashid's Blog

もう、答え出ちゃってる感あるけど、まずはこのサイトを参考に。というか、もうそのまま。Cacheに関してはここのクラスを使わずOutputCacheフィルターを使います。 まずは、テスト用にプロジェクトを作成。 で、スタイルシートのStylesheet1.css、Stylesheet2.css、Stylesheet3.cssと、JavaScriptのJScript1.js、JScript2.js、JScript3.jsをContentの中に作成(中身は適当で)。 で、Helpersフォルダをルートに作って、その中にCompressAttribute.csとCompressHelper.csをそれぞれ“クラス”テンプレートで作成。

img.aspx

↑こんな感じになりましょう。

CompressAttribute.csの中身は先のページのままコピペ。 ヘッダをみて、圧縮ロジックを決めて、Response.Filterにセットするだけ。

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

using System.Web.Mvc;
using System.IO.Compression;

namespace MvcApplication1.P5
{
 /// <summary>
 ///
 /// </summary>
 /// <seealso href="http://weblogs.asp.net/rashid/archive/2008/03/28/asp-net-mvc-action-filter-caching-and-compression.aspx">Original Source by Kazi Manzur Rashid</seealso>
 public class CompressAttribute : ActionFilterAttribute
 {
   /// <summary>
   /// Initializes a new instance of the <see cref="CompressAttribute"/> class.
   /// </summary>
   public CompressAttribute()
   {
   }

   /// <summary>
   /// Called when [action executing].
   /// </summary>
   /// <param name="filterContext">The filter context.</param>
   public override void OnActionExecuting(ActionExecutingContext filterContext)
   {
     HttpRequestBase request = filterContext.HttpContext.Request;

     string acceptEncoding = request.Headers["Accept-Encoding"];

     if (string.IsNullOrEmpty(acceptEncoding))
       return;

     acceptEncoding = acceptEncoding.ToUpperInvariant();

     HttpResponseBase response = filterContext.HttpContext.Response;

     if (acceptEncoding.Contains("GZIP"))
     {
       response.AppendHeader("Content-encoding", "gzip");
       response.Filter = new GZipStream(response.Filter, CompressionMode.Compress);
     }
     else if (acceptEncoding.Contains("DEFLATE"))
     {
       response.AppendHeader("Content-encoding", "deflate");
       response.Filter = new DeflateStream(response.Filter, CompressionMode.Compress);
     }
   }
 }
}

続いて、ちょっと便利に使うためのヘルパー関数群を定義したCompressHelper.cs↓。

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

using System.Web.Mvc;
using System.Text;

namespace System.Web.Mvc
{
 public static class CompressHelper
 {
   public static string ItemsKey = "_compressKeys";
   static string _formatScript = "<script type=\"text/javascript\" src=\"{0}\"></script>";
   static string _formatCss = "<link rel=\"stylesheet\" href=\"{0}\" type=\"text/css\" />";

   static Dictionary<string, List<string>> getItems(System.Web.UI.Page page)
   {
     var items = page.Items[ItemsKey] as Dictionary<string, List<string>>;
     if (items == null)
       page.Items[ItemsKey] = items = new Dictionary<string, List<string>>();

     return items;
   }

   public static void AddCompressSrc(this System.Web.UI.Page page, string key, string srcPath)
   {
     var items = getItems(page);
     if (!items.ContainsKey(key))
       items[key] = new List<string>();

     items[key].Add(srcPath);
   }

   public static string ScriptTags(this System.Web.UI.Page page, string key)
   {
     var items = getItems(page);
   
     var sb = new StringBuilder();
     foreach (var item in items[key])
       sb.Append(string.Format(_formatScript, item));
   
     return sb.ToString();
   }

   public static string CompressScriptTag(this System.Web.UI.Page page, string key)
   {
     return CompressTag(page, key, "application/x-javascript", _formatScript);
   }

   public static string CompressCssTag(this System.Web.UI.Page page, string key)
   {
     return CompressTag(page, key, "text/css", _formatCss);
   }

   public static string CompressCssTag(this System.Web.UI.Page page, string[] srcList)
   {
     var key = Guid.NewGuid().ToString();
     var items = getItems(page);
     items[key] = new List<string>();
     foreach (var src in srcList)
       items[key].Add(src);

     return CompressTag(page, key, "text/css", _formatCss);
   }

   private static string CompressTag(this System.Web.UI.Page page, string key, string type, string format)
   {
     var items = getItems(page);
     var list = items[key] as List<string>;
     if (list != null && list.Count > 0)
       return string.Format(format,
         string.Format("/Home/Compress?src={0}&type={1}",
                       page.Server.UrlEncode(list.Aggregate((s, ss) => s + "," + ss)),
                       page.Server.UrlEncode(type)));

     return "";
   }
 }
}

namespaceをSystem.Web.Mvcにしてるのは、ページでImport書くのが面倒だから。HtmlHelperにしてないのはPage.Itemsを使いたいから。 Page.Items にそのページで使うJavaScriptやらCSSやらを入れておいてまとめて出力するのに使ってます。Page.Itemsに入れておけば、マスターファイルとページファイルのそれぞれで入れた値をマスターファイル(Site.Masterね)から簡単に取り出せるから。同じコンテキスト内(HTTP 要求内って意味でのコンテキスト)でのデータの受け渡しができる(Session使う必要ないし)。 CSSの場合はページ上部にまとめて書いちゃうからPage.Itemsに入れる必要はないんだけど、その辺は好みの問題ってことで。 名前をつけて置けば、名前ごとの圧縮ファイルにできると思って、keyを渡すようにします。ぶっちゃけ意味なし。 どうやって使うかというと、まずはSite.Masterのheadタグの部分を↓こんな感じに変えましょう。

<head>
   <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
   <title><%= Html.Encode(ViewData["Title"]) %></title>
   <%=Page.CompressCssTag(new[] { "/Content/Site.css", "/Content/Stylesheet1.css", "/Content/Stylesheet2.css", "/Content/Stylesheet3.css" })%>
   <% Page.AddCompressSrc("msajax", "/Content/MicrosoftAjax.js"); %>
   <% Page.AddCompressSrc("msajax", "/Content/MicrosoftMvcAjax.js"); %>
</head>

CSSは文字列配列でがっつり渡す。JavaScriptはまだタグのレンダリングしたくないので、Page.Itemsに入れておく。headに書いてるのは定義場所が上の方が見やすいかな、と思ったから。 で、同じくSite.Masterのbody閉じ以降を↓こんな感じで。

</body>
<%=Page.CompressScriptTag("msajax")%>
<%=Page.CompressScriptTag("myscript")%>
</html>

ここまでで、"msajax"をキーにしたスクリプトは2つ定義してるけど、"myscript"をはどこで入れ点だよ!って思った?それはHome/Index.aspxの中で入れてるんですね~。 各ページごとのスクリプトがサイト共通のスクリプトよりも上位に展開されると、悲しい結末が訪れるから、ちゃんとHTMLの最後に展開されるようにしましょう。

で、Home/Index.aspxの最後の部分を↓こんな感じで。

    <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>
   <% Page.AddCompressSrc("myscript", "/Content/jscript1.js"); %>
   <% Page.AddCompressSrc("myscript", "/Content/jscript2.js"); %>
   <% Page.AddCompressSrc("myscript", "/Content/jscript3.js"); %>
</asp:Content>

これを実行すると展開されるHTML(Home/Index)は↓こう。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
   <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
   <title>Home Page</title>
   <link rel="stylesheet" href="/Home/Compress?src=%2fContent%2fSite.css%2c%2fContent%2fStylesheet1.css%2c%2fContent%2fStylesheet2.css%2c%2fContent%2fStylesheet3.css&type=text%2fcss" type="text/css" />
 
</head>

<body>
~省略~
</body>
<script type="text/javascript" src="/Home/Compress?src=%2fContent%2fMicrosoftAjax.js%2c%2fContent%2fMicrosoftMvcAjax.js&type=application%2fx-javascript"></script>
<script type="text/javascript" src="/Home/Compress?src=%2fContent%2fjscript1.js%2c%2fContent%2fjscript2.js%2c%2fContent%2fjscript3.js&type=application%2fx-javascript"></script>
</html>

これだけだと、外部ファイルはちゃんと読み込めませんね。 HomeコントローラーにCompressアクションを実装してませんでした。

    [Compress]
   [OutputCache(Duration=3600,VaryByParam="src")]
   public ActionResult Compress(string src, string type)
   {
     var items = src.Split(',');
     if (items != null && items.Length > 0)
     {
       var sb = new StringBuilder();
       foreach (var script in items)
       {
         string path = Server.MapPath(script);
         if (File.Exists(path))
           sb.Append(File.ReadAllText(path));
       }

       return new ContentResult() { Content = sb.ToString(), ContentType = type };
     }

     return new EmptyResult();
   } 

srcパラメータにまとめるファイル名をカンマ区切りで渡すと(渡すときにUrlEncodeしても、自動でUrlDecodeしてくれます)、ファイルを読み込んで1個にまとめて出力するようにしてます。 圧縮はCompressAttributeにお任せ。OutputCacheを指定して、パラメータ毎(外部ソース毎)に1時間キャッシュするように指定してみました。 CompressとOutputCacheを外してFirebugで見た結果が↓。

img.aspx2

※CSSのレスポンス

img.aspx3 ※JavaScriptのレスポンス。 どっちも圧縮されてないし、Expiresヘッダもないね。 で、CompressとOutputCacheをつけてFirebugで見た結果が↓。

img.aspx4 ※CSSのレスポンス。

img.aspx5 ※JavaScriptのレスポンス。

いずれもファイルサイズも小さくなってるし、Expiresヘッダもついてクライアントキャッシュが有効になってます。 ここまで来て圧縮なら「IISの静的ファイル圧縮使えば?」という思いが当然のごとく出てくるよね。もちろん試しましたよ!ファイルを1つにまとめる機能はないけど、圧縮はできるはずだもんね。でもね、なんかうまく出来なかったんですよ。CSSは出来てるのにJSが出来なかった。 httpCompressionのstaticTypesの指定が悪いのかなんなのか。何にせよ、設定でちゃんと思い通りに出来なかったらコードを書けばいいじゃないか! ※負け犬...。

2008年9月3日水曜日

ModelBinderに気をつけねば

前のエントリでModelBinderを使って、ActionのパラメータにDBから取ってきたエンティティモデルをそのまま渡す方法を書いちゃったけど、あれはダメでした。

DataContextをstaticにもつサンプルだったけど、そうだとしてもいつDisposeされるのかなんて分かんないから、試しに実装してみたらケチョンケチョン...。

で、正しい使い方はこちら↓。 How to use the ASP.NET MVC ModelBinder - Melvyn Harbour というのを、昨日ガスリー君のブログでも書かれててホッとした。 ASP.NET MVC Preview 5 and Form Posting Scenarios - ScottGu's Blog

サンプルをチェックしてると、ProductをModelBinderでとってるじゃないかと思えるかもしれないけど、あくまで新規登録時の空エンティティの時にしか使ってないですよね。 で、更新処理の時にはDataContextから取得したProductに対して、ModelUpdateを使って値の書き込み。 この方法だとですね、更新時にはProductBinderのGetValue走らない。サンプルだから両方乗せてるんだろうけど(ModelUpdateとModelBinder)、最初はどんな意味が込められてるのか混乱しちゃった。 ModelUpdateでセットされるデフォルトのエラーメッセージが気に入らなかったらどこで書き換えればいいのかはちょっと分かんなかった。リソースファイルに持ってるのをどうすればいいんだろか。

で、 ProductクラスはLINQ to SQLのクラスなんだけど、これに対してModelStateDictionaryへメッセージを突っ込むコードを書くと、クラスが密結合しすぎちゃうがために、あえてRuleViolationクラスを作って(後でModelStateDisctionaryに入れやすくするために)、 IRuleEntityを実装。

ProductクラスのOnValidateはSubmitOnChangeの時に自動で呼び出してくれるっていうのがミソですね! でも、実際の開発は、たぶんだけどLINQ to SQLのエンティティクラスに対して直接入力しないよね。もう一つ間にViewDataクラスをはさんで、ViewDataにDBから読み込んだ値を入れてFormに表示。更新の時にViewDataに読み込んだ後、エンティティクラスに値をマッピングしていって更新。そんな流れになると思うので、入力検証のサンプルが↓これ。

Maarten Balliauw {blog} - Form validation with ASP.NET MVC preview 5

このサンプルではViewDataに直接値を入れてるけど、ViewDataのクラスを用意してViewPage<UserViewData>でレンダリングをView(model)にするんだと、↓こんな感じになるんじゃないかと思いますがどうですかね。

public class UserViewData { public name {get;set;} public email {get;set;} public message {get;set;} }

Contactアクション(POST)の入力値の取得で

var viewData = new UserViewData(); ModelUpdate(viewData, new[]{"name","email","message"});

って、すれば個々に入力値を取得しなくてもviewDataに埋め込みますわね。 でも、それだと入力検証できませんわね。全部stringだし。 なので、UserViewDataクラスに検証用のメソッドを追加して、それを呼び出すときにModelStateDictionaryを渡すのが簡単でいいんじゃないかと思います。 例えば、↓こんな。

public bool Validate(ModelStateDictionary modelStates)
{
 if (string.IsNullOrEmpty(name))
  modelStates.AddModelError("name",name,"名前入れてね!");
 else if (name.length < 4)
  modelStates.AddModelError("name",name,"4文字以上で名前入れてね!");

return modelStates.IsValid;
}

で、アクションでは↓。

 var viewData = new UserViewData();
 if (TryModelUpdate(viewData, new[]{"name","email","message"})) {
  viewData.Validate(ViewData.ModelState);
 } 

なんかヘンテコなコード...。 ※ModelUpdateも中でModelStateDictionaryにエラー値を入れてくれます。キャストできないとか。 ※Prefixをつけた場合、今までは最後に'.'(ドット)を自分でつけなきゃいけなかったのに、自動でつくようになってちょっと涙目...。気がつくのに時間かかった。

ちなみにUpdateModelのキー名はmodelに持ってる項目だけにすべし。Modelの項目を指定して、Formにない場合はエラーにならないけど、その逆はエラー(FormにあってModelにない)になるので気をつけよう。 ModelState情報はうまく利用すれば、エラーフィールドを強調できる(class属性にinput-validation-errorが自動でつく)ので超便利です。

ModelState はRenderPartial時にViewDataDictionaryを渡さないと(ViewData.Modelだけだとダメ)ユーザーコントロールで取得できないから、入力項目を持つユーザーコントロールのRenderPartial時には問答無用でViewDataも渡すようにするのが吉!

その他気になったところ。

Default option label for DropDownList in ASP.NET MVC Preview 5 - Shiju Varghese's Blog

便利にはなるよね。でも、必須にしなくてもいいじゃないかと、思ってしまうんですよ。 LINQで取り出した、ソースの最初の行に空(ここでいうoptionLabel)のレコードを連結させるコードを書いてたから、それがなくなるのはいいんだけど。ちなみにotionLabelに空文字""を指定すると何も起きないからレンダリング結果は今まで通り。

Maarten Balliauw {blog} - ASP.NET MVC preview 5's AntiForgeryToken helper method and attribute Steve Sanderson’s blog » Blog Archive » Prevent Cross-Site Request Forgery (CSRF) using ASP.NET MVC’s AntiForgeryToken() helper 何に使うのかサッパリわからなかったけど、こうやって使うんだね。CookieとForm(HIDDEN)POSTの値を比較して有効なリクエストか判定。

file download as attachment in latest preview (like this blog post) - ASP.NET Forums

FileResultの簡単な使い方。FileResultはResponse.TransmitFileとかで結果を直接返さずにActionResultとして返すことで、テストしやすくなるよね。 AntiForgeryToken/FileResultともにMicrosoft.Web.Mvcに入ってるデス。

2008年9月1日月曜日

ActionNameとAcceptVerbs

やっと意味がわかりました。

How a Method Becomes An Action

Phil Haackさん(なんかインタビュービデオを見たんだけど、どう見てもモルダー...。ホントはFBIなんじゃん?)とこで細かく書かれてるんだけど、なんていうかさ、英語じゃん?

でも、くじけるわけにはいかねっす。作ってるプロダクトがまったく動かないからね。 今まで(Preview4)は、ControllerやActionの前後でなんかしたかったり、処理をざくっと注入するときはActionFilterAttributeクラスを派生したものを使ってました。 Preview5でも同じクラスはあるんですよ。だけど、これが罠でして。同じ名前のクラスなのにコロッとかわっててね。流石Preview版。

どう変わってるかというと、ActionFilterはもう前後に処理をはさむだけで、その中で別のActionを呼んだり、Actionそのものの実行をキャンセルするのは止めないか的な。 いや、filterContextにはResultプロパティがついてて、そこに結果を入れてしまえばいいんだけど。 例えば↓。

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

using System.Web.Mvc;

namespace MvcApplication1
{
  public class StopAttribute : ActionFilterAttribute
  {
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
      filterContext.Result = new ContentResult() { Content="途中で止める" };
    }
  }
} 

こんな感じでStopAttributeをActionFilterAttributeから派生して作っておいて、Home/Indexアクションにセットする。

    [Stop]
    public ActionResult Index()
    {
      ViewData["Title"] = "Home Page";
      ViewData["Message"] = "Welcome to ASP.NET MVC!";

      return View();
    } 

img.aspx

ばっちり中断して、違う結果を返してますね。 これはいいんですよ。別にこれがしたいわけじゃないんですよ。

RESTfulAttributeっていうのをずいぶん前に書いた(進化の過程をウキウキウォッチング)んですよ。 要は、GETの時に呼び出すActionとPOST/PUT/DELETEの時のActionを勝手に切り替えてくれるってもんです。でも、普通にブラウザからだとPUT/DELETEはダメなもんだから、prototype.jsの仕様に合わせて"_method"って名前でメソッド名を送信すると HTTP Methodじゃなくて、そっちをみて判断するようにしたものです(Railsっぽいよね!)。

これを実装するには、HTTP Methodを見て、Actionを切り替えるために元々のActionの実行をキャンセルして、HTTP Methodに合わせたActionを代わりに実効(ActionInvoke)して、その結果を返す必要があります。単純に考えたら filterContext.Resultに変わりになるActionの実行結果を入れてしまえばいいってことになるんだけどさ。もちろんそれも正解(上記サンプルのように実装できるし)。 でも、たぶん設計思想はそうじゃないっぽい。filterContext.Cancelがないし。

そこで出てきたのがActionNameAttributeとAcceptVerbsAttribute。ActionNameAttributeは、単純にAction名を別名に置き換えるもので、AcceptVerbsAttributeはHttpMethodを見て、実行可能な場合のみ(GETだけとかPOSTだけとか)Actionを実行するもの。 でね、それぞれ派生元のクラスがさ、ActionFilterAttributeじゃないんですよ。 ActionNameはAttributeクラス。AcceptVerbsはActionSelectionAttributeクラス。ActionSelectionAttributeクラスっていうのが今までなかったもので、今回追加されたんですね。 これは単純なabstractクラスで、

   public abstract bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo); 

たったこれだけ。

んじゃこのIsValidForRequestは何に使うのかというと、Actionの実行を許す時はtrueを返し、実行させたくないときはfalseを返す。 ウソじゃねっす!!試したっす! なので、こういうクラスが今回追加されてきたってことはですね、Actionの実行そのものと、Actionの実行を許可するかどうかは別々に実装すべきなんじゃないですかっていう設計思想なんじゃないかと。 いや、モルダーPhilさんがどう思ってるのかは知らないけど。 ※AuthorizeAttributeとかは別にActionSelectionAttribute派生じゃないから言い切れるわけじゃないっぺ!

なので、そういうことならそっちに合わせた方がカッコイイんじゃないかと。 作ってみたのがRESTfulVerbs。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using System.Web.Mvc;

namespace MvcApplication1
{
  public class RESTfulVerbsAttribute : ActionSelectionAttribute
  {
    // 有効なHTTP Method(複数可)
    public string HttpMethods { get; set; }

    public RESTfulVerbsAttribute() : this("GET,POST") { }
    public RESTfulVerbsAttribute(string methods)
    {
      HttpMethods = methods;
    }


    public override bool IsValidForRequest(ControllerContext controllerContext, System.Reflection.MethodInfo methodInfo)
    {
      string[] enableHttpMethods = HttpMethods.ToLower().Replace(" ", "").Split(',').Where(s => s.Length > 0).ToArray();

     string httpMethod = controllerContext.HttpContext.ToLower();

      // prototype.js対応
      if (httpMethod == "post")
        httpMethod = context.Request.Form["_method"] ?? httpMethod;

      return enableHttpMethods.Contains(httpMethod.ToLower());
    }
  }
} 

どうやって使うかというと↓。

    [RESTfulVerbs("GET")]
    public ActionResult Index()
    {
      return View();
    }

    [ActionName("Index"),RESTfulVerbs("POST")]
    public ActionResult IndexPost()
    {
      // Indexに対してのPOSTはここで実行
    }

    [ActionName("Index"),RESTfulVerbs("PUT,DELETE")]
    public ActionResult IndexPut()
    {
      // Indexに対してのPUT/DELETEはここで実行
    } 

簡単ね。ActionNameAttributeとのコンボです。 単純なんだけど、こういう形にしちゃうと既存コードの変更箇所が凄い多くなるのが痛い...。ガンバです! でもね、ひとつ前のRESTfulFilterAttributeの実装の時(ASP.NET MVCでRESTful)に思ったんだけど、今回みたいな実装にするっていうことはですよ、リクエスト毎にグルグルとActionNameで指定したActionを探すことになるよね、きっと。それが嫌だしちょっとカッコ悪いと思ったから2個目の実装にしたのに。 そんな負荷は微々たるもんだから気にするんじゃないってことかな?

ちなみにfilterContext.ResultにActionの実行結果を入れる版のRESTfulAttributeは↓こんな感じの変更です。

      Type ctrl = filterContext.Controller.GetType();
      if (actions.ContainsKey(httpMethod) && actions[httpMethod] != "")
      {
          MethodInfo method = ctrl.GetMethod(invokeAction);
          if (method != null)
          
filterContext.Result = ctrl.InvokeMember(invokeAction,
BindingFlags.InvokeMethod, null, filterContext.Controller,
filterContext.ActionParameters.Values.ToArray()) as ActionResult;
      } 

※こっちのほうが修正少なくて楽...。

早くソースが公開されないかな~。 とりあえず、今回実装されてるFilter一覧を見てたらすごく気になるもの発見。 まず、この2つは基本クラス。

  • ActionSelectionAttribute : Attribute public abstract bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo);
  • CustomModelBinderAttribute:Attribute protected CustomModelBinderAttribute();
で、今までもあったものを含めて使えるフィルターがこれ。
  • AcceptVerbsAttribute:ActionSelectionAttribute
  • ActionNameAttribute:Attribute
  • AuthorizeAttributeFilter:Attribute, IAuthorizationFilter
  • HandleErrorAttribute:FilterAttribute, IExceptionFilter
  • ModelBinderAttribute : CustomModelBinderAttribute
  • NonActionAttribute : ActionSelectionAttribute
  • OutputCacheAttribute : ActionFilterAttribute
最後に気になるフィルターがこれ。
  • MethodSelectionAttribute:Attribute public virtual MethodSelectionResult OnMethodSelected(ControllerContext controllerContext, string action, MethodInfo methodInfo); public virtual MethodSelectionResult OnMethodSelecting(ControllerContext controllerContext, string action, MethodInfo methodInfo);
何に使うんだろ...。

2008年8月30日土曜日

ModelBinderが素敵過ぎる

ASP.NET MVC Preview5の続き。

SingingEels : Model Binders in ASP.NET MVC

↑ここでサンプルダウンロードできるけど、DefaultModelBinderを派生させて独自Binderを定義しておくことで、Actionのパラメータをクラスの実体に置き換えることができるというもの。

とにかくダウンロードすればすぐわかるんだけど、少し解説。

まずはBinderのクラスを作成。これはDefaultModelBinderクラスを派生させましょう。 で、ConvertTypeメソッドをオーバーライドして、valueに入ってる値をdestinationTypeに変換してあげる。 例えば、ここのサンプルだとCustomersテーブルのID値をBase64にエンコードしたものをtargetCustomerという名前の QueryStringにしてActionLinkでリンクを生成して、そのリンクをクリックしたときのActionでCustomerクラスの実体が渡されるっていうものになってる。

this.Writer.Write(this.Html.ActionLink<HomeController>(c => c.Details(customer), customer.FullName));

↑これをIndex.aspx内で書いてて、リンク(aタグ)を出力してるんだけど、これの出力結果がたとえばID=1なら1をBase64エンコード('='は'_'に置換)して↓こうなる。

<a href="/Home/Details?targetCustomer=AQAAAA__" >Ivan Buckley</a>

これのアクション定義は↓。

public ActionResult Details(Customer targetCustomer){...}

んだけど、これはつまりデフォルトのルーティングをそのまま使うように変更してidという名前で渡すようにしてみるとですね、↓こうなるわけです。

アクション:public ActionResult Details(Customer id){...} リンク:<a href="/Home/Details/AQAAAA__" >Ivan Buckley</a>

これは、分かりやすくていいですよね。 型付きActionLinkで生成してるからIndex.aspxのほうは変更の必要なし。 わざわざアクションの中でIDからデータを取得するコードを書かなくても、MVCのハンドラがモデルに変換する処理をはさんでくれるというすぐれもの。 ただ、勝手に変換はしてくれないので、Global.asaxでModelBindersにどの型の変換をどのクラスで実行するかを登録しておく。

ModelBinders.Binders.Add(typeof(Customer), new MyCustomerBinder());

もう、楽しくてしょうがないね!

まさかまさかの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に入れたりして、エラー項目を保持するのを自分で実装しなきゃいけなかったりしてたけど、超いい感じ!

2008年8月29日金曜日

ASP.NET MVCの記事

待望の後編が!! もう一つのASP.NET 「ASP.NET MVC」を知る(後編):CodeZine 一通りの機能説明がされててナイスです!

とりあえず前後編読んでおけば何となくアプリケーション作れるんじゃない? あと、ActionResult(ViewResultとか)?

最近、知ったんだけど"The ASP.NET MVC Information Portal"っていうのができてて、これでもかってくらい外部のサイトとかFeedで集めまくってるのでちょっと楽しいです。

最近はSSLを使えるようにいろいろ試行錯誤。 Vistaだと自己証明証明書とかで簡単にテスト環境作れるのがいいね。 SSL に使用する証明書の構成 コマンドで作成すればもっと融通のきく勝手証明書が作れるのかな~? こっちはうまくできなかったけど。 で、SSLで通信できるようになったのはいいけど、今度はどうやってリンクをSSLにしましょうか、ってところですよ。

Html.ActionLinkやRouteLink、Url.Actionとかで吐き出すURLをどうすればいいんだろかと思って調べてたら、こんなすごいのを発見。 Steve Sanderson’s blog » Blog Archive » Adding HTTPS/SSL support to ASP.NET MVC routing 書いてるとおりにSystem.Web.Routingモジュールを外して、こっちに置き換えて、Global.asaxにRoute登録すると...。すげ~!!かっこよくHTTPSとHTTPが切り替わるように出力されてる。絶対URLと相対URLをうまく切り替えてて賢く動く。

でも、VisualStudioのWebDev.WebServerじゃ、SSLで動かせないからIISに設定し直してテストしてみると~!!なんとまぁ、ちゃんと動かない...。 Problem 1のところに本人も書いてるけど、同じ症状。残念です。"Some gremlin in the routing"だってさ。 でも、これを改善したという猛者がいたりして。 Dmitriy Nagirnyak: Fixing HTTPS Support in ASP.NET MVC Routing

なぬ~!!と速攻でこの修正を加えて試してみたけど、ダメだった...。なんでだろね。ソース追っかける元気が...。いつかきっと確認しときます。 あ、そういえば、SP1 RTMにアセンブリリダイレクトしてないからうまく動かないのかも?まぁ、いっか。 このままじゃ、面倒なことになるなと思ってたところで、代替案。 Troy Goode: SquaredRoot - SSL Links/URLs in MVC

使い方は超簡単。いや、そりゃそうだってなもんで。 こっちはRoutingがどうのこうのじゃなくて拡張メソッドでガリっとURL書き換え。 なので、自分で書き換えたい部分のコードを変更する必要あり。 ちょっと面倒だけど、確実なのでとりあえずはこの方法で進めることにしてみます。 ※Html.ActionLink(...).ToSslLink()とかUrl.Action(...).ToSslUrl()って感じで使います。 このままだと、いったんHTTPになった後にHTTPに戻しにくいので、同じ要領で↓こんなのも用意しときましょう。

    public static string ToHttpUrl(this string text)
    {
      if (Utility.UseSSL)
        return ToFullyQualifiedUrl(text).Replace("https:", "http:");
      else
        return text;
    }

    public static string ToHttpLink(this string text)
    {
      if (Utility.UseSSL)
        return ToFullyQualifiedLink(text).Replace("https:", "http:");
      else
        return text;
    } 

そうそう、最初のナオキさんの記事の最後のページにこのブログが紹介で載ってるんだけど、こういうときにブログのタイトルがこんなだとちょっと恥ずかしい...。

2008年8月9日土曜日

イベントの実行順が面白くて

SQL Server 2008に合わせて.NET Framework 3.5 SP1が見えてるところですね。 ウェイトリフティングの三宅選手がスナッチあげてる時に使ってるタオルがスティッチ。ジャークのときにはサメのキャラを期待。

ASP.NET MVCのControllerではoverride出来るイベント(Onなんちゃら)が6個ありまして。

  • OnAuthorization(承認)
  • OnException(例外)
  • OnActionExecuting(Action実行前)
  • OnActionExecuted(Action実行後)
  • OnResultExecuting(Result実行前)
  • OnResultExecuted(Result実行後)
このActionとResultって何なんですか?って気になるところだったりしませんか? 以下のようなコードをHomeControllerに書くとどんな出力がでるのか試してみるとよく分かります。

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

namespace MvcApplication1.Controllers
{
 [HandleError]
 public class HomeController : Controller
 {
    protected override void OnAuthorization(AuthorizationContext filterContext)
   {
     System.Diagnostics.Debug.WriteLine("OnAuthorization");
     base.OnAuthorization(filterContext);
   }

   protected override void OnException(ExceptionContext filterContext)
   {
     System.Diagnostics.Debug.WriteLine("OnException");
     base.OnException(filterContext);
   }

   protected override void Execute(ControllerContext controllerContext)
   {
     System.Diagnostics.Debug.WriteLine("before Execute");
     base.Execute(controllerContext);
     System.Diagnostics.Debug.WriteLine("after Execute");
   }

   protected override void OnActionExecuting(ActionExecutingContext filterContext)
   {
     System.Diagnostics.Debug.WriteLine("- OnActionExecuting");
     base.OnActionExecuting(filterContext);
   }

   protected override void OnActionExecuted(ActionExecutedContext filterContext)
   {
     System.Diagnostics.Debug.WriteLine("- OnActionExecuted");
     base.OnActionExecuted(filterContext);
   }

   protected override ViewResult View(string viewName, string masterName, object model)
   {
     System.Diagnostics.Debug.WriteLine("-- View");
     return base.View(viewName, masterName, model);
   }

   protected override void OnResultExecuting(ResultExecutingContext filterContext)
   {
     System.Diagnostics.Debug.WriteLine("- OnResultExecuting");
     base.OnResultExecuting(filterContext);
   }

   protected override void OnResultExecuted(ResultExecutedContext filterContext)
   {
     System.Diagnostics.Debug.WriteLine("- OnResultExecuted");
     base.OnResultExecuted(filterContext);
   }

   public ActionResult Index()
   {
      System.Diagnostics.Debug.WriteLine("-- Index action execute");

     ViewData["Title"] = "Home Page";
     ViewData["Message"] = "Welcome to ASP.NET MVC!";

     return View();
   }

   public ActionResult About()
   {
     ViewData["Title"] = "About Page";

     return View();
   }
 }
} 

で、これだけだとちょっと見落としちゃうタイミングがあるので、Views/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">

<% System.Diagnostics.Debug.WriteLine("-- page rendering"); %>

   <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>
</asp:Content>

これで、準備完了。 ※太字が追加したコードです。 実行してみると、デバッグ出力が↓こうなります。

before Execute OnAuthorization - OnActionExecuting -- Index action execute ← ここでアクション実行 -- View - OnActionExecuted - OnResultExecuting -- page rendering ← ここでASPXのレンダリング - OnResultExecuted after Execute

面白いでしょ? ASPX の実行はずいぶん後なんですよね。Viewの実行時にレンダリングされるわけじゃないっていうのが、なるほどなと思わずにはいられない。ちなみに Redirect/RedirectToAction/RedirectToRouteもContent/JsonもViewと同じタイミング。ASPX のレンダリングのタイミングと同じタイミングでResult実行されるのを気をつけておく必要ありです。 ControllerのExecuteの中でこんな順序で処理されてるっていうことが分かれば、いろいろできそうじゃないですか。 で、なんでこんなこと書いてるかというと、TempDataですよ。 この中でTempDataってどういうタイミングで保存復元されるんだろかと。 ソースを追いかけるとController.cs内のprotected internal virtual void Execute(ControllerContext controllerContext)に書かれてますね。 InvokeActionを呼び出す前に、TempData.Load(TempDataProvider)。InvokeAction後にTempData.Save(TempDataProvider)。 と、いうことはControllerのExecuteをoverrideしてbase.Executeの前後でTempDataにデータを入れても意味ないってことですよ(ね?)。 TempDataの出し入れのタイミングを間違えると、入れたのに取り出す時にはnullってことになりかねないので注意が必要です。

ViewDataに比べてあんまり注目されてない気がするTempDataだけど、結構使い道があって(メッセージ出力時や、ViewDataで使うモデルデータに関連するデータを入れたり)するので、積極果敢に攻めの姿勢で使っていこうと思うところですよ。 ※ただしTempDataはシリアライズの問題もあり、LINQ to SQLのモデルをそのまま入れることはできない(StateServerとSQLのSession変数に入れられないのと同じ理由)ので、匿名クラスとかに変換して入れたりします。 それにしても、スナッチ1回目で90kg上げる中国チン選手恐るべし!最終的にミスなしで95kgて...。自重の倍て...。

2008年7月29日火曜日

IsMvcAjaxRequest

ナオキさんのサイトで取り上げられていたので、流行りに乗っかっていこうと思います! ASP.NET MVC Preview 4からAjaxが少しとりいれられてます。

クライアントサイドはMicrosoft Ajax Libraryがベース。 これの使いどころはやっぱり部分更新ですよね。ASP.NET AJAXならUpdatePanelのような動きと言えばわかりやすいかな?

とにかく動かしてみることにしましょう。 まずは、Preview 4のプロジェクトテンプレートで新しいプロジェクトを作成。 そしたら、AccountControllerとHomeContoroller、Views/AccoutとViews/Homeとか出てきます。 Views/Homeの中には最初に表示されるIndex.aspxとAboutページのAbout.aspxが出来てます。 とりあえずHomeControllerのIndexアクションとViews/Home/Index.aspxだけを使って試してみることにします。

初期のIndexアクションは↓。

public ActionResult Index()
{
ViewData["Title"] = "Home Page";
ViewData["Message"] = "Welcome to ASP.NET MVC!";

return View();
}

Index.aspxは↓。

<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>
</asp:Content>

なんとまぁ、スッキリしたものが出てきますわ。 ※ViewData["Title"]はShared/Site.Masterで使ってますよ。 で、ページにFORMを張り付けてPOSTさせてみよう! IndexアクションでPOSTした値を取得してViewDataに入れようじゃないですか。

public ActionResult Index()
{
 ViewData["Title"] = "Home Page";
 ViewData["Message"] = "Welcome to ASP.NET MVC!";
   
 ViewData["result"] = "";
 if (Request.HttpMethod.ToLower() == "post")
 {
   ViewData["result"] = string.Format("こんにちは、{0} さん!", Request.Form["yourName"]);
 }

 return View();
} 

↓これをIndex.aspxに追加(</p>の後に)。

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

      名前は?<%= Html.TextBox("yourName") %>
      <input type="submit" value="ぼたん" />
      <span id="result"><%= ViewData["result"] %></span>
    
   <%} %>

こんな感じですね。POSTしたyourNameをViewData["result"]入れて、それをspanタグ内に表示するものです。ここまではすんなりです。 img.aspx

↑こんな表示になるんで、テキストボックスに適当になんか入れて「ぼたん」押すと、↓こんな感じでボタンの横に表示されます。

img.aspx2 とびっきり普通の処理です。

ここからです! IndexアクションへのPOSTが発生した場合、ブラウザからのものなのかXMLHttpRequestからのものなのかを簡単に判別する方法として、Request.IsMvcAjaxRequest()というのがあるので、それを使うことにします。 なので、Indexアクションを変更。

    public ActionResult Index()
   {
     ViewData["Title"] = "Home Page";
     ViewData["Message"] = "Welcome to ASP.NET MVC!";
   
     ViewData["result"] = "";
     if (Request.HttpMethod.ToLower() == "post")
     {
       ViewData["result"] = string.Format("こんにちは、{0} さん!", Request.Form["yourName"]);
        if (Request.IsMvcAjaxRequest())
         return Content((string)ViewData["result"]);
      }

     return View();
   } 

↑太字の部分が追加したコードです。 差を分かりやすくするのに、Index.aspxには追加でAjax.Formを入れることにします。 なので、Index.aspxの全体は↓。

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

<asp:Content ID="indexContent" ContentPlaceHolderID="MainContent" runat="server">
   <script src="/Content/MicrosoftAjax.debug.js" type="text/javascript"></script>
   <script src="/Content/MicrosoftMvcAjax.debug.js" type="text/javascript"></script>
 
   <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>
 
   <% using (Html.Form("Home", "Index"))
      { %>

      名前は?<%= Html.TextBox("yourName") %>
      <input type="submit" value="ぼたん" />
      <span id="result"><%= ViewData["result"] %></span>
    
   <%} %>
 
   <% using (Ajax.Form("Index", new AjaxOptions { UpdateTargetId = "result2" }))
      { %>
    
      名前は?<%= Html.TextBox("yourName") %>
      <input type="submit" value="ぼたん" />
      <span id="result2"><%= ViewData["result"] %></span>
    
   <% } %>
 
</asp:Content>

これを実行すると2つのテキストボックスとボタンが表示されます。 img.aspx3

フォームも2つあるからそりゃそうですね。 最初に上段のテキストボックスとボタンに”さる”と入れて上段のボタンを押します。

img.aspx4

ボタンの横に両方とも”さる”と出ます。 続いて、下段のテキストボックスに”いぬ”と入れて下段のボタンを押します。

img.aspx5 小さすぎて見にくい...。 まぁ、それはいいとして、下段だけが"いぬ"になりましたね。 結果だけを見ると分かりにくいんですけど、実際にコードを書くとですよ、上段のボタンはページ全体をPOSTで取得するのに対し、下段はボタン横のテキストのみAJAXで取得して書き換えてる動きになってるのが確認できると思います。

2008年7月18日金曜日

やったねPreview 4!

一部で話題沸騰中のASP.NET MVCですが、Preview 4が出ましたね! 教えてくれたナオキさんに感謝デス!

ASP.NET - Release: ASP.NET MVC CodePlex Preview 4 Installer + Source

さっそくダウンロードしてインストール。Preview 3のアンインストールが必須です。

で、今回のリリースでどこがどう変わったのか気になるよね! イロイロ変わってるみたいですが、Preview 3からの変更点はそれほど大きくなくて、実装してるものはほぼそのまま移行できるというすぐれものです。 ただし!ViewDataは手を加える必要があるくらい変わりましたね。

例えば。 Page(aspx)でUserControl(ascx)をHtml.RenderUserControlでレンダリングするとき、第2引数にViewDataを指定しないなら、そのままUserControl(ascx)でもViewDataが参照できます。 ここまでは今まで通り。

じゃ、ViewDataとして匿名クラスを渡した場合どうなるでしょ?

<% = Html.RenderUserControl("~/Views/UserControls/MyControl.ascx", new {Val1=1,Val2="弐"}) %>

↑こんな時ね。あるよね?ない?

今までは上記のMyControl.ascx内で参照するとき(int)ViewData["Val1"]とか(string)ViewData["Val2"]で参照できたとことが、Preview 4になってからはそれができなくなりました。 デバッガでどうなってるのか見てみると、上記のような場合でもViewData.Modelに値が入ってしまうんですね。でも、待って下さい、匿名クラスなんですが! そうね、UserControl内でキャストしようにもできませぬ。

そんな時はViewDataDictionaryクラスに新しく追加されたEval関数を使いましょう! 上記の例で行くと(int)ViewData.Eval("Val1")とか、(string)ViewData.Eval("Val2")って具合です。 ※もっといい書き方あるかもしれないけど...。

匿名クラスをViewDataとして渡すという書き方自体があまりオーソドックスじゃないかもしれないけど、新しくクラスを作るほどでもなく、複数の値をUserControlに渡したいというものぐさな人にはピッタリ(自分)。 今回のPreview 4で感激したのがAuthorizeAttributeとHandleErrorAttribute。いずれのフィルターもControllerのActionどっちでも使えて楽ちんぽん!

Ajax 的な処理の部分はprototype.jsを使ってるので、改めてMicrosoft Ajax Libraryを使う気にはなれないけど、Microsoft Ajax Libraryが標準装備(scriptタグは自分で書かなきゃダメだけど)になってて、Ajax.Formなんかで部分更新(UserControlのレンダリングは見つからないけど)なんかも簡単にできるようになってますね。

Scott Hanselman's Computer Zen - ASP.NET MVC Preview 4 - Using Ajax and Ajax.Form

あとはね~、そうだな~、そうだ!TempDataがSerializableになりました!TempDataProviderっていうのを実装すれば、Sessionじゃないところにも入れれるけど、そこまではちょっと...。単体テスト向け? これで自分で作ったFlashDataなんていうクラスも使わなくて済むかも!と、思ったけど、TempDataに入れたのがずっとSessionで残ったままになってて意味ないじゃん!みたいな。 ※StateServer指定して試しました。 ↑ちゃんと消えました。 すいません。

HomeControllerでのテスト。 1.HomeController.Index TempDate["Message"]=”Test”; 2.Home.aspx <%= TempData["Message"] %> 3.About.aspx <%= TempData["Message"] %>

↑こんな感じで実行すると、Homeではもちろん表示されます。

で、"About us"リンクをクリックすると、Aboutでも表示されます。 もう一度"About us"リンクをクリックすると、今度は消えてます。 1の処理でTempDataに値を入れます。で、2の処理は同じパイプラインでの実行なので、そのまま表示されます。その後、同じパイプラインでTempDataProvider.SaveTempDataでセッションに保持。 一回目のAbout表示の時にTempDataProvider.LoadTempDataでセッションから取り出し、セッションをクリア。で、取り出したのを表示するから、ちゃんと出る。でも2回目のAbout表示ではセッションから消しちゃってるから表示されませんね。 あってます!自分が処理間違ってました!さーせん!TempData最高!

AccountControllerは結構普通だったのがちょっと残念かも。 そうそう、最初に生成されるSite.Masterのcharsetが相変わらずiso-8859-1なのは、なんか意味があるんだろうか。 ちなみに今回のリリースで一番感動するのが「ASP.NET MVC API Changes From Preview 3 to CodePlex Preview 4」というタイトルのソースの変更箇所一覧が書かれてるPDF。超感動。前回は差分がどこかわかんなくて苦労したけど、今回はこれがあるから大丈夫(だと思います)!

2008年7月17日木曜日

コメントがんばるガスリー君

ASP.NET MVC Preview 4 Release (Part 1) - ScottGu's Blog

ナオキさんとこで知ったPreview 4。
まだ水曜だけど、iPhone以上にリリースが待ち遠しくて、ボケ~と読み返してたんだけど、コメント欄が凄いことになってる...。

ガスリー君3つ子説俄然説得力を増してくる...。
JoeOn.net In Japanese : Scott Guthrie はどうしてあれほど多くの仕事ができるのか

気になるコメント。
>>>>>> Can you please give some date as to when will the ASP.Net MVC go live?

You can go live with ASP.NET MVC today.  The license supports production deployments.

製品開発に使ってもよかとですか。

>>>>>> Now that we are on Preview 4, what comes next? Beta 1?  RC1?

I think Beta1 probably isn't far off now.

P4の次はとうとうベータ1!?

>>>>>>> Hi Scott, love the blog, always a great read! One slightly (read completely) off topic question, what theme are you using for VS? It looks fantastic!

I have a slightly custom theme that I use.  You can download it here: www.scottgu.com/.../scottgu-dark.zip

ペーター君はなぜにそこが気になったのか...。
Using SubDataItems and View User Controls in ASP.NET MVC
Page でRenderUserControlするときに、第3引数になんらしらのViewDataを渡すと、UserControl内で ViewData.Modelは見れても、Controllerで入れたViewData[~]が空になって全く参照できなくてガッカリなことがあると思うんです。
で、Preview3のソースを追っかけてたところ、UserControlExtensions.csのDoRendering関数が怪しいんじゃないかと思って、検索してたら発見したのが↑のサイト。
同じようにSubDataItemsも子コントロール内での参照ができないから、ViewDataもソースいじれば引き継いでくれるようになるんだろうけど、なんかいじるのヤダナ。消極的にPreview 4を待ちます...。

2008年7月1日火曜日

ユニットテストですよね

ASP.NET MVC Tip #12 – Faking the Controller Context - Stephen Walther on ASP.NET MVC

Rhino Mocksでコンテキストをうまいことくるんでテストコード書いてたんだけど、これがまた使い方が良くわかんなくてシックハック症候群。

そんなところで、モック無でのテストコードを見せられた日には飛びつきたくなるってものですよ!
MvcFakeプロジェクトだけを使わせてもらえば、それはそれは至れり尽くせりとまではいかないまでも簡単テストができるじゃないですか。感激ですね。

Tip11への突っ込みに対して(個人的な心の中で)、見事Tip12で答えに導くあたり、心を読まれてるんじゃないかと軽くエスパー疑惑です。

2008年6月20日金曜日

DataControllerでDynamic Dataみたいな

ASP.NET MVC Tip #4 - Create a Custom Data Controller Base Class - Stephen Walther on ASP.NET MVC これもありだな~、と思います。

Google App Engineでの開発っぽい。 DataControllerだけが大事なところなのにコード量がべらぼうに少なくて、なんか嬉しい。 Form 値はRequest.Form.Keysをforeachで取り出したのとエンティティのプロパティが同じなら(上手く言えないけどリフレクションで)書き換える感じで。エンティティそのままでViewData用のクラスなわけじゃないから、 System.Web.Mvc.BindingHelperExtensions.UpdateFromを使えないのかな(NewとUpdateで同じコード書いてるのか切ない部分)?

でもね、GetDynamicGetがカッコよすぎる。 IdentityColumnNameも主キーがintの項目を1つにするルールを貫けば(Railsとかそうだし)超絶便利な予感。 派生してるのがHomeControllerだからリソース名がHomeに思えるけど、そこはデフォルトのままいじってないってことなんだろうから気にしない。実際にはこのサンプルならMoviesControllerとかにするのがナウなヤングのRails風?

RESTfulじゃないけど「設定より規約」なところがいいっす! 最近のお気に入りはDBのテーブル全てにEntryDateとModifyDateをDateTime型で作っておいて、DataContextクラスのSubmitChangesをオーバーライドして勝手に値を入れるようにすること。

public override void SubmitChanges(ConflictMode failureMode) { ChangeSet changes = this.GetChangeSet(); DateTime now = DateTime.Now; Action setNow = (entity, name) => { var etype = entity.GetType(); var prop = etype.GetProperty(name); if (prop != null) prop.SetValue(entity, now, null); }; // insert foreach (var entity in changes.Inserts) { setNow(entity, "EntryDate"); setNow(entity, "ModifyDate"); } // update foreach (var entity in changes.Updates) { setNow(entity, "ModifyDate"); } base.SubmitChanges(failureMode); }

GetChangeSet()で追加・更新・削除それぞれの対象レコードを取得できるから、それを取り出して自動更新。エンティティの型は宣言しないでvarで。varと規約ベースの開発は最高っす! ただ悩みもありまして...。

入力値の検証(Validation)をどこでやるのがいいのかってことなんですけどね、LINQ to SQLのEntityのpartial classでやると、確実にチェックではじけていいんだけど、タイミング的にはもう少し早い段階でやりたかったりする。ViewDataクラスにバリデーション機能をつけるのがいいのかな...。アプリケーションの制約と、データベースの制約とレイヤーが違うからそれぞれに必要なんだろうけど(例えばAと Bっていうカラムがあって、更新するときにどっちも空は嫌だけど、どっちも空でもDBでは問題なしとか)。 タイムリーにオノさんところで紹介されてたサンプル(SingingEels : ASP.NET MVC in the Real World)を見てみたけど、普通にコントローラでForm値を検証してるじゃんよ...。 こんな感じなのかな~(とりあえずTempDataに入れちゃうとロードバランサとかで無茶ぶりできなくなる)。

追記なんですけど。 ASP.NET MVC Tip #5 – Create Shared Views - Stephen Walther on ASP.NET MVC これまた面白いのが...。Tip #4の続きで今度はまさにDynamic Data。 コントローラ用のViewフォルダに、コントローラでView()するASPXがない場合の初期動作として、Sharedフォルダ使う動きをうまく生かして、すべてに共通のViewでページ生成。なので、インターフェイスを動的に作る必要があるからASPXのコードビハインドにコード書いてる。 全然気になんない!Scaffold!