2009年3月22日日曜日

初めての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)で開発できるのかな。教えて偉い人。

2009年3月17日火曜日

ASP.NET MVCでJavaScriptを上手いこと使う

How to iterate through objects in ViewData via javascript on the page/view? - Stack Overflow

この質問の回答としては、ページにscriptタグを書き、その中でJavaScriptを書きつつサーバーサイドのコード(foreachやViewDataの参照)も埋め込んでしまえばいいじゃないか、というもの。

コレまでもさんざん悩んで、ViewEngineの実装に寄り道したり右往左往したけど、結局はグローバルな変数にJavaScriptで使用したいデータをViewDataから取り出して登録しておき、ロジック(JavaScriptで書くクライアントでのっていう意味で)は外部JSファイルに。外部JSファイルからグローバル変数にアクセスすることで、Ajaxによるサーバーからのデータ取得や、サーバーサイドコードの埋め込みを不要にして、純粋なJavaScriptコードのみの外部JSファイルを作成する感じです。

まずJavaScriptの用途は、サイト全体で共通のコード(jQuery.jsやprototype.jsなどのフレームワーク、またはアプリケーション共通機能)と、そのページでしか必要のない機能に分けられる。さらにデータとしてはJSONPでサーバーサイドのデータをクライアントサイドに渡したり、サーバーサイドのデータをクライアントサイドにJSONにシリアライズしてページに直接埋め込む方法がある。もちろんXHRで取得や、DOMそのものをデータとして使う方法など他にもあるけど、今回はその辺はとりあげません。

整理すると処理コードとして

  • サイト共通
  • ページ固有

データコードとして

  • JSONP
  • JSONシリアライズのページ埋め込み

というのを今回のテーマとします。

サイト共通コードは、通常~/Scriptsフォルダに入れておくのがRC以降のテンプレートになってます。ページ固有コードは通常View内に直接scriptタグを書いて、その中に直接書くのがどこのチュートリアルにも書かれてるやり方になってます。が、View内に直接は書かないでViewと同じフォルダに外部JSファイルとして作成する方法をとります。なんでViewと同じフォルダに入れるかというと、スクリプトファイルを管理しやすくしたいからです。もちろん~/Scriptsに入れてもいいんだけど、そうするとViewと同じようなフォルダ構成(~/Content/Home/Index.jsとか)を作ってその中に入れるか、ルール通りのファイル名(Home.Index.jsとか)にして1つのフォルダ内にすべて保持するか。もちろんルール無用でViewとJSとを管理してもいいとは思うけど、簡単に管理しつつ、極力簡単なヘルパーでscriptタグを出力できるようにしたいので、Viewと同じフォルダ構成にしておき、アクション名.jsと命名するのがいいんじゃないかと。ただ、そうすると全く同じフォルダ構成を用意するというちょっとかっこ悪いことになり、スッキリしない!という理由でページ固有コードはViewと同じフォルダに保持するのがいいんじゃないかと思う次第です。

~/Viewsフォルダには通常aspx/ascxのいずれかを入れておく(WebFormViewEngine)んですが、そこに.jsファイルを置いておき、scriptタグでsrc指定するとどうなるかと言うと、そんなアクションありませんと怒られます。

~/Views/Home/Index.jsがあるからといって

<script type=”text/javascript” src=”/Home/Index.js”></script>

と書いてもきちんとダウンロードされないということです。

そりゃ、そうです。規約でそうなってるから。独自のMasterLocationFormatsやViewLocationFormatsを定義したとしても、BuildManager.CreateInstanceFromVirtualPathでJScriptのコンパイラが走ってあえなくエラー(クライアントサイドのコードなのにサーバーサイドのコードとしてコンパイルしようとする)。ViewEngineがらみをどうのこうのするのは、ちょっと面倒。

簡単な解決方法は、ページ固有コードをscriptタグのsrcとして指定する場合にはパスのルートに何かしら固定のフォルダ名を指定するようにして、そういうルーティングを行うようにしてしまう。これなら簡単。ルーティングが出来てもMvcRouteHandlerを使ったんじゃ結局コンパイルしようとするので、そこはファイルを直接返すだけの簡単なRouteHandlerを書いてしまいましょう。ようするにStaticFileHandlerなんだけど...。

