2009年4月19日日曜日

いっぱいいっぱい

4月だけど、もう十分暑いね~。あまりにも人が少ない&オグさんいないってことで、気分転換もかねて久しぶりにアメージングに行ってきました。

海の向こうではNHLのプレーオフが始まってて、NHL.comのトップページにオベチキンガックリの写真が上がってたりと目が離せないね。レギュラーシーズンの録画も全然観れてないんだけど...。レンジャーズが2-0でリードしてるけど、まだまだこれから巻き返せる範囲だからキャピタルズにはがんばってもらいたい。初戦を落としたシャークスもダックス相手にがんばって!レッドウィングスはとりあえず2-0でリードだから連覇を目指してこの調子で勝ってね!

自分達の試合の話は...あんまり覚えてないな~。とりあえずレフェリーがタカチさんでチョイチョイいじられたのだけは覚えてる。ハッキリと。4点目のダブルアシストを指差して除外されたのもハッキリ覚えてますよ!

ぎりぎりまでゴーリーがいないってことで、オグさんゴーリーに引き続き今回はオレがゴーリーやんだろーなーと思ってたら、タクちゃんが来てくれるってことになって、超助かった。試合前から「滑れないから無理」と弱気なタクちゃんだったけど、そんなのどうでもいいっす。もう来てくれただけで助かる。ところでタクちゃんと一緒にいた女性の方は誰なんですかね。気になってしょうがなかったじゃないか。

試合開始前に男子3人女子3人+タクちゃんで、どういうセットにしようか悩んだけど、開始直前にミズノメンが来てくれて男子4人になったから、これまた助かった。これなら常時男2人をリンクに乗せておけるもんね。 試合は開始早々にミズノメンの個人技で2得点。うひょ~。オレいらね。今日はこのままミズノメンになんとかしてもらいたいところデス。

が、ミズノ君と一緒に組んでたテンパ君がゴール前のマークをことごとく外して3失点。アレは無理。止められません。タクちゃんが滑れる滑れない以前に無理。バックドアから触るだけで入るシュートだし。外す方が難しい。テンパがんばって。後輩がベンチから応援してるんだから! たけはらさんはね~、まぁね~、ぼちぼちですよ。普通にブラスに溶け込んでる感じっていうんですかね。あの子がんばってるね、的な?いっぱいいっぱいデス。

とりあえず、ギリギリだけど勝ててよかったね。ここで負けてたら負け越すところだったしね。負け越すといえば、駒沢NEXTにヤマちゃん出てたね。対戦するのかな。楽しみにしとこ~っと。今期から加入のミサキちゃん(?)はなにげに上手い気がする。下のクラスで出てるって話だけど、ブラスでも別に問題ない気がする。何度も惜しいシュートがあったしね。

そういえばS40蒲公英には"ミ"が付く女子が多い気がする。ミカ・ミチ・ミナ・ユミ・ミサキ...。7人中5人もいるね。流行なの?

2009年4月13日月曜日

ASP.NET MVCをMonoで動かしてみる

長い戦いだった...。現時点での結論から言えばそれなりに動くけど、型付きViewPageだけはどうにもならない感じでしょうか。

そもそもMonoでASP.NET MVCを動かすことにどれほどの意味があるのかという所だけど、これは凄く意味のあることですよね。なんせ実行環境はすべてオープンソースで構築できるんだから。こうなってしまえば個人的にはJavaに何もメリットを感じない。Google App Engineが羨ましいくらいか。でもPythonあるし、そもそもAzureがあるし。

構築した環境はVista Ultimate 32bitにVMWare Server 1.0.9を入れ(2.0.1だとどうも上手く動かなかった)、Linuxディストリビューション(っていうの?)はCentOS 5.3openSUSE 11.1、Mono 2.4とMonoDevelop 2.0、データベースにMySQL 5.0.45(CentOS上にyum install)。

My Adventures Installing mono 2.0 on CentOS 4 to work with apache via mod_mono « Pale Musings

Linuxを生まれて初めて触ってみたけど、さっぱりチンプンカンプンですね。でも、ネットで検索すればいくらでも情報が出てくるから、とりあえず動かすくらいならどうとでもなるもんです。ちゃんとまともに運用環境を作るにはかなり難しそうな気がするけどね。

何でCentOSとopenSUSEを入れたかたというと、MonoDevelopをCentOSにインストールするのに3日がんばったけどできなかったから。GLibのバージョンが入ってるのじゃダメと言われ、ソースコンパイルからやってみたけど、芋ずる式にアレもコレもダメだと言われ、コンパイルできてmake installで実行しても、Gtk+からちゃんと新しいのが見えないと言われ...。うぎゃ~!!となってやめました。Monoはすんなり入るのにね。で、一番簡単なのはNovellが出してるディストリビューションを使う事だと思って、openSUSEも入れることにしたんです。まぁ一緒だろと思ったらyumじゃなくてzypper(ソフトウエア管理のコマンドライン比較 – openSUSE)だとかよくわかんないことに時間を取られたものの、流石Novellなだけあってワンクリックでインストールもあっという間に完了。

mono1 mono2

MonoDevelopでASP.NET MVCのプロジェクトを新規作成したところ、流石にテンプレートは空っぽでControllerもViewも何もない状態でした。T4無いしね(たぶん)。なので、簡単にControllerとViewを作って動かしてみると、あら素敵。アッサリ動いた。簡単なものだけど、動くとビックリ。スゴイね~。んじゃ、ってことで、コレをCentOSの方にコピーしよと思ってハタと手が止まる。あれ?こういう時ってFTP?それともSSHっていう何かよくわかんないシェル?ふぬ~。あ、産婆。じゃなくてsamba。

Windowsファイルサーバー構築(Samba) - CentOSで自宅サーバー構築

これをCentOSとoenSUSE両方で起動してコピペ。ちなみにVMWareで動かしてるときにopenSUSEは何もしなくてもマウスが動くしGNOME端末での日本語入力もできるのに、CentOSはなんかやらなきゃいけないみたいで。VMWare toolsっていうんですかね。よくわかんないけど、さんざんやって上手く出来なかったから諦めた。いいもん。毎回Alt+Ctrl押すもん。

とりあえず、MonoDevelopだとVisual Studioに慣れた体では開発しにくいし、どうせ動かすのはCentOSだしってことで、ここヵらはCentOSにデプロイ、Vista+VSで開発に戻ります。openSUSEで動かしてもいいんだけどさ~。なんとなくきかん坊が気になるからさ~。

mono3

