2009年4月4日土曜日

> 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の結果をレンダリング。賢い~!!

2009年3月27日金曜日

DomainServiceが気になる

誰も彼もが気になってしかたがない.NET RIA Services。そうでもない?その中でも特に気になるのがデータ操作をラッピングしてサーバーサイドとのプロキシ動作を司るDomainServiceからはしばらく目が離せそうに無いですよね。得意のなんの脈絡もない展開ですいませんね。

.NET RIA Services - Building Data-Driven Applications with Microsoft Silverlight and Microsoft ASP.NET - MIX Videos

Microsoft ASP.NET 4.0 Data Access: Patterns for Success with Web Forms - MIX Videos

あれですよ、とにかく上記2つのMIX09セッションを見るべし、ですよ。英語だけど気にすんなよな!

ビデオ見た感じSilverlight3が目立ってる感じするかもしれないけど、そこじゃねっす。まじっす。気になるのはデータアクセス部分っす。Silverlight3からもASP.NETからも、はたまたJavaScript(これはちょっと特殊だけど)からも同一のサーバーサービスを呼び出してるよね。サーバーサービスを呼び出してるというか、サーバーサービス定義を元にジェネレートしたクライアント用クラスを使ってるよね。中身はサーバーサービスの呼び出しと、データ操作のトラッキングだろうと。サーバーサイド実装が何を使ってるのか気になる。ヒントはJavaScriptから呼び出してるデモの所で、DataService.axdがURIに含まれてるから、基本HttpHandler内に組み込まれてる機能なんだろうとは思うけど、それがWCFなのか独自実装(だとしたらパネーっす)なのか。Windows 7じゃないんだからWindows 7: Web Services in Native Code | pdc2008 | Channel 9ってことはないでしょうが。そんなこんなで"絶対読めよ"と↓こんな資料も。

Public Sector Developer Weblog : A MUST READ: Microsoft .NET RIA Services Overview

しょうがないからダウンロードして読んでみる。がんばる。読むというか見る...。

前半40ページくらいまで、こういう感じよ、こうやって書くよ、こんな感じで作るのよ、な説明が続くので、ビデオ見たのでそこはサラッと流す。EnableClientAccess属性が付いてるとクライアントコードをジェネレートするって書いてるけど、ジェネレートしなくてもサーバー上でだけでも使うならそれもよし。

こっからが、少し気になる部分の説明に入っていく。CRUD操作のR部分。デモでは何気なくGet~で書いたものをプロキシクラス呼び出すクライアントではLoad~って書いてロードしてるよね。ItemsSourceのところはコレクションプロパティ(this.Entities.GetEntityList<T>()って何が返るのかな~)を指定してるし。どういう風な関連付けがされてるのか気になるっす。空のコレクションを返しといて、あとはObserveに任せるのかな。Get~の部分はプレフィックスが「“Get”, “Fetch”, “Find”, “Query”, “Retrieve”, or “Select”」なら何でもいいみたいね。というか、Query属性をつけるなら、それすら関係無い。CoCなり。読み込みクエリーはLoad~が非同期で実行。

更新系もデモだとInsert/Update/Deleteがプレフィックスに付いてるけど、ここも何種類か規約でプレフィックスが用意されてる。データ取得と同じく、属性ベースでも指定可(Update/Insert/Delete属性)。クライアントでの更新結果はEntities.GetChanges()でEntityChangeSetを取得することですべて抽出できる。たぶんLINQ to SQLのDataContext.GetChangeSet()と同じようなものなんだろう。SubmitChanges()で更新情報をサーバーに送信して確定させるか、RejectChanges()で破棄。サーバー上では6つのパイプライン(クライアントから受信したもの→認可→検証→実行→永続化→同時実行エラー)に更新セットを流す(?)。トランザクション制御はやらないので、必要なら自分でオーバーライドしてTransactionScope使うべし。

Custom属性をつけたメソッド内での更新はクライアントのコンテキスト上で実行されるみたいで、ちゃんとクライアントてSubmitChangesしないとサーバーには反映されない。プロキシクラス内にそのまんまメソッドが展開されるんでしょうね。更新を伴わない場合はCustom属性じゃなくてServiceOperation属性。どっちも非同期みたい。