IRouteHandlerの実装と、IHttpHandlerの実装。

  public class ViewScriptRouteHandler : IRouteHandler
 {
   public IHttpHandler GetHttpHandler(RequestContext requestContext)
   {
     string scriptName = requestContext.RouteData.GetRequiredString("scriptName");
     string scriptPath = string.Format("~/Views/{0}", scriptName);
     return new ViewScriptFileHandler(scriptPath);
   }
 }

 public class ViewScriptFileHandler : IHttpHandler
 {
   string virtualPath;

   public ViewScriptFileHandler(string path)
   {
     virtualPath = path;
   }

   public bool IsReusable
   {
     get { return true; }
   }

   public void ProcessRequest(HttpContext context)
   {
     var path = context.Server.MapPath(virtualPath);

     context.Response.ContentType = "application/x-javascript";
     context.Response.TransmitFile(path);
   }
 }

これをRouteCollectionに登録します。ページ固有コードなので/ViewScripts/コントローラ名/アクション名.jsというURLでアクセスさせたいと思います。MapRoute拡張メソッドを使った場合、登録したRouteが返ってくるので、それに直接ハンドラをセットするやり方です。

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

たったこれだけ。これだけのコードを書いておけば~/Views/Home/Index.jsを

<script type=”text/javascript” src=”/ViewScripts/Home/Index.js”></script>

として取得できます。あら簡単。

処理コードはとりあえずこれで組み込めるようになったので、こんどはJSONP。これはもう特に難しいことを考えずにコントローラのアクションとして実装してしまい、JavaScriptResultで返すのが一番簡単。

  [OutputCache(Location = OutputCacheLocation.None)]
 public ActionResult JsonpDate()
 {
   string result = "{}";
   try
   {
     result = Utility.ToJSON(new {Today = DateTime.Today.ToString(“yyyy/MM/dd”)});
   }
   catch { }

   return JavaScript(string.Format("var jsonp = {0};",result));
 }

例えば、HomeControllerに↑こんなアクションがあったとしたら

<script type=”text/javascript” src=”/Home/JsonpDate”></script>

と、書く事でjsonp変数に値が入ります。↓こう書いたのと同じ意味になります。

<script type="text/javascript">
var jsonp = {Today: '2009/03/17'};
</script>

ちなみにUtility.ToJSONの中身は単純。

    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 "";
   }

System.Web.Script.SerializationのJavaScriptSerializerを使います。MVCの中でも同じように使ってるから問題無いでしょう。

最後にページ固有のデータをViewDataに入れておき、View内でヘルパーを使ってscriptタグに展開してしまうコードを作成します。使い方のイメージを先に書いてしまうと↓こんな感じです。

  <% = Html.RenderJSON(new {
      Today = DateTime.Today.ToString("yyyy年MM月dd日"),
      AjaxUrl = ViewData["Url"]
    }) %>

ViewData[“Url”]にアクション内で値を入れてるという前提で。

  public static string RenderJSON(this HtmlHelper helper, object values)
 {
   var modelValues = new RouteValueDictionary(values);
   var dict = new Dictionary<string, object>();
  
   foreach (var modelValue in modelValues)
     dict.Add(modelValue.Key, modelValue.Value);
    
   var formatJSON = "<script type=\"text/javascript\">\n" +
                    "var localjson = {0};\n" +
                    "</script>\n";
   string json = "{}";
  
   if (dict.Count > 0)
   {
     var modelData = dict.Select(v=>new {
                               Key = v.Key,
                               Value = v.Value
                           })
                         .ToDictionary(v=>v.Key, v=>v.Value);
     json = Utility.ToJSON(modelData);
   }

   return string.Format(formatJSON, json);
 }

こんな感じでしょうか。↓ViewData["Url"]に"どこそこ"とアクションで入れてればこう展開されます。