VSで作ったデフォルトのプロジェクトをそのままの状態でビルド>発行して、CentOSにコピー。んで、Apacheで動かすためにmod_monoの設定をしようかなってところで、XSPっていうのが簡易Webサーバーの役割を果たしてくれるということなんで、コピーしたフォルダをカレントに"xsp2"!アッサリ動くの図↑。

続いてMembershipを使いたいから、データベースを入れなきゃいけないよね。なのでMySQL。入れるのは簡単。とにかくyumさん。なんだこいつ、スゴイヤツだ。sudoさんと仲がいいのか?最初はsuで切り替えてやってたけど、普通のユーザーアカウントで読み取れないフォルダがばかばかできちゃって面倒なことになったから、結局何度もやり直して須藤さんsudo(SuperUser DO?)に任せる事に。

でもって、MembershipProviderがなんかあるはず。

MySQL :: MySQL 5.1 リファレンスマニュアル :: 24.2.2.2 Mono を使用した Unix に Connector/NET をインストールする

あるのね...。Connector/Netと言われても...。GACに登録したいけど、インストールパスってどこなんでしょう。そもそもどこにインストールされてるのかが分からない。"/usr/lib/mono/2.0"にいろいろ入ってみたいだけどここでいいのかな...。とりあえずコピーしてgacutilでグローバルアセンブリキャッシュに入れて見る。エラーは出てないからいいのかな。

んで"nolan bailey's blog: MySQL ASP.NET Membership and Role Provider"や”knowledge shared is knowledge²: ASP.NET and MySQL - membership provider (part 1)”ここに書かれてるようにweb.configをセット。autogenerateschema=”true”にしておくと勝手にテーブルを準備してくれるからaspnet_regsqlやInstallMembership.sqlをどうのこうの気にしなくてもいいから楽ちんです。忘れずにセットしなきゃね。

mono4

うほほ。ちゃんとデータベースに登録されました。

でも、本当はここが最初ちゃんと動かなくて、あれれな感じだったのがプロジェクトのアセンブリに追加したMySql.Webをローカルコピーしとくようにしたらなんとなく先に進むようになるものの、そこからさらにエラーでなんとも動かなくて。

mono5 mono6

ユーザー登録はできたのに、ログインでエラーになるの図。これはなんだも。

System.ArgumentNullException: Argument cannot be null.
Parameter name: type
 at System.ComponentModel.TypeDescriptor.GetConverter (System.Type type) [0x00000]
 at System.Web.Mvc.DefaultModelBinder.BindModel (System.Web.Mvc.ControllerContext controllerContext, System.Web.Mvc.ModelBindingContext bindingContext) [0x00000]
 at System.Web.Mvc.ControllerActionInvoker.GetParameterValue (System.Web.Mvc.ControllerContext controllerContext, System.Web.Mvc.ParameterDescriptor parameterDescriptor) [0x00000]
 at System.Web.Mvc.ControllerActionInvoker.GetParameterValues (System.Web.Mvc.ControllerContext controllerContext, System.Web.Mvc.ActionDescriptor actionDescriptor) [0x00000]
 at System.Web.Mvc.ControllerActionInvoker.InvokeAction (System.Web.Mvc.ControllerContext controllerContext, System.String actionName) [0x00000] 
なんでこんなエラーが...。全然ダメじゃないか。いろいろ検索してみたら同じ現象が起きてる人もいて。

http://go-mono.com/forums/#nabble-to23006435

特に返信もなくて途方に暮れかかったけど、Forumあるならスレッド見ていけばなにかヒントがあるかもと、見ていくと、↓こんな書き込みがありました。

http://go-mono.com/forums/#nabble-to22854337

Windowsで使ってるアセンブリをローカルコピーしとけと。なるほど。確かにバイナリ互換なMonoなんだからそういう手もありだ。System.Web.Mvc.dllをコピーして動かす。

mono7

拡大すると見えるけど、ちゃんとログインできてますね。グレート!

あとはLINQでデータベースにアクセスできれば普通に開発できる。でも、最初に書いたとおり型付きViewPageだけは今のところ解決策が見つからずなので、当面はコードビハインド指定で乗り切ることになるのかな。そんなこんなでここまで5日ほどかかったけど、面白い発見もいろいろあって実行環境としてLinuxも選択肢に入れていこうと思ったり思わなかったり(EC2もWindows Server+SQLServerよりもLinux+Mono+MySQLの方が同じインスタンスでも安く運用できるからね)。

2009年4月4日土曜日

もう一度AsyncController

正式にドキュメントが公開されてるので、改めてドキュメントに書かれてるようにサンプルを書いてみました。

ASP.NET - Release: ASP.NET MVC v1.0 Source

現時点で”Using the AsyncController.docx”はあんまり人気無いみたいね...。ダウンロード数の桁が違う。

async

ドキュメントから勝手に引用。ようするに非同期ハンドラなんですが、いろいろ(アクションだけじゃなくて非同期で大丈夫なように)とあるようで結構なコード量での対応になってます。

非同期にするにあたり、ルーティングの登録が通常の物と違います。非同期ハンドラを使わないとダメなので、MapRouteではなくMapAsyncRouteでルーティング登録。とりあえず、Futuresのアセンブリ参照とMicrosoft.Web.Mvcのusingは忘れずに。

続いて、非同期化したいコントローラのベースクラスをControllerからAsyncControllerに変更しましょう。AsyncManagerやActionInvokerなんかがあるし。ここまでは決まり事です。

非同期アクションを実装する場合、何パターンかあります。ドキュメントを上から見ていくと以下のようなパターンが書かれてます。

  • IAsyncResultパターン
  • イベントパターン
  • デリゲートパターン

それぞれ書き方も違うので一つずつ見ていきます。画像のURLを渡して、そのバイナリを取得し、そのままレスポンスするというものをサンプルに書いてみます。

まずはIAsyncResult。これはアクションが呼び出される際に、アクション内で使用する引数の他にAsyncCallbackとobjectが渡されて来ます。"IAsyncResult を使用した非同期メソッドの呼び出し"これの一番うえのパターンでしょう。End~は勝手に呼び出してくれます。

    public IAsyncResult BeginLoad(string url, AsyncCallback cb, object state)
   {
     var req = WebRequest.Create(url);
     req.Method = "get";
     return req.BeginGetResponse(cb, req);
   }

   public ActionResult EndLoad(IAsyncResult ar)
   {
     WebRequest req = ar.AsyncState as WebRequest;
     var res = req.GetResponse();
     return new FileStreamResult(res.GetResponseStream(), res.ContentType);
   }