フィールドレベルの検証はDataAnnotationsを使ってMetadataクラスに定義。この辺はだいたいみんな同じような作り方になるのね。独自検証の場合はShared属性をつけたクラスで実装しとけば、それも自動生成に含めてくれるから、単純な検証以上の事をしたい場合はこれで。ビジネスロジックとしての検証はどこに書くのかな~?モデルクラスなのかな~?

後は認証や、認可の属性と続いてSilverlight、ASP.NET内で使うDomainDataSourceコントロールの説明と続く。ページングやらフィルタの指定もXAML内でできるのがカッコイイ。

後は、サンプルコードみたりしないとちょっとよく分からないけど、ビデオでは紹介されてない実装の細かいところはこのドキュメントにある程度書かれてる感じです。でも、もっと知りたいし、出来れば.NET RIA Services(Silverlight3環境必須)を入れなくてもDomainServiceだけでも試したいな~。コードサンプルが小野さんに教えてもらったASP.NET Dynamic Data 4.0 Preview 3にあるので、そっちを眺めてみようと思います。

2009年3月22日日曜日

初めてのAzure Storage

そういえばAzureのInviteが来てから、なんにも遊んでないことを思い出した。手を動かしてみよう。

Building Web Applications with Windows Azure - MIX Videos

ちょうどいい具合のビデオ。Azureがどういうものかは分かってるっていうことでそこは省略。ビデオの通りのデモを作るってだけの単純なエントリ。だけど、どこで何をしなきゃいけないのかは分かるようになると思われる。

まずは、Visual StudioでWebRoleプロジェクトを作成。これで8割完成。もちろん最新のSDK&Toolsを入れて無いとダメっす。

Download details: Windows Azure SDK Download details: Windows Azure Tools for Microsoft Visual Studio March 2009 CTP

Azure StorageとSDSと2種類のストレージサービスがあるけど、SDSは今回関係無いのであしからず。SDKを入れるとローカルでAzureプロジェクトが動くようにDevelopment Fabric(ローカル実行環境)とDevelopment Storage(ローカルストレージ)がインストールされます。今回はWeb実行環境はローカルなんだけど、ストレージはローカルストレージを使わずにAzure Storageを直接使うという内容なので、Development Storageは停止状態でOKです。

Azure Service Developer Portal

Azureのポータルでまずはストレージを作成。とりあえず初めてなので"FirstWeb"という名前にしてみた。

azurestorage1

後はビデオ通り。と、ここでエントリが終わってもつまんないから、とりあえずは手順を。

csdefにConfigurationSettingsを追加。

<?xml version="1.0" encoding="utf-8"?>
<ServiceDefinition name="CloudService2" xmlns="http://schemas.microsoft.com/ServiceHosting/2008/10/ServiceDefinition">
<WebRole name="WebRole">
  <InputEndpoints>
    <!-- Must use port 80 for http and port 443 for https when running in the cloud -->
    <InputEndpoint name="HttpIn" protocol="http" port="80" />
  </InputEndpoints>
  <ConfigurationSettings>
    <Setting name="AccountName" />
    <Setting name="AccountSharedKey" />
    <Setting name="TableStorageEndpoint" />
  </ConfigurationSettings>
</WebRole>
</ServiceDefinition>
これに合わせるように、cscfgにはvalueをセットしたものを書き込みます。もう、ここまでで9割完成。

次にモデルクラスを作成。エンティティ自体はPartition keyとRow keyさえあれば、あとはどんなスキームでもいいのでサンプル通りのメッセージクラスを作成。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Microsoft.Samples.ServiceHosting.StorageClient;
using System.Data.Services.Client;

namespace CloudService2_WebRole
{
public class ShortMessage : TableStorageEntity
{
  public string Message {get;set;}

  public ShortMessage(){}
  public ShortMessage(string pk, string rk, string message):base(pk,rk)
  {
    Message = message;
  }
}

public class MessageContext : TableStorageDataServiceContext
{
  public DataServiceQuery<ShortMessage> MessageTable
  {
    get
    {
      return CreateQuery<ShortMessage>("MessageTable");
    }
  }
}
}

テーブルの行に相当するエンティティはTableStorageEntityクラスから派生させる。自分でPartition KeyとRow Key書けばいらないけど、この実装が一番簡単。ストレージで管理したいテーブルのコンテキストをTableStorageDataServiceContextから派生。ここまではもう、こうする物だと覚えておこう。自分で全部実装してもいいけど、せっかくStorageClientのサンプルライブラリが提供されてるんだからそれを使う。.NET RIA Servicesに付いてくるサンプルはまた違う実装を使ってるので、なんならそっちを使う(Microsoft.Azure.StorageClient)っていうのも面白いかもね。中身はRESTfulな実装になってるから好みの実装で。