<script type="text/javascript">
var localjson = {Today: '2009/03/17', AjaxUrl: 'どこそこ'};
</script>

これで、共通コードは~/Contentなり~/Scriptsから取得、ページ固有はViewと同じフォルダ内にjsファイルとして作成したものを~/ViewScriptsから取得。Jsonpは通常のアクションとして実装し、ページ固有のデータはViewData経由でActionからViewに渡した物をJsonで展開。クライアントサイドでちゃんとevalするべきところではあるけどその辺はご愛敬ってことで。

これだけでも、コードとデータをきちんと分離は出来てるけど、もっと便利に簡単に使うために、スクリプトとデータのRegisterヘルパーと、それらをページの最後でまとめて書き出す、Renderヘルパーなんかを作っておいて、さらに圧縮(Compress)/縮小化(Minify)/連結(Merge)なんかも実装してたりするんですが、それはまた今度。いつの日か。

2009年3月12日木曜日

IDataErrorInfoの危険ゾーン

ASP.NET MVCで入力検証を行う場合、以前の投稿でDataAnnotationsとIDataErrorInfoの組み合わせ最強!なんて言ってましたが、ASP.NET MVC eBook Tutorialを読んでいて、おやや?と思ったわけです。ナゼ、彼らはコレを使ってないんだろうかと。

このチュートリアルの中で実装されている入力検証ではIDataErrorInfoを使わず、ASP.NET MVC Preview 5 and Form Posting Scenarios - ScottGu's Blogで紹介されてるRuleViolationクラスを使う方法なんですよね。どういう検証ロジックになってるのかを公開されてるソースとPDFに書かれてる内容から整理してみます。

LINQ to SQLを使ってモデルを作成してるので、モデルクラスのOnValidateパーシャルメソッドを使って、データ更新時に検証ロジックが実行されるようにしてます。

LINQ to SQL (Part 5 - Binding UI using the ASP:LinqDataSource Control) - ScottGu's Blog

実行した検証結果はRuleViolationクラスのインスタンスとして、プロパティ名とエラーメッセージを保持。GetRuleViolations()で、プロパティ毎の検証を実施。この関数はIEnumerable<RuleViolation>でyield return new RuleViolation(~)の形式で各プロパティの検証結果を返すようにすることでLINQのAny()でエラーの有無をすべてチェックしなくても判定できるってことですね(実際はCount()==0でエラー無しっていうコードになってるけど)。OnValidate()内ではIsValid()を呼んで、エラーがあるなら例外(ApplicationException)を発生させてるんですね。

更新処理自体はRepositoryの中で実装してますが、実質InsertOnSubmit()してるだけ。それをAction内で実行するわけですが、入力エラーがある場合Repository.Save()を呼び出した段階で例外が発生するので、catch内でGetRuleViolations()を呼び出して、エラー詳細を取得。ヘルパーを用意しておき、RuleViolationをModelState.AddModelError経由でModelErrorとして展開。これでViewの処理時に自動でエラークラスもセットされるし、ValidationSummaryの表示なんかもされる。

ここまでで、検証ロジックについて触れてなかったですけど、そこはもうベタな感じで作ってる。String.IsNullOrEmpty()で必須チェックや、正規表現で形式チェック。どこにもDataAnnotationsなんて使ってないです。しかも検証処理の実行をDefaultModelBinderに任せないのでIDataErrorInfoの実装もしていない。

ちょっと長くなったけど、こんな感じです。

昨日のエントリでViewData.Eval()が便利だよ、って話をしました。ここでやっと今回の本題。MVCに用意されてるHtmlHelperのInputExtensions(Html.TextBoxとかの入力用ヘルパー)は、内部でこのViewData.Evalを実行してるんですね。で、そのEvalの中で呼び出されてるのが以下の関数。

  private static object EvalComplexExpression(object indexableObject, string expression) {
     foreach (ExpressionPair expressionPair in GetRightToLeftExpressions(expression)) {
         string subExpression = expressionPair.Left;
         string postExpression = expressionPair.Right;

         object subtarget = GetPropertyValue(indexableObject, subExpression);
         if (subtarget != null) {
             if (String.IsNullOrEmpty(postExpression))
                 return subtarget;

             object potential = EvalComplexExpression(subtarget, postExpression);
             if (potential != null) {
                 return potential;
             }
         }
     }
     return null;
 }