こんな感じでしょうか。時間のかかる処理向けというよりは、Begin/Endの組み合わせでIAsyncResultを返す非同期機能を呼び出すときに使うものですね。

次にイベントパターン。最初に呼び出されるアクションの戻り値はvoidにして置いて、引数も必要なものだけを定義。完了時に呼び出されるコールバックのサフィックスにCompletedをつけるルールる、そのコールバックがActionResultを返します。ここでAsyncManagerが登場します。AsyncManager.Parametersディクショナリにコールバックに渡される引数名と同じキーで値を入れておくと、勝手に引数に渡してくれます。コールバックがいつ呼び出されるのかというとAsyncManager.OutstandingOperations.Incrementした値がDecrementで0になったときみたいです。なので、複数の時間のかかる処理を平行して実行させるとき、それぞれの処理をQueueUserWorkItemに登録。同じ数だけインクリメントをしておくけど、処理終了時点でデクリメント。すべてのQueue内の処理が完了するとカウンタが0になるのでコールバックが呼ばれる流れですね。コールバックにはサフィックスとしてCompletedをアクション名につけたものを登録する掟です。

    public void LoadE1(string url)
   {
     AsyncManager.OutstandingOperations.Increment();
     ThreadPool.QueueUserWorkItem(w => {
       var req = WebRequest.Create(url);
       req.Method = "get";
       var res = req.GetResponse();
       AsyncManager.Parameters["stream"] = res.GetResponseStream();
       AsyncManager.Parameters["contentType"] = res.ContentType;

       AsyncManager.OutstandingOperations.Decrement();
     }, null);
   }

   public ActionResult LoadE1Completed(Stream stream, string contentType)
   {
     return new FileStreamResult(stream, contentType);
   }

こんな感じで動いてます。長時間の処理でIAsyncResultじゃない場合にはこれがいいかと。でも正直AsyncManager.Parametersに値を入れるのがこのタイミングでいいのかは自身ないです。Futuresのソース見れば分かると思うけど...。

同じくイベントパターンですが、今度はAsyncManager.RegisterTaskを使って実行したい処理を登録しておく方法です。これはIAsyncResultパターンとのコンビネーションみたいな感じです。ただし自分でインクリメントやデクリメントして処理数を管理しないです。RegisterTaskの2個目の引数に渡すendDelegateが完了したら勝手にカウンタを管理してCompletedを呼び出してくれる感じです。これも複数タスクを登録可能。

    public void LoadE2(string url)
   {
     var req = WebRequest.Create(url);
     req.Method = "get";
     AsyncManager.RegisterTask(
         callback => req.BeginGetResponse(callback,null),
         ar => {
           var res = req.EndGetResponse(ar);

           AsyncManager.Parameters["stream"] = res.GetResponseStream();
           AsyncManager.Parameters["contentType"] = res.ContentType;
         });
   }

   public ActionResult LoadE2Completed(Stream stream, string contentType)
   {
     return new FileStreamResult(stream, contentType);
   }

これで。

最後がデリゲートパターン。さっきもデリゲートじゃないか!と言われればごもっとも。何がデリゲートかというとアクションの戻り値がデリゲートです。これまでとは少し感じが違って1つのアクション(メソッド)しか定義しません。コールバックはどうするかというと、コールバックをこのアクションの戻り値にしてしまうというのが、デリゲートパターンの名前の由来(ウソです、知りません)。

    public Func<ActionResult> LoadD(string url)
   {
     var req = WebRequest.Create(url);
     WebResponse res = null;
     req.Method = "get";
     AsyncManager.RegisterTask(
         callback => req.BeginGetResponse(callback, null),
         ar => {
           res = req.EndGetResponse(ar);
         });

     return () => {
       return new FileStreamResult(res.GetResponseStream(), res.ContentType);
     };
   }

1つの定義で済むので完結に書けていいですね。これもRegisterTaskを使ってるので、複数登録してもすべてが終わるまでコールバックは呼ばれないですね。

だいたいこんな感じです。

通常のControllerとAsyncControllerを簡単に比べるためのアクションを書いてみます。3秒スリープして画像ファイルをレスポンスするという簡単なものです。

同期版

    public ActionResult Dog()
   {
     Thread.Sleep(3000);
     return File("/Content/dog.jpg", "image/jpeg");
   }

非同期版

    public void Dog()
   {
     AsyncManager.OutstandingOperations.Increment();
     ThreadPool.QueueUserWorkItem(w => {
       Thread.Sleep(3000);
       AsyncManager.Parameters["path"] = "/Content/dog.jpg";
       AsyncManager.Parameters["contentType"] = "image/jpeg";
       AsyncManager.OutstandingOperations.Decrement();
     }, null);
   }

   public ActionResult DogCompleted(string path, string contentType)
   {
     return File(path, contentType);
   }

実行するとどっちも3秒たってから画像が表示されます。

async2

前回同様Measure-CommandとTinyGetでパフォーマンスを測定。

PS C:\Program Files\IIS Resources\TinyGet>  Measure-Command {.\tinyget -srv:localhost
-r:27721 -uri:/Home/Dog -threads:50 -loop:1}

Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 10
Milliseconds      : 165
Ticks             : 101652219
TotalDays         : 0.00011765303125
TotalHours        : 0.00282367275
TotalMinutes      : 0.169420365
TotalSeconds      : 10.1652219
TotalMilliseconds : 10165.2219

PS C:\Program Files\IIS Resources\TinyGet> Measure-Command {.\tinyget -srv:localhost
-r:27721 -uri:/Async/Dog -threads:50 -loop:1}

Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 7
Milliseconds      : 314
Ticks             : 73144834
TotalDays         : 8.46583726851852E-05
TotalHours        : 0.00203180094444444
TotalMinutes      : 0.121908056666667
TotalSeconds      : 7.3144834
TotalMilliseconds : 7314.4834

気持ちだね...。サンプルが良くないから。 早くするというよりも、サーバーのコネクションを素早く解放して同時実効性能を上げるのが目的なんだから早さ見てもしょうがないですよね。

今回登場してもらったわんちゃんは"SPOILED-ROTTEN-TO-THE-CORE dog on Flickr - Photo Sharing!"です。

> UpdateModel で、コレクションであるプロパティの各要素の、一部のプロパティだけ更新する

Developer @ ADJUST : ASP.NET MVC - UpdateModel で、コレクションであるプロパティの各要素の、一部のプロパティだけ更新する

コメント欄に書こうと思ったんですが、サンプルが長すぎたのでこっちに書きます。

おっしゃるとおり、復元したいクラスがリストじゃない場合は簡単に制限をかけられるんですが、コレクションのインスタンスに対しての制限は少し面倒ですね。