Development Storageを使う場合、"Create Test Storage Tables"を実行すればローカルにテーブルが作成されて簡単でいいんだけど、クラウドのAzure Storageを使う場合、クラウド内にちゃんとテーブル定義を作らないとダメ。ビデオではその部分をPythonのスクリプトを書いて実行してるけど、なんせマイマシンにはPythonの実行環境もないので、そこは他のサンプル同様Global.asaxに書いてしまうことにします。

    protected void Application_Start(object sender, EventArgs e)
  {
    try
    {
      var account = StorageAccountInfo.GetDefaultTableStorageAccountFromConfiguration();
      var tableStorage = TableStorage.Create(account);
      if (!tableStorage.DoesTableExist("MessageTable"))
        tableStorage.TryCreateTable("MessageTable");

    }
    catch{}
  }

一回作ってしまえば、クラス定義が変わるまで、存続するはずだからわざわざBeginRequestに書かなくてもいいんじゃないの?と常々思ってたので今回はApplication_Startに書いてます。コレでもちゃんと動くし。9割5分完成。

最後にdefault.aspxを書き換えて終わり。

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="CloudService2_WebRole._Default" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
  <title>ShortMessage</title>
</head>
<body>
  <form id="form1" runat="server">
  <div>
  何か一言<asp:TextBox ID="InputMessage" runat="server"></asp:TextBox>
  <asp:Button ID="SendMessage" runat="server" Text="送信" OnClick="SendMessage_Click" />
  </div>

  <div>
    <asp:Repeater ID="MessageList" runat="server">
      <HeaderTemplate>
      <ul>
      </HeaderTemplate>
      <ItemTemplate>
      <li>メッセージ:<%# Eval("Message") %></li>
      </ItemTemplate>
      <FooterTemplate>
      </ul>
      </FooterTemplate>
    </asp:Repeater>
  </div>
  </form>
</body>
</html>

コードビハインドはこっち。

using System;
using System.Collections;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Xml.Linq;
using Microsoft.ServiceHosting.ServiceRuntime;

namespace CloudService2_WebRole
{
public partial class _Default : System.Web.UI.Page
{
  protected void Page_PreRender(object sender, EventArgs e)
  {
    MessageList.DataSource = (from m in new MessageContext().MessageTable
                              where m.PartitionKey == "FirstWeb"
                              select m);
    MessageList.DataBind();
  }

  protected void SendMessage_Click(object sender, EventArgs e)
  {
    var context = new MessageContext();
    context.AddObject("MessageTable",
      new ShortMessage(
        "FirstWeb",
        Guid.NewGuid().ToString(),
        InputMessage.Text));
    context.SaveChanges();

    InputMessage.Text = "";
  }
}
}

Page_LoadじゃなくてPreRenderにデータバインドを書くのを忘れずに。コレを動かす!

azurestorage2

見にくいけど、ローカルストレージは全部Stopped。WebRoleはローカルのFabricで動いてブラウザ内にはメッセージリストがちゃんと表示されてる。一応、Fiddlerで確認も。

azurestorage3

見にくいけど、2行目が最初の表示のバインドのためのリクエスト。3行目がメッセージのポスト時に実行されるPOST。4行目が、ポストバック後のバインドで取得するリクエスト。

単純なサンプルだけど、Azure Storage単体をAzureにホストしていないプログラムから利用するのが超簡単だというのが分かったので良しとしましょう!

初めてのLive Mesh

Mesh使ってファイル同期なんていうのは、目に見えるアプリケーションとして日常のなかで使う事もあると思うけど、Meshの凄さはそこじゃない、とみんな感じてる模様。

クラウドで保持してるエンティティをMeshデバイス間で自動で同期することの一つの実装としてのファイル共有(“P2P=ファイル共有=悪”と同じ発想にいっちゃうと何が起きてるのか分からなくなるから)。データの持ち方が階層構造になってて、アクセスはすべてAtomPubのRESTful。認証すらRESTful。スゴイね。

ベータとして一般に使えるMeshクライアントは、ファイル同期とリモートデスクトップだけしか遊べないけど、開発者向けのCTPを使うとそこでアプリケーションが開発出来て、カスタム同期アプリケーションを構築出来る。ただ、こっちはファイル同期とリモートデスクトップは使えないから、両方のバージョンを入れないとちょっと不便。この辺についてはいろんなサイトで取り上げてるから割愛。