ViewDataDictionary.csの200行から抜粋。プロパティが別のクラスのインスタンスになってても取り出せるように再帰ですね(例えばModel.Address.ZipCodeとか)。ここでGetRightToLeftExpressions()で上手いこと取得したいプロパティをすべて取り出せるんですが、問題はそのプロパティを取り出す関数であるGetPropertyValue()。またしてもソースを引用しておきます。

  private static object GetPropertyValue(object container, string propertyName) {
     // This method handles one "segment" of a complex property expression

     // First, we try to evaluate the property based on its indexer
     object value = GetIndexedPropertyValue(container, propertyName);
     if (value != null) {
         return value;
     }

     // If the indexer didn't return anything useful, continue...

     // If the container is a ViewDataDictionary then treat its Model property
     // as the container instead of the ViewDataDictionary itself.
     ViewDataDictionary vdd = container as ViewDataDictionary;
     if (vdd != null) {
         container = vdd.Model;
     }

     // Second, we try to use PropertyDescriptors and treat the expression as a property name
     PropertyDescriptor descriptor = TypeDescriptor.GetProperties(container).Find(propertyName, true);
     if (descriptor == null) {
         return null;
     }

     return descriptor.GetValue(container);
 }

GetIndexedPropertyValue()ですよ。これが最初に値を取得するために呼び出されるんだけど、これが内部でGetProperty("Item",~)を呼び出すんですよね。

  private static object GetIndexedPropertyValue(object indexableObject, string key) {
     Type indexableType = indexableObject.GetType();

     ViewDataDictionary vdd = indexableObject as ViewDataDictionary;
     if (vdd != null) {
         return vdd[key];
     }

     MethodInfo containsKeyMethod = indexableType.GetMethod("ContainsKey", BindingFlags.Public | BindingFlags.Instance, null, new Type[] { typeof(string) }, null);
     if (containsKeyMethod != null) {
         if (!(bool)containsKeyMethod.Invoke(indexableObject, new object[] { key })) {
             return null;
         }
     }

     PropertyInfo info = indexableType.GetProperty("Item", BindingFlags.Public | BindingFlags.Instance, null, null, new Type[] { typeof(string) }, null);
     if (info != null) {
         return info.GetValue(indexableObject, new object[] { key });
     }

     PropertyInfo objectInfo = indexableType.GetProperty("Item", BindingFlags.Public | BindingFlags.Instance, null, null, new Type[] { typeof(object) }, null);
     if (objectInfo != null) {
         return objectInfo.GetValue(indexableObject, new object[] { key });
     }
     return null;
 }

しつこくソースを引用しますが、ViewDataDicstionaryそのものであれば、直接ディクショナリの値を返そうとしてます。でも、内部ではTryGetValueで一致するものがないからとnullを返します。そうなると、Modelを参照して、プロパティを取得するので、ViewData.Modelにセットされてるインスタンスのプロパティを取得するときはそのままインスタンスのプロパティを返してくることになるので、問題にはなりません。が、しかしですよ、このModelにセットしてるインスタンスが別のクラスのインスタンスを保持してて、それをプロパティとして公開してる場合にはですね、GetIndexedPropertyValue()が"Item"プロパティを参照するようになるんです。この”Item"っていうプロパティはデフォルトインデクサのプロパティ名みたいで、IDataErrorInfoを実装するっていうことはコレを実装することになるので、なんとまぁエラーメッセージが返ってきてInput要素に出力されてしまうんですね。最高に分かりにくい文章ですいません。サンプル書くと長くなりそうなんです...。なので、適当なコードで表現してみます。

public class Person : IDataErrorInfo
{
 [Required(ErrorMessage="名前は?")]
 public string FirstName { get; set; }
 [Required(ErrorMessage="名字は?")]
 public string LastName { get; set; }

 public Person Father { get; set; }
 public Person Mother { get; set; }