復元したいインスタンスの項目を制限する場合、UpdateModelを使うなら

  • IncludeやExcludeを指定
  • インターフェースを指定
  • Form復元用のベースクラスを指定

の3パターンあると思います。

が、Listの場合、Listそのもののインスタンスを生成するところから始まるのでUpdateModel<List<interface>>(…)とは書けないですね。インスタンスが作れないので。かといってUpdateModel<List<FormBase>>(…)と書くと、すべてが復元されないので一発で欲しいインスタンスが生成できません。

Listに関する部分に限りこういう使い勝手になってしまうので、アイテム毎にUpdateModelを実行するのが簡単な解決策になります(たぶん)。

ところで、一般的にこういう形式のデータをどのように復元させているのか気になる所ですね。いろいろなコードを眺めるとLINQ to SQLなんかのオブジェクトを使ってる場合は、必要な項目だけ復元させてあとはオリジナルのインスタンスにAttachという形式が多いと思います。オリジナルのインスタンスに直接復元させないところがミソかなと。

一旦復元用のインスタンスに値を入れて、それをオリジナルにコピー(Attach)してしまうのがもう一つの手段として考えられるのではないかなと思う次第です。

  public class ViewModelBase<T>
 {
   public object Attach<T>(object target)
   {
     foreach (var iprop in typeof(T).GetProperties())
     {
       var srcProp = target.GetType().GetProperty(iprop.Name);
       object val = srcProp.GetValue(target, null);
       if (val!=null)
         this.GetType()
             .GetProperty(srcProp.Name)
             .SetValue(this, val, null);
     }

     return this;
   }
 }

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

 public class Person : ViewModelBase<IPerson>
 {
   public int? Age { get; set; }
   public string Name { get; set; }
 }

 public class Team
 {
   public List<Person> People { get; set; }
 }
こういうモデルがあるとして(ちょっとダサイですが)。コントローラに↓こういう処理を書くようなイメージです。
    Team LoadTeam()
   {
     return new Team { People = new List<Person>{
         new Person{ Name="Boo",Age=11},
         new Person{ Name="Bar",Age=21},
         new Person{ Name="Foo",Age=43}
       }
     };
   }

   public ActionResult Index()
   {
     var team = LoadTeam();

     return View(team);
   }

   [AcceptVerbs(HttpVerbs.Post)]
   public ActionResult Index(List<Person> people)
   {
     var team = LoadTeam();
     for (int idx = 0; idx < team.People.Count; idx++)
       team.People[idx].Attach<IPerson>(people[idx]);

     return View(team);
   }
オリジナルのTeamにpeopleのアイテムを埋め込んでいく感じです。実際にはIDかなにかでAttachしたいインスタンスとされるインスタンスを結びつける(この例だと単純にインデックスで結びつけてます)ことにナルト思います。

いかがでしょうか?

2009年4月2日木曜日

FancyBox

スクリーンショットや写真なんかをブログに貼り付けること多いです。特に最近はWindows Live Writerで投稿することがほとんどなんだけど、画像の挿入機能を使うと自動で小さなサイズの画像の生成と大きいサイズの画像へのリンクが貼り付けらて、楽ちんだなと思ってました。

<a href="{大きい画像のURL}">
<img style="border: 0px none; display: inline;"
title="{ファイル名}" alt="{ファイル名}"
src="{小さい画像のURL}" border="0" height="{高さ}" width="{幅}" /></a> 
こんな感じのタグが挿入されますね。でも、単純にアンカーでリンクしてるだけなので表示のされ方がちょっと素っ気ない。というか画像だけが表示される。

と、言うわけで、もう少しかっこよく表示させたいので、jQueryを以前ブログで使えるように取り込んでるので、FancyBoxっていうのを使ったかっこよく表示されるようにしてみました。

Fancybox | Fancy lightbox alternative 個人的にはZoomしなくてもちゃんと見れるように、ちゃんと切り抜いて必要な箇所だけにした画像を貼り付けるほうが見やすいとは思うけど、面倒だしね...。

使い方は簡単。Usageに書いてる通り。

  1. scriptタグ書く。
  2. linkタグでcssを取り込む。
  3. aタグで囲まれたimgタグを書く。
  4. エレメントに対してfancybox()を呼ぶ。

これだけ。scriptにはjQueryを含んでないとダメだけど、これはGoogle Ajax Libraryを参照。その他のファイル群をどこに置くのか悩んだけど、結局Google Page Creatorにアップ。もう新規アカウントを作成できないから、アカウント持ってない人は違うスペースを用意しないとね。一般的にはどうするんだろ?SkyDrive?

ページのローディングが終わったところで、fancyboxを呼び出してみる。

$(function(){
$("a[href*=ggpht.com]").fancybox();
});

なにも表示されない...。悲しい。これでいいはずなのに...。大きい画像のURLが間違ってるのかなと思ったけど、そんなこともなくブラウザ上ではちゃんと表示される。なんでだろうと大きい画像をダウンロードしてローカルで確認しようと思ってaタグのリンク先を保存したら、理由が分かった。aタグのhrefに入ってるURLは画像そのものを指してるんじゃなくて、画像を表示するHTMLを指してた。常識?

例えば↓この画像。

Boston City Flow

生成されるHTMLは

<a href="http://lh5.ggpht.com/_3YQxskhNNyI/SdQ4ep_eVJI/AAAAAAAABNM/3hRpwvyUMV4/s1600-h/Boston%20City%20Flow%5B3%5D.jpg">
<img style="border-width: 0px; display: inline;" title="Boston City Flow" alt="Boston City Flow" src=http://lh6.ggpht.com/_3YQxskhNNyI/SdQ4fLPxrfI/AAAAAAAABNQ/pbG3lWDojxQ/Boston%20City%20Flow_thumb%5B1%5D.jpg?imgmax=800”
   border="0" height="160" width="240" />
</a>
こうなる(Picasaだからね)。このアンカーのリンク先はHTMLで画像じゃないんだけど、中身は↓。
<html>
<head>
<title>Boston City Flow[3].jpg (image)</title>
<script type="text/javascript">
<!--
if (top.location != self.location) top.location = self.location;
// -->
</script>
</head>
<body bgcolor="#ffffff" text="#000000">
<img src="http://lh5.ggpht.com/_3YQxskhNNyI/SdQ4ep_eVJI/AAAAAAAABNM/3hRpwvyUMV4/s1600/Boston+City+Flow%5B3%5D.jpg" alt="[Boston City Flow[3].jpg]" border=0>
</body>
</html>

