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

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: 家電・カメラ

dotnetConf2015 Japan

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