 public string Error {
   get { // model validation }
 }

 public string this[string key] {
   get { // property validation }
 }
}

こんな感じのクラスがあったとします(中途半端ですいません) 。これをViewData.ModelにセットしてViewを表示するとしましょう。

public ActionResult Details(int? id)
{
 var person = new Person() { Father = new Person(), Mother = new Person() };

 return View(person);
}

こんな感じでしょうかね。これを表示するとしましょう。そうするとPersonクラスのFatherとMotherプロパティは空のPersonインスタンスが入ってます。でも、PersonクラスはIDataErrorInfoを実装しててプロパティの検証コードが入ってます(DataAnnotationsの検証実行が入ってるとしてください)。そうするとですね、Viewで<%= Html.TextBox(“Father.FirstName”) %>なんて書いてると、ViewData.Eval経由で値を取得しようとするけど、上記理由のためIDataErrorInfoの実装のつもりで書いてるデフォルトインデクサにアクセスされて、エラーメッセージとしての"名前は?"がテキストボックスのvalueに設定されてしまうという罠。しかもコレはテキストボックスのinput要素を作成してるInputHelperの引数useViewDataにtrueをセットして呼び出した時にEvalが実行されるんだけど、プロパティの値がnullならtrueとなる実装。そもそもstringの初期値はnullだから、今回のような場合必ずDataAnnotationsの検証が実行されて、エラーメッセージが表示されてしまいます。はてさて、どうしたものか。初期値として空文字("")をセットするとかしておけばnullじゃないから、デフォルトインデクサにアクセスされないんだけど、なんか気持ち悪し。IDataErrorInfo使わない方がいいんですかね~。う~ん。う~~~~ん。どうすればいいのか。教えて偉い人!

2009年3月11日水曜日

Futuresに含まれるMvc Controls

RC2のFuturesアセンブリ(ソースでも)には、Mvc Controlsが含まれてます。System.Web.UI.Controlクラスの派生クラスとして作成されてるので、WebFormsで使うサーバーコントロールと同じですね。HTML+CSS+JavaScriptで作成するようなクライアントサイドのコントロールじゃなくてASPXのPage Lifecycleの中でコントロールツリーとして生成されるサーバーコントロールです。

Futuresに含まれるコントロールは以下の7つ。

  • Label
  • TextBox
  • Password
  • Hidden
  • DropDownList
  • ActionLink
  • Repeater

はて、ナゼMVCにこれらサーバーコントロールが含まれてるんだろう(Futuresだけど)。ポストバック(ASP.NETの)もViewStateも無いので、ポストバックされた後のコントロールツリー構築なんてことは発生しない(そもそもポストバック先がPageじゃなくてControllerなんだから)し、かといって、積極的にコントロールツリーを構築してからレンダリングするなんてことは全然見通しのいい完全制御出来るHTMLとは言い難い(オレはサーバーコントロールのレンダリング結果を完全に把握してるぜ!という話じゃなくてデス)。なので、考えられる理由は、<%~%>での埋め込みコードを減らして、見やすくしようという意図なのかな~、と推測してます。理由なんてどーでもいいんですけど。

ASP.NET MVC Release Candidate 2: I declare myself to be declarative! - Eilon Lipton's Blog

↑こちらでしっかりと紹介されてます。

試しに使って見ましょう。MVCのプロジェクトを新規に作成し、プロジェクトの参照設定にMicrosoft.Web.Mvcを追加しましょう。続いて、web.configの設定をしておきます。

<system.web>
 …
 <pages>
   <controls>
     <add tagPrefix="asp" namespace="System.Web.UI" assembly="System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
     <add tagPrefix="asp" namespace="System.Web.UI.WebControls" assembly="System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
     <add tagPrefix="mvc" namespace="Microsoft.Web.Mvc.Controls" assembly="Microsoft.Web.Mvc"/>
    </controls>
   <namespaces>
     <add namespace="System.Web.Mvc"/>
     <add namespace="System.Web.Mvc.Ajax"/>
     <add namespace="System.Web.Mvc.Html"/>
     <add namespace="System.Web.Routing"/>
     <add namespace="System.Linq"/>
     <add namespace="System.Collections.Generic"/>
     <add namespace="Microsoft.Web.Mvc"/>
    </namespaces>
 </pages>
 …
</system.web>
上記太字の部分、サーバーコントロールのプレフィックス登録と、ページでのネームスペース登録。これをやっておかないと記入が面倒なことになるので忘れずに。

HomeControllerのIndexアクションで出力用のデータをViewDataにセットしておきます。

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

namespace Mvc.RC2.Controllers
{
 [HandleError]
 public class HomeController : Controller
 {
   public ActionResult Index()
   {
     ViewData["Message"] = "Welcome to ASP.NET MVC!";
      ViewData["TextBox"] = "テキストボックスに表示するメッセージ";
     return View(new { Label = "ラベルに表示するメッセージ" });
    }

   public ActionResult About()
   {
     return View();
   }
 }
}

ViewDataにテキストを入れて渡す方法と、Viewへ匿名クラスを直接渡す方法を書いておきます。匿名クラスも簡単に取り出せるのを覚えておくとちょっと便利です。

これらを表示するタメにViews/Home/Index.aspxにコードを追加します。

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

<asp:Content ID="indexTitle" ContentPlaceHolderID="TitleContent" runat="server">
   Home Page
</asp:Content>

<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>
  
    <p>
     <mvc:TextBox runat="server" Name="TextBox"></mvc:TextBox>
     <mvc:Label runat="server" Name="Label"></mvc:Label>
   </p>
</asp:Content>

サーバーコントロールなのでrunat="server"を忘れずに。Nameで指定したのがエレメントのName属性にセットされるのと同時にViewDataから同名の値を取得して展開してくれます。 なので、これで出力されるHTMLは↓こうなります。

    <h2>Welcome to ASP.NET MVC!</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>
  
   <p>
     <input name="TextBox" type="text" value="テキストボックスに表示するメッセージ" />
     ラベルに表示するメッセージ
   </p>

匿名クラスの値がそのまま展開されてますね。これは内部でViewData.Eval(データーキー)を呼び出してるからです。なので、 MVC Controls特有の動きというわけではなく、自分でも普通に同じ方法で値を取得出来ます。Viewに以下のようなコードを書いてみます。

    <p>
     <div><%= ViewData.Eval("TextBox") %></div>
     <div><%= ViewData.Eval("Label") %></div>
   </p>

これは以下のようなHTMLとして展開されます。

    <p>
     <div>テキストボックスに表示するメッセージ</div>
     <div>ラベルに表示するメッセージ</div>
   </p>

匿名クラスの場合、Modelのクラスが不明なので、インテリセンスでの取得は出来ないですけど、Evalを使う事で取得は出来ます。例えば、RenderPartialするユーザーコントロール(ascx)に、匿名クラスで値を渡す時(View Modelクラスを書くほどでも無いけどViewDataをそのまま参照するのもコード見にくい、なんて時)でもユーザーコントロール内でViewData.Eval()で取得出来るので、何かと便利だったりします。

ちなみにこのmvc:LabelコントロールにはTruncateLengthとTruncateTextなんていうプロパティがあって、コレを指定することで自動で長い文字列をカットしてくれます。例えば先ほどのLabelの部分を以下のように書き換える。

    <p>
     <mvc:TextBox runat="server" Name="TextBox"></mvc:TextBox>
     <mvc:Label runat="server" Name="Label" TruncateLength="5" TruncateText="~"></mvc:Label>
   </p>

そうすると5文字にカットしてサフィックスをくっつけてくれます。HTMLは↓こうなります。

    <p>
     <input name="TextBox" type="text" value="テキストボックスに表示するメッセージ" />
     ラベルに表~
   </p>

TruncateTextは初期値として"..."がセットされてるので、何も指定しなければ今回の例でいうと「ラベルに表…」となります。

RepeaterはWeb FormsのRepeaterと同じように動くので、繰り返しをforeachで書きたくないなんて場合は利用するのもいいんじゃないかな。DataSource指定やDataBind()はしないのでコードビハインドも不要です。今後いろんなコントロールが出てくるかもしれないね。

最近のお気に入り

IMG_0457 IMG_0460 

Bluetoothスピーカー。

キラキラボディーでお気に入り。PC本体のスピーカーだとシャリシャリ感が気になるし、あんまり音も大きくならないもんね。ヘッドホンも聞きやすくていいんだけど、いちいち線をつなげて、耳に当ててとかっていうのが面倒。移動するとき外すのも面倒だし。音楽を聴いたりするわけじゃないから、音質はそんなに気にならないよ。ハンズフリーのプロファイル(?)は削除しとかないとなんか調子悪かった。

Amazon.co.jp: Bluetooth ステレオスピーカー BIT-STB2825S シルバー A2DP・HFP・HSP・ワンセグ音声SCMS-T: 家電・カメラ

IMG_0467 IMG_0468

でろ~ん。スライムじゃないよ~。キーボードを掃除するCyber Cleanっていう製品(スライムだな...)。こないだハンズで売ってるのを見かけて買っちゃった。ベトベトしないし、ちゃんとゴミを吸着してくれるし、なんか楽しいし。

Amazon.co.jp: アイリスオーヤマ サイバークリーン 135gプラスチックボトル入り PCP-135: 家電・カメラ

2009年3月8日日曜日

第2戦(Brass Division)

前回の反省を生かし、今回の試合では前半から強気の攻めで、試合の流れを持ってくることを心がけよう。そんな目標を事前に立てての試合。試合前にはメンバーが全然集まって無くて唖然としたけど、試合始まる直前にはなんとか2セット揃ってホッとした。目標を立てたのに人がいないことにアワアワし過ぎて、すっかり目標のことを忘れちゃってたよ。 前半からガンガンがんばれ作戦っていうこともあって、セットの組み合わせも1stタケ・ミズ・セト、2ndオグ・シゴ・ミチと今期から参加のシンタローと組むことになり、走りまくらなきゃいけない予感。前回の試合で走らなすぎたら見方からのブーイングで心折れそうになったのを思いだし、今回はちゃんと最初から攻め気も見せつつプレーしなきゃね。 珍しくイマイちゃんとユミちゃんがいないおかげで5メンズ1ウーマン。オニャノコいないと、ブラスではブーイングがでちゃうS40なんだけど...。 まぁ、そんなこと心配してもしょうがね。 試合開始するまで全然気がつかなかったけど、開始2分で3点...。おかしい。いや、得点できることはいいんだけど、いくらなんでもおかしい。そんなアッサリ目標通り得点できるなんておかしすぎる。相手ゴーリーをよくよく見てみると、なんかスケーティングがゴーリーのそれとは違う。横移動も前後移動もままならない感じで、ほぼゴールライン上で微動だにしてない。もしや、プレーヤーゴーリー?ちゃんとオスギに確認しとけば良かったけど、試合は始まっちゃってるし、相手ベンチに「ゴーリーの人プレーヤー?」なんて聞けるわけもなく。 そのまま、前半から攻め続けて、と言うか我守らずを全員がモットーとしてるんじゃないのかってくらいの守備放棄。カネコさんが初戦に引き続いての鉄壁っぷりを発揮してくれてたからよかったものの。そんな流れで8-0で前半終了。どうも外野からのブーイングが気になる...。 後半も相変わらずノーガード。流石に守りきれず目の前でリバウンドを叩かれて失点。自分の目の前っていうのが責任感じる。守ってたのに守ってなかったんじゃないのかっていう失点の仕方だったのが余計にたちが悪い。それでも、攻めに攻めて得点を重ねるんだけど、終了間際にまたしても自分がリンクに乗ってるときに失点...。なんかもう、プレーオフの時からそうだけど、こうも終了間際の失点が続くと切なくなるね。 試合終了後、オスギに確認したら、急遽ゴーリーがこれなくなったそうで、プレーヤーがゴーリーをしてたんだそうな。単純に得点しまくってたのはそんな理由からですよ。17-2と勝ったはいいけど全然勝った気もせず。失点には常に絡む自分を励ましたい。 今回の試合では自陣ベンチからのブーイングがなかったものの、外野からのブーイングが炸裂。日曜に戸塚に行っても「昨日アメージングに鬼が出た」だの「悪魔っているんだね」だの、さんざんな言われよう。これからは演技派を目指そう。必死のプレーでゴール前でこけるとか、シュートをカラぶるとか、見てて一生懸命なのは伝わるけど、コレといって活躍しないっていう...。もう、それしか、自分を守る方法が思いつかない。ぎゃふん。

2009年3月4日水曜日

ASP.NET MVC RC2リリース

when its going to be released - ASP.NET Forums

↑ここで発見。最後の発言が...。まぁ、そうですけど...みたいな。

いろいろ気になるところがあったんでしょうね。安易にRTMを出さず、品質上げるためにRC2リリースの決断に敬意を表します。

ASP.NET MVC Release Candidate 2

とはいえ、RC1との違いが単にインストーラだけなわけ無いですよね。しっかりとリリースノートを確認しましょう。

CodePlexにソース/Futuresも上がってるんですが、その下にDataAnnotationsを使ったModelBinderのサンプルが...。もっと早く出してくれれば、自分でサンプル書かなくても良かったのに...。

このDataAnnotationsModelBinderの実装を確認してみると、その内容がまた超かっこいいです。IDataErrorInfo使わずに、DefaultModelBinderを派生させて、OnPropertyValidatingのオーバーライドメソッド内でValidationAttribute属性の取得・実行とModelStateへのエラー投入を行ってる。入力検証の集約というより、DataAnnotationsはそれだけで検証を完結させておき、モデル自体の検証や、ビジネスルールの検証は切り離して行うという設計。IDataErrorInfoならErrorのオーバーライドでモデル検証も集約できるから、どっちが分かりやすいコード(このサンプルだとモデル検証は別途モデル層に実装しましょうということになる)になるかは、アプリケーション設計者の好みでどうぞ、ってことですかね。

リリースノートに書かれてる変更点(それほど多くない)をさらっと確認してみると、以下のような感じです。

  • .NET Framework 3.5SP1必須です。入れといてね。
  • サーバーインストールモードを用意しました。VS関係のインストールがないのでデブロイ環境へのインストールはこれを使いましょう。 msiexec /i AspNetMVC1-RC2.msi /l*v .\mvc.log MVC_SERVER_INSTALL="YES"
  • GACに入れるよ。
  • AntiForgeryのCookie出力時にパスを指定できるようにしました。なので、デプロイ環境(ルートからとかサブフォルダあるとか)に合わせてパスを調整できます。
  • DefaultModelBinderが出力するエラーメッセージをローカライズできるようにしました。resxファイルを独自に定義して分かりやすいエラーメッセージを登録しておきましょう(InvalidPropertyValueとPropertyValueRequiredの2個だけ)。
  • ValidationSummaryのオーバーロードが増えたよ。メッセージリストをul/liタグで展開する前にspanタグでのタイトル表示できるなり。
  • jQuery1.3.1にしたよ。
  • あとバグフィックス。DropDownListの例外。web.configのauthenticationの値をLogOnに。Site.Masterとかで使ってるheadタグがrunat="server"をちゃんと動くように。checkboxとradiobuttonをModelStateからちゃんと復元。ルートのDefault.aspx(ルーティングでコントローラ+アクション指定してるとルートアクセスでデフォルトのコントローラ+アクションが実行されるけど、これにOutputCacheを指定しても効かない)でOutputCacheがちゃんと効くように。

基本的にRC1のコードならそのまま動くっぽいですね。

追記:2009/03/04 16:50

System.Web.AbstractionsとSystem.Web.Routingの2つのアセンブリがSP1のものを利用するように変わったんですね。なので、System.Web.Mvcだけ(Futuresを利用するならMicrosoft.Web.Mvcも)を配布すればよくなりました。グッジョブ!

dotnetConf2015 Japan

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