アンカーとのURLの違いはHTMLが/s1600-h/で、画像が/s1600/。なのでこれをfancybox適用前に書き換えてしまいましょう。

$(function(){
$("a[href*=ggpht.com]").each(function(idx,elem){
  elem.href = elem.href.replace('/s1600-h/','/s1600/');
  $(elem).fancybox();
});
});

全然、他のパターンを考慮してないけど、いいよね。これで、画像表示も見やすくなりました。

Costa Rican Frog metalking

2009年4月1日水曜日

EC2のWindows上で日本語Webアプリケーションを動かす

ここ最近、ずっとAmazon AWSでの環境構築をしてて、運用環境の変更を行ってました。クラシックASPや、ASP.NET 1.1、2.0で動かしているものをEC2に移行してしまおうという魂胆です。ハードウェアも古くなりすぎてて、にっちもさっちも行かない感じだったのもあり、いっそのことクラウドへ。Azureにいければ一番良かったけど、用途も合わないし、そもそもがまだ無理だしね。

Amazon EC2でWindowsサーバーを動かすだけなら、前に一度チャレンジしてるし今ならいろんな所でエントリもされてるから検索すれば沢山見つかると思います。もちろん日本語で。実際の運用環境を作ってみるにあたりデプロイ含めて少し。自分が忘れないためのメモエントリです。作業はElasticfoxとS3 Organizer、AWS Management Consoleですべて完結。自動化するなら"Amazon Web Services Developer Community : C# Library for Amazon EC2"を使ってコード書いて対応。今回は2台しかインスタンス立ち上げないので使わないです。

まずはどのインスタンスを使うか。x86(32ビット)かx64(64ビット)が大きな分かれ目。x86ならStandard Smallか、High-CPU Mediumの2択。x64ならStandard Large/Extra Largeか、High-CPU Extra Largeの3択。それぞれのVMスペックは"Amazon EC2 Instance Types"に全部書いてるの詳細はそちらで確認。それぞれのインスタンスにSQL Server 2005か同Expressが入ってるイメージがあるけど、ぶっちゃけStandard Smallでは使い物にならない。SQL Serverを使うならx64しか選べないけど、同Expressを使うならx86でもいいので、High-CPU Mediumが一番安く現実的な選択肢。そうなるとSQLServer AgentがないのでWindowsのタスク機能を使ってバックアップとることになるので、ちょっと作業が増える。それよりなによりライセンス的に大丈夫なのかがよく分からない。ウェブアプリケーションのストレージとしてExpressっていいんでしたっけ?Datacenter Editionだとなんかライセンス違うのかな。ちなみにAuthentication Serviceは今回使ってないのでよく分かりません。インスタンスの料金については"Amazon Elastic Compute Cloud (EC2) Running Microsoft Windows Server and SQL Server"ここで。なにげにSQLServerを使いたいなら最低$1.10必要だから1日$26.4が最低価格になるんだよね。1アプリケーション1DBサーバーなんて贅沢な使い方をしようとすると、それだけで月に$800くらいかかっちゃうから気をつけないとね。

AMIも最初は名前見ただけじゃ何がインストールされるのかよく分からなかったけど、何度もやってると普通の名前に思えてくるから恐ろしい。

ec2_1

OwnerがAmazonになってるWindows AMIはこれで全部。Authenticationの有無(WinAuthと付いてるか付いてないか)の組み合わせが5種類なので全10種類のみ。”i386”が32ビットで”x84-64”が64ビット。SQLServer入り(SqlSvrStd)が2種類で、同Express(SqlSvrExp)入りが4種類。どっちも入って無いのが4種類。
...分かりやすいじゃないか。

これらとインスタンスタイプの組み合わせだけなので、慣れればなんてことないね。

で、日本語化。当然、すべて英語版なのでそのままだと日本語が表示されない(インスタンス上で)ばかりか、各種書式フォーマット(ロケール)も変えないと、アプリケーションで変な表示になってしまう。内部的にも文字列から復元出来なかったりするところが出るかもしてないし。

と、言うわけで"Amazon Web Services Developer Community : Configuring Windows Components on Amazon EC2"に書いてるスナップショットIDをもとにしたVolumeを作成してドライブを割り当てておいてから、コントロールパネルの"Regional and Language Options"で設定を変えること。

ec2_2

ec2_3

ec2_4

それと、Time Zoneも変えた方がいいかも。

とりあえずこれで、サーバー上で日本語が使えるようになるし、書式化フォーマットも見慣れた物になるけど、ASP.NETアプリケーションでの書式が変。ここの設定が反映されてない。おかしいな~、と思って調べて見たら"globalization 要素 (ASP.NET 設定スキーマ)"でカルチャー設定するのが正しい対応でした。てへ。

DBも言語や照合順序の設定が日本語向けのものじゃないから、その辺の設定も忘れずに。だいたいこんな感じでOKなんじゃないでしょうか(ちょいちょいコントロールパネルの設定が英語に戻ってるのは、なにがきっかけなんだろう)。

2009年3月29日日曜日

NerdDinnerでYSlow

TEERA 2.0 » YSlow web page optimization for ASP.NET MVC

上記サイトでは一般的な話になってるけど、せっかくなのでNerdDinner(NerdDinner.com - Where Geeks Eat - Home)がどこまで最適化できるのか試してみようと思います(ビデオ見疲れたし)。

まずは現状↓こんな感じです。

nd1

nd2

※クリックで拡大。

見ての通り、あまり最適化の余地がない...。localhostからの取得自体が少なすぎ。Site.cssとHome/IndexのHTMLと、jQueryとMap.js。あとはVirtualEarthのサーバーから取得。

トータルGradeは60ポイントでD判定。

うぬ~。マッシュアップ恐るべし。

あまりいいサンプルじゃないのは分かってるけど、少し最適化。最適化手法そのものはどんなサイトでも通用する物なので。今回どこを最適化するかというとCSSとJavaScript。画像もないし、ETagもVirtualEarthがらみだし、CDNも勘弁。NerdDinnerサンプルではCSSは1個しかないけど圧縮して、キャッシュが効くように。同じくJavaScriptも圧縮してキャッシュが効くようにするけど、さらに動的に1つのファイルにしてしまう。今回はHome/Indexしかいじらないですが、Viewに埋め込まれてるJavaScriptも外だしにしてしまいます。

ようするに以前の投稿の続きです。スタティックハンドラはそのまま使います。JSONPが無いのでそこもスルー。