デベロッパーとしては、開発者向けで少し遊んでみたいと思っちゃうよね。とりあえず利用申請(Connectで)もして、プロジェクトが作れるようになってるところまでは準備が出来てるものとします。

Download details: Live Framework Tools for Microsoft Visual Studio April 2009 CTP

↑ここからSDKとToolsがダウンロード出来るんだけど、これが少し罠を仕掛けられてて、このままだと日本語のVisual Studioにインストール出来ない。英語版じゃないとダメ!って言われてインストーラーが停止する。けど、これは選別トラップと見た。これをクリアしてこそMeshで開発するにふさわしい開発者だというメッセージなんじゃ。んな、わけないっつの。

lft

ダウンロードしたLiveFrameworkTools.exeは自己解凍形式のCABファイルなので、CABが解凍できるツールを使えば簡単に解凍できます。解凍後にLiveFrameworkSDK.msiとLiveFrameworkTools.msiが出てくるから、それを直接インストールすれば、上記メッセージ(言語環境チェックされない)も出てこないので日本語版でも開発することが出来るようになります。もちろん自己責任でってことにはなるけど。CTPだからそもそも自己責任だし(MSがダメだと言うなら諦めよう)。

lfi2

これで、開発出来る状態ができあがったね。

Mesh-Enabled Web Applications - MIX Videos

そしたら↑コレ見ましょう。前半デモがグダグダだけど、そこは無視。内容自体はグッとくる。面白そうでたまらない。なので「初めてのLive Mesh」アプリケーション開発をしてみる。

Silverlightも初めてだし、小粋な物は作れないので、ビデオで最初にデモしてる、ログインしてるユーザー名を表示する部分だけ作ってみることに。簡単だし。

LiveFrameworkSDK&Toolsを入れとけばVisual Studioでテンプレートが出てくるので、"Silverlight Mesh-enabled Web Application"を選択して作成。これで9割5分完成。

次に、XAMLにテキストブロックが最初から張り付いてるので、そこにx:Nameで"HelloMessage"と命名。9割7分。

<UserControl x:Class="MeshApp1Silverlight.Page"
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   Width="400" Height="300">
   <Grid x:Name="LayoutRoot" Background="White">
       <TextBlock x:Name="HelloMessage">Hello from MeshApp1Silverlight</TextBlock>
   </Grid>
</UserControl>
コードビハインドのmeshAppLoadedに以下のコードを書く。
    // Called when application loads
   void meshAppLoaded(object sender, EventArgs e)
   {
     // Mesh application service object is now loaded and usable.
     HelloMessage.Text = "Hello! " + meshApp.LiveOperatingEnvironment.Mesh.ProvisionedUser.Name;
   }
完成。あとは、実行させるのみ。何も開発してないじゃないか!なんてことは言わないで...。

あとはビデオのやり方通りに淡々と決まり事を実施。新規なのでAzure上にLive Frameworkのプロジェクトを作成(Mesh用)。そこでzipファイルをアップロード。

mesh1

開発したもののアップロードはVSが自動でやってくれる(ボーッと待つ)。

mesh2

そしたら、ブラウザが立ち上がって、Live Desktopが表示されてアプリケーションが実行される。

mesh3

もちろんデスクトップにも自動でアイコンが表示されて、同じものがローカルマシンでも動かせます。それを起動。

mesh4

これで開発出来る環境が出来てることは分かったし、ここからデータを追加したりして同期するのを楽しめるコードを追加していく。でも...、ビデオの通りやれば出来るって分かってるんだけど、なんか面倒くさくなってきたので、今日はここまで。

アプリケーション毎にMesh上のどのオブジェクトを参照できるのか設定出来たり、ローカルLOEを経由しない(しないよね?)で直接Meshにデータを登録したり出来るみたいで、普通のWebアプリケーションから認証(Live ID)と認可(OAuthじゃないけどそんな感じのもの)するアプリケーションも作れたり、興味が尽きない。

あとは.NET RIA Servicesが強烈過ぎるのでそっちでも遊んでみたい。でも、Silverlight 3はまだ入れたくない気がしなくもなく。2と3のどっちも同じマシン内に入れて、それぞれ(MeshとRIA)で開発できるのかな。教えて偉い人。

dotnetConf2015 Japan

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