圧縮と縮小化、連結、キャッシュヘッダのコントロールを行うのに新しいコントローラを作成します。コントローラのアクションの結果が圧縮されたCSSかJavaScriptになるようにします。わざわざStaticFileHandlerじゃないのは複数ファイルを連結させたいからです。

まずは圧縮させるためのActionFilterを定義。

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

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

namespace ClientTagHelpers
{
public class CompressAttribute : ActionFilterAttribute
{
public override void  OnActionExecuting(ActionExecutingContext filterContext)
{
 // base.OnResultExecuted(filterContext);

 var request = filterContext.HttpContext.Request;

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

 if (string.IsNullOrEmpty(acceptEncoding)) return;

 acceptEncoding = acceptEncoding.ToUpperInvariant();

 var 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);
 }

}
}
}
なんてことないです。Response.Filterに圧縮用のストリームを指定するだけ。後はよしなにはからってくれます。使い方はこれをコントローラのアクションに指定するだけです。続いてコントローラ。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Mvc.Ajax;
using System.Text;
using System.IO;
using System.Web.UI;

using System.Web.Caching;
using ClientTagHelpers;

namespace NerdDinner.Controllers
{
public class UtilityController : Controller
{
[Compress, OutputCache(Duration = (30 * 24 * 3600), VaryByParam = "*")]
public ActionResult Compress(string src, string key, string ver)
{
 var mime = key.Equals("css", StringComparison.CurrentCultureIgnoreCase) ?
   "text/css" : "application/x-javascript";
 var path = TagRegisterHelper.GetKeyVirtualPath(key);
 var items = src.Split(',');

 if (items != null && items.Length > 0)
 {
   var cacheKey = src + key + ver;
   var responseText = (string)HttpContext.Cache.Get(cacheKey);
   if (responseText == null)
   {
     var srcText = new StringBuilder();
     foreach (var script in items)
     {
       string fullpath = Server.MapPath(path + script);
       if (System.IO.File.Exists(fullpath))
         srcText.Append(System.IO.File.ReadAllText(fullpath));
     }

     if (key.ToLower() == "css")
     {
       responseText = srcText.ToString();
     }
     else
     {
       var minJs = new StringBuilder();
       using (TextReader tr = new StringReader(srcText.ToString()))
       {
         using (TextWriter tw = new StringWriter(minJs))
         {
           new JavaScriptSupport.JavaScriptMinifier().Minify(tr, tw);
         }
       }
       responseText = minJs.ToString();
     }
     HttpContext.Cache.Insert(cacheKey, responseText);
   }

   return Content(responseText, mime );
 }

 return new EmptyResult();
}
}
}
Utility/Compress?src={カンマ区切りでファイル名}&key={種類}&ver={キャッシュさせるためのバージョン} ↑こんな感じのUrlで使用します。ルーティングを登録すればもう少し綺麗になりますね。気になる方はチャレンジしてみてください。

ここでJavaScriptの縮小化をするのにJavaScriotMinifierと言うのを使ってます。これはSmallSharpTools.Packer - Tracから取得出来ます。自分で書いてもいいです。単純にコメントと空白を削除するだけなので自分でも書けるところですが、ここでは楽します。変数名やファンクション名を最適化とかは無しです。自分でそこまでするならハフマン的なことをすればいいかも。

ファイルを1つに連結して縮小化のステップを毎回処理するのは大変(CPUとIOが)なので、HttpRuntimeのCacheに入れてしまいましょう。キャッシュのクリアは考えない富豪プログラミングで。パラメータで指定しているverには指定したファイル群の中で最大の最終更新日時を整数化したものを渡すように作れば上手く行く寸法です。

OutputCache属性はMVCにそもそも用意されてるものなので、そのまま利用しましょう。ここでは有効期限1ヶ月を指定してます。もっと長くてもいいです。どうせverの値が変われば再生成なのでお構いなし。

サーバー上でメモリに実体をキャッシュし、クライアントでも有効期限を指定してファイルキャッシュさせるのは無駄なように見えますが、違うUAからのアクセスならクライアントキャッシュは入って無いので、その時サーバー上のメモリキャッシュが賢く機能してくれます。その為には両方のキャッシュを上手く使う事が大事。中間にリバースプロキシを入れてそこでキャッシュさせたりすると、アプリケーションサーバーの負荷を下げるのに役立つよね。やったこと無いですが。

後は、Viewで使うヘルパーを。

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

using System.Web.Routing;
using System.Web.Mvc;
using System.Text;
using System.IO;
using System.Web.Script.Serialization;

namespace ClientTagHelpers
{
public static class TagRegisterHelper
{
public static string SourceKey = "_sourceKeys";
public static string ModelKey = "_modelKeys";
public static string UrlKey = "_urlKeys";
static string _formatScript = "<script type=\"text/javascript\" src=\"{0}\"></script>\n";
static string _formatCss = "<link rel=\"stylesheet\" href=\"{0}\" type=\"text/css\" />\n";
static string _pageScriptPath = "~/Views/";

public static string ToJSON(object values)
{
 if (values != null)
 {
   #pragma warning disable 0618
   JavaScriptSerializer serializer = new JavaScriptSerializer();
   return serializer.Serialize(values);
   #pragma warning restore 0618
 }
 else
   return "";
}

public static Dictionary<string, string> PathList = new Dictionary<string, string>()
 {
   {"css","/Content/"},
   {"site","/Scripts/"},
   {"page","/ViewScripts/"},
   {"jsonp",""}
 };

static Dictionary<string, List<object>> GetContextItems(HttpContextBase context, string key)
{
 var items = context.Items[key] as Dictionary<string, List<object>>;
 if (items == null)
   context.Items[key] = items = new Dictionary<string, List<object>>();

 return items;
}

public static string GetKeyVirtualPath(string key)
{
 var path = PathList[key.ToLower()];
 if (key.Equals("page", StringComparison.CurrentCultureIgnoreCase))
   path = _pageScriptPath;

 return path;
}

static DateTime GetLastModify(string basePath, IEnumerable<string> fileNames)
{
 return fileNames.Max(fileName => File.GetLastWriteTime(basePath + fileName.Replace("/", "\\")));
}

// -------------------------------------

public static void RegisterViewScripts(this HtmlHelper helper)
{
 helper.RegisterViewScripts(null, null);
}

public static void RegisterViewScripts(this HtmlHelper helper, string scriptName)
{
 helper.RegisterViewScripts(scriptName, null);
}

public static void RegisterViewScripts(this HtmlHelper helper, object values)
{
 helper.RegisterViewScripts(null, values);
}

public static void RegisterViewScripts(this HtmlHelper helper, string scriptName, object values)
{
 // ViewPathからScriptファイル名を推測
 if (string.IsNullOrEmpty(scriptName))
 {
   var webFormView = helper.ViewContext.View as WebFormView;
   if (webFormView != null)
   {
     var viewFile = (helper.ViewContext.View as WebFormView).ViewPath;
     scriptName = viewFile.Replace(".aspx", "") + ".js";
   }
 }
 else if (!scriptName.StartsWith(_pageScriptPath))
   scriptName = _pageScriptPath + scriptName;

 // 実体パス
 var filepath = helper.ViewContext.HttpContext.Server.MapPath(scriptName);

 // ファイルがあるならリストに追加
 // ※ベースフォルダを除外したパス
 if (System.IO.File.Exists(filepath))
   helper.RegisterSource("page", scriptName.Replace(_pageScriptPath, ""));

 if(values!=null)
   helper.RegisterJSON(values);
}

public static void RegisterJSON(this HtmlHelper helper, string key, object value)
{
 var items = GetContextItems(helper.ViewContext.HttpContext, ModelKey);

 // ModelDataの場合は同一キーで値を入れようとしてもダメよ。
 // 常に最初に入れた値だけが取り出せます。
 if (!items.Keys.Contains(key))
   items.Add(key, new List<object>());

 items[key].Add(value);
}

public static void RegisterJSON(this HtmlHelper helper, object values)
{
 var modelValues = new RouteValueDictionary(values);

 foreach (var modelValue in modelValues)
   helper.RegisterJSON(modelValue.Key, modelValue.Value);
}

public static void RegisterSource(this System.Web.Mvc.HtmlHelper helper, string key, string fileName)
{
 var items = GetContextItems(helper.ViewContext.HttpContext, SourceKey);
 if (!items.ContainsKey(key))
   items[key] = new List<object>();

 if (fileName.StartsWith("~/"))
   fileName = VirtualPathUtility.ToAbsolute(fileName, helper.ViewContext.HttpContext.Request.ApplicationPath);

 items[key].Add(fileName);
}

// -------------------------------------
public static string RenderModelJSON(this HtmlHelper helper)
{
 var formatJSON = "<script type=\"text/javascript\">\n" +
                  "var pageModel = {0};\n" +
                  "</script>\n";
 string json = "{}";
 var values = GetContextItems(helper.ViewContext.HttpContext, ModelKey);
 if (values != null && values.Count > 0)
 {
   var modelData = values.Select(v=>new {
                             Key = v.Key,
                             Value = v.Value[0]
                         })
                         .ToDictionary(v=>v.Key, v=>v.Value);
   json = ToJSON(modelData);
 }

 return string.Format(formatJSON, json);
}

private static string ScriptTags(this System.Web.Mvc.HtmlHelper helper, string key)
{
 var items = GetContextItems(helper.ViewContext.HttpContext, SourceKey);
 var sb = new StringBuilder();

 if (items.ContainsKey(key))
   foreach (var item in items[key])
     sb.Append(helper.ScriptTag(PathList[key] + item.ToString()));

 return sb.ToString();
}

public static string ScriptTag(this HtmlHelper helper, string source)
{
 return string.Format(_formatScript, source);
}

public static string RenderScriptTags(this HtmlHelper helper)
{
 return helper.RenderScriptTags("site") +
        helper.RenderScriptTags("jsonp") +
        helper.RenderModelJSON() +
        helper.RenderScriptTags("page");
}

public static string RenderScriptTags(this System.Web.Mvc.HtmlHelper helper, string key)
{
 var nonCompress = new[] { /*"page",*/ "jsonp" };
 if (nonCompress.Contains(key.ToLower()))
   return helper.ScriptTags(key);
 else
   return helper.CompressTags(key, _formatScript);
}

public static string RenderCssTags(this System.Web.Mvc.HtmlHelper helper, string key)
{
 return helper.CompressTags(key, _formatCss);
}

public static string RenderCssTags(this System.Web.Mvc.HtmlHelper helper, string[] fileNameList)
{
 var key = "css";
 var items = GetContextItems(helper.ViewContext.HttpContext, SourceKey);
 items[key] = new List<object>();
 foreach (var fileName in fileNameList)
   items[key].Add(fileName);

 return helper.CompressTags(key, _formatCss);
}

private static string CompressTags(this System.Web.Mvc.HtmlHelper helper, string key, string format)
{
 var basePath = GetKeyVirtualPath(key);
 var path = helper.ViewContext.HttpContext.Server.MapPath(basePath);
 var items = GetContextItems(helper.ViewContext.HttpContext, SourceKey);
 if (items.ContainsKey(key))
 {
   var list = GetContextItems(helper.ViewContext.HttpContext, SourceKey)[key].Cast<string>();
   var maxDate = GetLastModify(path, list);
   var ver = maxDate.ToFileTime().ToString();

   string names = list.Select(l => l).Aggregate((s, ss) => s + "," + ss);
   if (!string.IsNullOrEmpty(names))
     return string.Format(
              format,
              string.Format("/Utility/Compress?src={0}&key={1}&ver={2}",
              new[]{
             helper.ViewContext.HttpContext.Server.UrlEncode(names),
             key,
             ver})
            );
 }

 return "";
}
}
}

前回のエントリの中でも説明しましたが、CSS・サイト共通外部JS・ページ固有外部JS・ 無名クラスをJSONで展開の4パターンを上手いこと処理するヘルパーです。それぞれ固定のパスにファイルがあるという前提でタグを出力します。CSSは/Content、サイト共有のJavaScriptは/Scripts、ページ固有のJavaScriptはViewと同じフォルダにViewと同じ名前で作成(/ViewScripts/Home/Index.jsの形式でアクセス)。

Compressアクションに指定するverを生成するために、GetLastModifyがファイルに直接アクセスして最終更新日時を取得してるけど、ここでもHttpRuntimeのCacheをCacheDependencyを上手く利用してしまえば、実体へのアクセス(IO)はなくせるので、さらに改良の余地あり。

Viewも変更します。まずはSite.Master。
<%@ Master Language="C#" Inherits="System.Web.Mvc.ViewMasterPage" %>

<!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 id="Head1" runat="server">
<title><asp:ContentPlaceHolder ID="TitleContent" runat="server" /></title>
<% = Html.RenderCssTags(new[] { "site.css" }) %>
<meta content="Nerd, Dinner, Geek, Luncheon, Dweeb, Breakfast, Technology, Bar, Beer, Wonk" name="keywords" />
<meta name="description" content="Host and promote your own Nerd Dinner free!" />
<% Html.RegisterSource("site", "jquery-1.2.6.js"); %>
</head>

<body>
<!-- 省略 -->
<%= Html.RenderScriptTags() %>
</body>
</html>

ヘッダでCSSをlinkタグで書いていたのをHtml.RenderCssTagsに変更。引数は文字列配列でファイル名を並べてください。いくつ書いてもOKです。すべてを1つのファイルに連結して、圧縮したレスポンスを返すようにUtility/Compressを参照するlinkタグを生成します。同じくjQueryのscriptタグをHtml.RegisterScriptTagsに変更。最初の引数"site"はサイト共有を表します。閉じbodyの直前にあるHtml.RenderScriptTagsでRegisterしているすべてのスクリプト(サイト共有、ページ固有、無名クラス3つとも)を展開します。こうすればページの最後ですべてのスクリプトを展開出来るようになって、YSlow的にも高評価。

今回はHome/Indexしか最適化しない(全部は面倒)ので、Home/IndexのViewを少し変更します。

<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %>

<asp:Content ID="Title" ContentPlaceHolderID="TitleContent" runat="server">
Find a Dinner
</asp:Content>

<asp:Content ID="indexContent" ContentPlaceHolderID="MainContent" runat="server">

<script src="http://dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6.2" type="text/javascript"></script>

<h2>Find a Dinner</h2>

<div id="mapDivLeft">

<div id="searchBox">
   Enter your location: <%= Html.TextBox("Location") %> or <%= Html.ActionLink("View All Upcoming Dinners", "Index", "Dinners") %>.
   <input id="search" type="submit" value="Search" />
</div>

<div id="theMap">
</div>

</div>

<div id="mapDivRight">
<div id="dinnerList"></div>
</div>

<% Html.RegisterSource("site", "Map.js");%>
<% Html.RegisterViewScripts();%>

</asp:Content>

ここでマッシュアップするためにVirtualEarthのサーバーからスクリプトを取得するようになってますね。これに関してもちゃんとページの最後にタグ出力しなきゃいけないところですが、そこまでは作ってないので、これまた興味のある人はRegisterOuterScripts的な関数を作ってみてSite.MasterのRenderで一括出力できるようにしてみてはどうでしょう。今回そこは作ってないので、直接ここに残したままにしときます。サイトで使うVirtualEarth用の関数群を保持してるMap.jsは、本来サイト共有のスクリプトなのでSite.Masterで一括して指定しておきたいところですが、無駄にすべてのページに反映されるのも良くないので、ViewでRegisterします。こうすると、サイト共有のスクリプトキャッシュが2種類作成されることになります。1つはjQuery+Mapと、もう一つはjQueryのみのキャッシュ。それほど迷惑な話でもないので、これでもいいんじゃないかなと思いますがどうですかね。最後のRegisterViewScriptsがページ固有の外部JSを登録してる部分です。Viewのパスから勝手にスクリプト名を判断してます。複数のViewで共有っていうこともあり得る(例えば、データの新規登録と編集では共通のスクリプトを使いたいけど、それはサイト共有じゃない)ので、スクリプト名を指定するオーバーロードもあります。細かいところはソースを見てね。

最後にViewScripts用のルートを登録すれば完成です。前投稿と同じですが再掲(Global.asax)。

          // -----------------------------------------
       // view scripts routing
       routes.MapRoute(
         "ViewScripts",
         "ViewScripts/{*scriptName}",
         null,
         new { scriptName = @"[\w]*\/[.|\w]*\.js" }
       ).RouteHandler = new ClientTagHelpers.ViewScriptRouteHandler();

これで、再度YSlow!

nd3

nd4

※クリックで拡大。

ちゃんとCSSとJavaScriptは圧縮されてファイルサイズも小さく(トータルが425KBから325KBに縮小)なってるけど、全体的には全く効果が無いに等しい...。Grade 67でDのままだし。7ポイントアップはしたけど。

VirtualEarthが有効期限の無いレスポンスを返してるのと、Etagがちゃんと制御されてないのが大きなところ。これ以上はどうしようもないかな~。

もっと、本格的なアプリケーションだとこれでずいぶん最適化される(CompressをHome/IndexアクションにつけるとかもOKだけど認証かかってる物のキャッシュは気をつけてね)し、画像なんかが多い場合も最適化の余地が残されてる可能性大なので。

興味ある人はお試しアレ。

今まで、プロジェクトのソースを直接書いてたけど、ダウンロード出来る方が試しやすいと気がついたので、貼り付けておきます。

そうそう、Prototype of VEToolkit + ASP.NET MVC 1.0 Component Checked InというサイトでVirtualEarthのMVC用ヘルパーを公開中です。Ajax.MapでJavaScript(NerdDinnerのMap.jsの部分)を出力してくれます。外部ファイルにならないけど、パラメータ指定がチェインしててなんかオシャレな感じ。でもこれなら無名クラスでしていして、内部でデフォルト値とマージするスタイルの方がいい気がするね。

        <%-- Create a Map and name it's global JavaScript variable "myMap" so it can be referenced from your own JavaScript code --%>
     <%=Ajax.Map("myMap")
         .SetCssClass("MapWithBorder")
         .SetCenter(new Location(44, -78))
         .SetZoom(6)
         .SetMapStyle(MapStyle.Road)
         .SetOnMapLoaded("MapLoaded")%>

↑こうやって使うんだけど、↓こうの方が分かりやすくないですか?

        <%=Ajax.Map("myMap")
         .Set(new {
           CssClass = "MapWithBorder",
           Center = new Location(44, -78),
           Zoom = 6,
           MapStyle = MapStyle.Road,
           OnMapLoaded = "MapLoaded"
         }) %>
         

この辺は好みだから、自分で作っちゃえばいいんだけどね!ちなみにソースを確認してて、面白いEnumヘルパー発見。EnumExtensionsクラスでEnumに拡張メソッドを指定してるんだけど、Enumに属性でJavaScriptに出力するときの文字列を指定しておくというもの。例えば↓。

    public enum MapMode : int
 {
     /// <summary>
     /// Displays the map in the traditional two dimensions
     /// </summary>
     [JavaScriptValue("VEMapMode.Mode2D")]
     Mode2D = 0,
     /// <summary>
     /// Loads the Virtual Earth 3D control, displays the map in three dimensions, and displays the 3D navigation control
     /// </summary>
     [JavaScriptValue("VEMapMode.Mode3D")]
     Mode3D = 1
 }

こうやって、Enumの属性に指定しておいて、出力するときにToJavaScriptValueの結果をレンダリング。賢い~!!

dotnetConf2015 Japan

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