2010年2月28日日曜日

続Demotterについて

途中まで書いて力尽きたので、続きを。

ValuProviderFactoryについての説明まで進みましたが、そもそものIValueProviderは何なのかというところをすっ飛ばしてます。IValueProvider自体はMVC 1の頃から存在してるものです。GetValueでValueProviderResultとして値を取得するためのインターフェースです。UpdateModelなんかもIValueProviderを渡すオーバーライドがちゃんとあります。FormCollection.ToValueProvider()なんかを渡しますね。

ようは何でもいいからキーに対応する値を渡せばいいんです。基本実装のDictionaryValueProviderは値の実体そのものをキーと共に保持してるものなんですが、キーに対応する値を常に保持しておく必要はなく、GetValueのたびに取得しにいくという実装もアリです。HttpCookieValueProviderの例としてとてもいいサイトが有ります。

Dive Deep Into MVC - IValueProvider - Mehdi Golchin's blog

このサンプルコードではGetValueのたびにRequest.Cookieを参照する作りなのは、値の実体はHttpContextにそもそも保持されてるので、Dictionaryとして二重に保持する必要がないからです。ただ、これだとテストはHttpContextBaseのモック作成から必要になるんで、少し面倒ですけど。

Demotter(MvcPresetner)で作成したのは、PresentationZipValueProviderFactoryでプロジェクトルートにあるやつです。

mvcp

これまで説明したValueProviderFactoryのサンプル実装としてZipファイルをアップロードすると、サーバーサイドで解凍し、Presentationクラス(特にModelBinderの対象となるクラスを限定する縛りはないです)のインスタンスを生成するためにDefaultModelBinderから利用出来るようになっています。

なんで、ここに注目してるのかというと、SRP(Single Responsibility Principal)でテストを簡単にしたいからです。やっぱり楽して作りたいというのがあるからね。Actionのテストってモデルクラスを渡してしまえば、実行コンテキストに依存させなくてもいいじゃないです。DefaultModelBinderは標準機能だからテストなんかしなくていい。そうすると、カスタムValueProviderFactoryだけがHttpContextBaseのMockを使ったテスト対象になるわけです(ViewのテストはWebサーバーで実際に動かしてSeleniumとかでどうぞ)。素晴らしいリファクタリングだと思います。

Zipファイルを解凍するために、外部の依存アセンブリとしてDotNetZip Libraryを利用しています。ZipFileクラスのReadでZipファイルを指定し、ExtractAllで全解凍です。使ってる機能はそれだけ。

PresentationのリポジトリとしてStoragePresentationRepositoryクラスを作ってます。IPresentationRepositoryのファイルストレージ保存用の実装です。なので、ここはデータベースに保存するようなRepositoryを実装すれば、データファイルの保存場所は上位のサービス層(このサンプルではサービス層はなくControllerで直接Repositoryを使ってます)が知る必要はないような作りです。

実ファイルを”~/App_Data”に保存するようにしてるのでServer.MapPathを使う必要があり、Controller.InitializeでIPresentationRepositoryの実装クラスのインスタンスを作成してるので、LinqToSqlPresentationRepositoryを作成したとしても、単純変更はできないのが、手を抜いたところです。RepositoryのResolverというかCreatorを作っておいて、そいつに任せるようにしておく実装であれば依存性を排除できますね。

    private IPresentationRepository _repository;
    public static Func<RequestContext,IPresentationRepository> RepositoryCreator = 
      (requestContext) => new StoragePresentationRepository(
requestContext.HttpContext.Server.MapPath("~/App_Data")
); protected override void Initialize(RequestContext requestContext) { base.Initialize(requestContext); // Serverプロパティなどの参照はInitialize以降じゃないと // できないので気をつけましょう _repository = RepositoryCreator(requestContext); }

たとえば、現状のコードを↑こうしてみるとか。LinqToSqlPresentationRepositoryはRequestContextを必要としないですけど、簡単にするにはこういうのを渡すルールにしておくのもいいんじゃないですかね。Global.asaxなんかで以下のようにCreatorを変えちゃえば、うまくいくはず。

    protected void Application_Start()
    {
      AreaRegistration.RegisterAllAreas();

      RegisterRoutes(RouteTable.Routes);

      ValueProviderFactories.Factories.Add(new PresentationZipValueProviderFactory());

      HomeController.RepositoryCreator = (_) => new LinqToSqlPresentationRepository();

      // カスタムModelBinderを使うなら↓ここで登録忘れずに。
      //ModelBinders.Binders.Add(typeof(Presentation), new PresentationModelBinder());
    }

ここはMVC関係ないところ。今回のサンプルではControllerが生成されるたびに、毎回Repositoryの中でApp_Dataを見てPresentationのインスタンスを取得するので無駄が多いですが、その辺もサンプルということで勘弁してもらえると助かります。

ModelBinderは標準のDefaultModelBinderを使っていて、DataAnnotationModelValidatorがそのまま機能します。カスタムModelBinderでもDataAnnotationを機能させるなら、多分以下のような作り方になると思います。

  /// ValueProviderFactoryを定義しない従来の手法だと、ModelBinderを作成して
  /// 以下のように自分でマップしたモデルに対してValidationを実行することになります。
  public class PresentationModelBinder : DefaultModelBinder
  {
    public override object BindModel(ControllerContext controllerContext, 
ModelBindingContext bindingContext) { // ここでモデルを生成してModelMetadataに入れておくと、 // CreateModelでは生成せずに、OnModelUpdating/OnModelUpdated // を内部で呼び出してくれるようになる。 // でも、この書き方であってるのか自信無いですが...。 var valueResult = bindingContext.ValueProvider.GetValue("Name"); var model = StorageAccessor.Load(
PresentationZipValueProviderFactory.UploadTempPath,
true, valueResult.AttemptedValue); bindingContext.ModelMetadata.Model = model; return base.BindModel(controllerContext, bindingContext); } }

これがあってるのかどうかは自信がないです。BindModelの戻り値にインスタンスをそのまま返すだけではDataAnnotationが効かないので、BindingContext.ModelMetadata.Modelに対象モデルのインスタンスを入れて、後はbase.BindModelに任せてしまう実装です。いいのかな~。ちゃんと動くのは確認してます。

リポジトリから取得できたものをHTMLとして生成するために、Viewにモデルを渡し(Presentationクラスのインスタンス)、後はViewにまるなげです。

スライドとして表示したいデータをMarkdown書式で送信したものを利用するようにしてるので(Stackoverflow.comをまねっこしてみたかった)、Markdown書式からHTMLに変換する必要があります。クライアントでの変換実装としてWMDというのもありますが、今回はサーバーサイドでHTMLに変換するMarkdownSharpを利用しています。2個目の外部依存アセンブリです。Markdownで厳しいところはUL/LIの入れ子が2段までしかできないところ。できる方法があるんだろうか。当たり前ですが、利用目的がスライドじゃないのでしかたないところですね。

スライドの動き自体はカーソルの上下でフェードさせながらの切り替え、左右でアニメーション無しでの切り替えの2種類のみで、リッチなアニメーションは実装してないです。その辺はS5やS6なんかがあるので、そっちに差し替えてもらえればいいかな~、なんて。

そんなこんなで、実行すると↓こんなです。

mvcp2

後は、F11でフルスクリーン表示にしておけば、それっぽく見えます。

あとアップロードしたコンテンツの削除をHttpDelete属性を指定したActionで実装してますが、これだけだとHttpVerbs.DeleteなリクエストじゃないとActionInvokerの対象として選択されないです。一般的なブラウザではGET/POSTしか送信してくれないので困りもの。でも、MVC 2ではHttpVerbsのオーバーライドを簡単にできるように拡張メソッドも用意されてるので、HtmlHelperの拡張メソッドHttpMethodOverrideをForm内で呼び出せば、POSTでもオーバーライド(hiddenに埋め込まれる)されてうまく動くようになります。Railsなんかでも"_method"でHttpVerbsをオーバーライドできるのでそれと同じです。

Viewでは以下のように書いてます。

    <ul>
    <% foreach (var item in Model) { %>
    
    <li>
      <%= Html.RouteLink(item.Name, "Viewer", new { id = item.Name })%>
      &nbsp;-&nbsp;
      <% using (Html.BeginForm("Delete", "Home", new { id = item.Name }, 
FormMethod.Post, new { style = "display:inline;" })) { %> <%= Html.HttpMethodOverride(HttpVerbs.Delete) %> <input type="submit" value="削除" /> <% } %> </li> <% } %> </ul>

簡単ですね。

mvcp3

↑こんなボタン出てきます。なんで、わざわざHTTPメソッドでActionを選択するのかというのはRESTfulなアーキテクチャスタイルの話になるので割愛。ただ、この実装方法であれば、ブラウザ以外からDeleteやPutのリクエストと、ブラウザからの同リクエストを区別するようにActionを書かなくて済むのがいいですよね。もちろんAction名が”Remove”とか”Update”とかでPostで処理をするようにしても、結果は一緒ですけどね。

DemotterことMvcPresenterが何を実装したサンプルなのか、だいたい分かってもらえたでしょうか。これを10分で話すのはさすがに無理ですね。詰め込みすぎました。

Demotterについて

ASP.NET MVC 2になって変更された箇所はとても多いです。MVC 2での新しくなった部分を紹介するサンプルとしてMvcPresenterというのを作成することにしました。Demotterという名前はEdtterへのオマージュ(?)。

MVC 2の新機能のうちマニアにはたまらないだろうなと思って目をつけたのが各種Providerモデルへのリファクタリング部分で、MvcPresenterではそのうちIValueProviderを実装したValueProviderFactoryのカスタム化と言う部分をメインに実装しています。

いきなりそんな話をされても意味がわからないと思うので、順を追って説明していきます(ASP.NET MVCについての基本的な知識は前提です)。

そもそもMVCではポストバックがないので、TextBoxやRadioButtonなどの入力用サーバーコントロールは使用しません。HTMLとしてのinputやtextarea、selectを使用するのみです。そうすると入力値をサーバーサイドで取得するにはどうすればいいかというと以下の3通りあります。

  • Request.Formで取得
  • FormCollection型の変数をAction引数に指定する
  • ModelBinderに任せる

Request.FormとFormCollectionを使用する方法はあまりにも原始的すぎます。入力に対する検証も自分で行う必要があり、とても煩雑なコードになります。

Viewとして以下のようなものがあるとします。

  <% using (Html.BeginForm("FormPost1")) { %>
  
    <% = Html.TextBox("name") %>
    <% = Html.TextBox("age") %>
    <% = Html.CheckBox("isDeveloper") %>
    
    <input type="submit" value="do post" />
    
  <% } %>

これを受け付けるActionとしてRequest.FormやFormCollectionの場合↓こうなります。

    public ActionResult FormPost1()
    {
      var name = Request.Form["name"];
      int age;
      int.TryParse(Request.Form["age"], out age);
      bool isDeveloper;
      bool.TryParse(Request.Form["isDeveloper"], out isDeveloper);

      // 処理

      return View();
    }

    public ActionResult FormPost2(FormCollection form)
    {
      var name = form["name"];
      int age;
      int.TryParse(form["age"], out age);
      bool isDeveloper;
      bool.TryParse(form["isDeveloper"], out isDeveloper);

      // 処理

      return View();
    }

ほとんど同じなんですが、テストする際にRequestなどのコンテキストに依存させないようにするためにFormCollectionを使用するという書き方が存在します。

これに対しModelBinderを利用するスタイルの場合は以下のようになります。

    public ActionResult FormPost3(string name, int age, bool isDeveloper)
    {
      // 処理

      return View();
    }

Action引数に直接入力値が入ってきます。型変換も自動です。変換できないならエラーになるという便利なものです。でも、これだと細かく入力エラーを処理できないです。しかも値が多いとAction引数がとんでもないことになります。

なので以下のようにクラスを定義して、そのクラスのインスタンスをAction引数に取得するというスタイルがオーソドックスな手法となるはずです。

  public class Person
  {
    public string Name { get; set; }
    public int Age { get; set; }
    public bool IsDeveloper { get; set; }
  }

↑これがクラス定義で、↓これがAction。

    public ActionResult FormPost4(Person person)
    {
      // 処理

      return View();
    }

何が違うかは一目瞭然。本来personという仮引数名を使用する場合、Formのname属性にプレフィックスとして"person."とつけておくんですが、そこは自動でプロパティ名とname属性をみて一致するなら埋めてくれます。なので、あえて"person.name"や”person.age”とname属性に指定しなくてもModelBinderは賢いのでなんとかしてくれるんですね。明示的に分けたいときにname属性にプレフィックスを指定する必要があります。

クラスを指定するのも基本型を指定するのもModelBinderにしてみれば同じことです。固有のクラスを使用して、DefaultModelBinderがきちんとインスタンスを生成できないときには独自のModelBinderを作成することになると思いますが、MVC 2ではそういう手法はあまりとらないんじゃないかと思ってます。理由はValueProviderFactoryが指定できるようになったからです。

不思議に思わないですか?Routeに指定した場合でもAction引数に割り当てられるし、FormからPostしても割り当てられる。もちろんQueryStringの場合でも自動でAction引数に値がわたってくるんですよ?データの出所がそれぞれ違うじゃないですか。RouteとQueryStringはURLだから同じだと見ることもできるんですけど。

ここで、もうひとつ忘れてはいけないのがUpdateModelとTryUpdateModelです。これはIValueProviderを指定するか、Formの値を利用するかのどちらかでクラスのインスタンスを生成してくれるんですが、それもValueProviderFactoryを利用することでデータの出所を意識しなくても良くなります。

さっきから"データの出所(でどころ)"という言葉を使ってますが、それってどういう意味かというと、ModelBinderが値を復元する際にどこから値を持ってくるのか?ということです。Request.FormなのかRequest.QueryStringなのか、RouteData.Valueなのか、ですね。じゃーRequest.Cookieから復元させたいときはどうすればいいと思いますか?MVC 1の時はAction内でRequest.Cookieを直接みて自分で変数に割り当てるか、カスタムModelBinderを作成し、そこでRequest.Cookieを参照してモデルに復元させる必要がありました。MVC 2になるとデータの出所をValueProviderFactoryから取得するという仕様になっているので、カスタムなValueProviderFactoryを作成し、Global.asaxでValueProviderFactoriesに追加しておけば、標準のValueProviderFactoryで見つからなかった場合、カスタムValueProviderFactoryから値を取得して、ModelBinderが値(クラスのインスタンスか基本型)を復元してくれます。Cookieから値を取得して復元させたければ、ModelBinderを作成するのではなく、そこはDefaultModelBinderに任せたまま、CookieValueProviderFactoryを作成するとなるでしょう。そうすることでDataAnnotationも有効な状態で値を取得できます(カスタムModelBinderでもできますがそれはまた別の時に)。

ModelBinderのデータの出所(データ取得元)を自分で好きなように指定できるということです。すごいことですよね。ちなみにValueProviderFactoryとして実装しなければいけない唯一のメソッドは

public override IValueProvider GetValueProvider(ControllerContext controllerContext)

です。IValueProviderの基本実装はDictionaryValueProvider<object>で、KeyValueなディクショナリです。ModelBinderの仕組みそのものはMVC 1の時から変わってないので、詳細ははしょりますが(書いたほうがいいですか?)、キーとしてForm要素のname属性やQueryStringのKeyを指定するのを想定して値を取得します。

なのでDictionaryとしてCookieから取得した値を返そうが、JSONをデシリアライズしてキー指定で取得できるようにしたものを返そうがXMLをキー指定で取得できるようにしたとしても、Dictionaryとして取得できるならなんでもいいんです。データの出所だけではなくデータのフォーマットにも依存させなくて済むということです。

JSONとして以下のようなデータがあったとしましょう。

{
  name:'たけはら',
  age:15,
  isDevelopper:true
}

ここから以下のように値が取得できるDictionaryを返すことができれば、ModelBinderは値を復元できるということです。

dict["name"] = "たけはら";
dict["age"] = 15;
dict["isDeveloper"] = true;

おなじ理屈でXMLでもいいですよね。自分で取りやすいスキーマさえ定義しておけばいいので。つまり、ファイルシステムにKeyValueでアクセスできるValueProviderFactoryを作成するなら、Zipでアップロードしたファイルを解凍し、フォルダ構造とファイル名がキーになっていてファイルの実体が取得できるようなものも作成できるわけです。実際にファイルの実体をbyte[]なんかで復元するのはリソースの無駄遣いになるので、ModelBinderが復元するのはファイルのパスにしておくというのが現実的でしょう。MvcPresenterがまさにそのように処理をしています。

ちょっと長くて疲れてきたので、続きは今度にしてもいいですかね。いいですよね。中途半端でさーせん。眠いっす。

2010年2月24日水曜日

ダイヤモンドは砕けない

ゆりかもめに初めて乗りました。

techdays2010 

わーい、自由の女神だー。あははー。あはははー。はぁ...。

台場でTechDays2010のBoFに出てきたんです。

4 の時代の Web アプリケーションを語ってみよう

小野さんとナオキさんと三人でのおしゃべりはTechEd 2009の時とあわせて2度目です。相変わらず打ち合わせとかしないのにはヒヤヒヤです。

ASP.NET 4になっていろいろすごいじゃないですか。その辺の話をしましょうよ、ということだったので、たけはら担当はもちろんASP.NET MVC 2。一応事前に書いておいたメモをブログにも載せておきます。

メジャーな部分

  • データ検証の方法がDataAnnotationsを基本にしたものになりました。

これまで通りIDataErrorInfoを実装したものも有効ですが、ValidationAttributeをモデルまたはモデルプロパティに指定(ModelMetadataで外部に定義したものでも可能)して、DataAnnotationsModelValidatorを使用するようDefaultModelBinderが変更になっています。
LinqBinaryModelBinderも標準実装に組み込まれたので、System.Data.Linq.Binary(SqlDbType.Timestampなど)をbase64でhiddenからポストしたときも、自動で復元されます!
またLinqBinaryModelBinder、基底クラスのByteArrayModelBinder(byte[]を復元)もFuturesから昇格です。

  • ASP.NET 4で組み込まれるSystem.Web.IHtmlStringを使えるようにMvcHtmlStringクラスが導入されました。

MvcHtmlStringのソースファイルを確認するととても勉強になります。まさに黒魔術。
すべてのヘルパーはstringではなく、このMvcHtmlStringを返すよう変更されています。
<%: expression %>としている場合、自動的にHtmlEncodeした結果がレンダリングされるので、今後はこれが主流になります(きっと)。
MVC2の実装でもそうですが、2.0ベースのSystem.Webにはそんなものないので、なので、ASP.NET 4じゃない場合はこれまで通り、Html.Encode(string)を使いましょう。
IHtmlStringの場合、<%: ihtmlstring %>となっていても、HtmlEncodeをかけずに出力するので、Partial HTMLをレンダリングする場合(ヘルパーのレンダリングとかも)は、IHtmlStringとして渡しておきましょう。

  • AcceptVerbsAttributeがHttpVerbs.Get/Post以外に、Put/Delete/Headにも対応するようになっています。

Get/Post以外は通常、ブラウザからは送信されないですが、HtmlHelper.HttpMethodOverride()をformタグ内で呼んでおくことで、hiddenにX-HTTP-Method-Overrideという名前で、メソッドオーバーライドを保持するようになり、POSTを使ったHttpVerbsの上書きができるようになります。
この辺の実装はHttpRequestExtensionsに用意されてるHttpRequestBaseに拡張メソッドとして実装しているGetHttpMethodOverride()がまるっと処理してくれるようになっています。
もちろんここも、AcceptVerbsAttributeの判定メソッドがoverride可能になっているので、独自のAcceptVerbsAttributeを実装することで、Railsライクに"_method"というオーバーライドを使うように変更することも可能です!
これは今までのバージョンではとても面倒な実装 にしないとできない部分でした。
ちなみに、RESTfulなController実装をするときに、これができないと、ブラウザからのリクエストと、その他のクライアントからのリクエストの処理を簡単に切り分けできなくてとても不便です。

  • JsonResultを返すActionの場合、HttpVerbsがPOSTであることが基本条件になりました。

セキュリティ的にゴニョゴニョらしいです。

  public ActionResult Json()
  {
    //var json = Json(new {result = "json!"});
    //json.JsonRequestBehavior = JsonRequestBehavior.AllowGet;
    //return json;
return Json(new {result = "json!"},JsonRequestBehavior.AllowGet); }

Getなら↑これで大丈夫。

修正:2010年3月9日

  • DefaultControllerFactoryからRequestContextプロパティが無くなりました。

依存性をなくすのはいいことです。その代わり、Controllerのインスタンスを生成するためのCreateControllerにRequestContextをパラメータとして渡すようになってます。
Mockを作る時が楽ちんです。

  • AreaをサポートするためにRouteData.Valuesで"area"が予約されるようになりました。

自分仕様でこの名前の値を使ってる場合は変更しておきましょう。

  • Html.Substituteは残念ながら使えなくなりました。
  • クライアントサイド検証が標準で組み込まれてます。

このクライアントサイドコードを生成するために、C#からJavaScriptを吐き出す
ScriptSharpが使われています。
コード生成に興味がある方は、参照してみるといいんじゃないかと思います。

Script#
http://projects.nikhilk.net/ScriptSharp

ボスが最近似たようなのを見つけてはしゃいでました。

SharpKit - Write C# instead of JavaScript
http://sharpkit.net/

  • Templateベース

DynamicDataと同じようにTemplateベースのモデルレンダリングを実装したHtml.EditorForやDisplayFor、プロパティベースのレンダリングを行うTextBoxForやLabelForなんかも目が離せない便利機能です。
System.ComponentModel.DisplayNameAttributeやUIHintAttributeを使ってカスタマイズもしやすくていいですね。

注目機能

  • ChildActionOnly属性

HtmlHelperのRenderAction/Actionからの要求しか受け付けないようにするActionFilter。

  • UrlParameter.Optional

Route登録時にRouteData.Valuesにキーすら含ませないようにするオプション。

Deep Dive!

一番注目したいのはASP.NETらしさをしっかり継承したProviderモデルへのリファクタリングです。マニアックな部分ですが、拡張性を考慮して、より自由どの高い開発を行えるよう沢山のフックポイントを提供するために実装されています。処理の流として入力→検証(Model)→処理(Controller & Model)→出力(View)
というフローになるそれぞれのつなぎ部分で拡張できる感じです。

MVCソースから"Provider"と付いているクラスを検索!

  • ModelMetadataProviders
    • ModelMetadataProvider
      • AssociatedMetadataProvider
        • EmptryModelValidatorProvider
        • DataAnnotationsModelValidatorProvider

ModelMetadataを取得するためのプロバイダ。
モデルやモデルプロパティに関するすべてのメタ情報。
ValidationAttributeの定義や、型情報、出力方法など。

  • ModelValidatorProviders
    • ModelValidatornProvider
      • AssociatedValidatorProvider
        標準のValidationAttribute用のAttributeAdopterクラスとDataAnnotationsModelValidatorを管理。
        • EmptryModelValidatorProvider
        • DataAnnotationsModelValidatorProvider
      • DataErrorInfoModelValidatorProvider
        IDataErrorInfoの実装に対するDataErrorInfoPropertyModelValidatorとDataErrorInfoClassModelValidatorを使って、検証の結果を取得する。
      • ClientDataTypeModelValidatorProvider
        数値型に関するNumericModelValidator。
    • DataAnnotationsModelValidatorProvider
    • DataErrorInfoModelValidatorProvider
    • ClientDataTypeModelValidatorProvider
  • IValueProvider
    • NameValueCollectionValueProvider
      • FormValueProvider
      • RouteDataValueProvider
      • QueryStringValueProvider
    • HttpFileCollectionValueProvider

ModelBinderがモデルにデータをバインドするさいに、値を取得する際に利用する。データの出所がどこなのかをModelBinderは意識しなくてもいいんです。

次に"Factory"とついたクラスを検索!

  • DefaultControllerFactory
  • ValueProviderFactories
    • ValueProviderFactory
      • FormValueProviderFactory
      • RouteDataValueProviderFactory
      • QueryStringValueProviderFactory
      • HttpFileCollectionValueProviderFactory

コントローラの生成用のControllerFactoryと、IValueProviderを実装したValueProviderFactoryたちの2種類です。これがリファクタリングした超重要な部分になります。

で、後は、これらを利用したサンプルの紹介という流れです。

MVC Presenter

↑これね。このサンプルそのものがWebアプリケーションになっていて、サンプルの実行そのものがプレゼンスライドの表示になるという仕掛けだったんですけど...。

プロバイダモデルがうんぬんくらいからちょっとツマラナイ話になってしまいましたね。今回はコードに関する説明がごっそりできてないので、MVC2の良くなった部分がサンプルのどこにどう使われているのか、全然伝えられませんでした。反省してます。Zipで圧縮したのをアップロードしてどうのこうのとか、実はパワポじゃないんだよとか、それ以前に何を伝えようとしてるのかが、伝えられてなかったです。ドン引きってこんな時に使うのかな。

心が折れて、帰りのゆりかもめでは夕日が目に染みた。泣いてなんかないやい。

JOJO'S BIZARRE ADVENTURE Part4 Diamond is not Crash

けど、魂を砕くことはできないぜ!また機会があったら、今回の教訓を生かし、もっと参加者に楽しんでもらえるよう的を絞ったプレゼン+コードにしようと思います。

サンプルの説明を近いうちここにエントリしようと思います(ここ知らない参加者の方には申し訳ないですが)。

2010年2月15日月曜日

Sauce IDE+RCはFirefox+Firebugでの使い勝手がとてもいい

Webアプリのテストの定番といえば、SeleniumかWatiNなのかな、というレベルのWebアプリテスト初心者ですが、Seleniumの後継にあたる(この辺あいまい)Sauceっていうのを試してみたよ!

Products - Sauce Labs

まだないけどいつかDimeCastsでビデオが公開されるに違いない。

試すにあたり、何かしらプログラムが必要だろうと、昨日ハンサムスーツを見ながらシンプルブログ(全然ブログじゃないというのは本題じゃないYO!)を作ってみた。

タイトルいれて本文入れるだけね。せっかくだからASP.NET MVC RC2で。話はそれるんだけど、RC2のItem Templateはイマイチかも。TextBoxForなんかは新しいTemplateベースのDisplayForやEditorForに揃えてくれるともうちょっと素敵さが増すんじゃないかな~。せっかくDataAnnotationsでDisplayName属性を指定してもLabelForで展開しないとそこから表示名を取得してくれないじゃないっすか。Listテンプレートは面倒かもしれないけど、そうじゃないところはDataAnnotationsの属性を生かせるヘルパーを使った物に統一してもらいたいデス。せっかくUIHintでカスタムEditorTemplatesを指定しても効いてくれなくてちょっと切なかった。

sauce1

↑一覧ページ。殺風景ですいません。

sauce2 sauce2_2

↑新規ページ。同じフォーマットで編集ページもあるよ。ここで入力エラーがあると、DataAnnotationsがバシっと効いてくれるのがV2の素敵なところ。

sauce3

↑新規登録すると一覧に表示されるよ。殺風景ですいません。

sauce4

↑詳細ページ。コレといって何も無いですね。あ、本文にはHTMLが入るイメージなのでHTMLエディターをEditorTemplatesで作ってもいいね。今ならMarkdownのほうがオシャレかの~。

sauce5

↑削除ページ。削除すると、一覧ページに戻るだけ。

とりあえずこんなのを作って、これに対してテストを実施させてみようじゃないですか。

実行したいテストは以下の通り。

  1. 1件も登録されてない状態で新規登録
  2. 1件以上登録されてる状態で新規登録
  3. 未入力状態でCreateを押してモデル検証をエラーにする
  4. 登録されているすべてのエントリを削除する

今回認証は無しで。

そもそもSeleniumすら使った事ないけど、まずはSauce LabsのProductsトップにあるビデオを確認。ふ~ん、ってなもんです。とにかく、Sauce IDEをFirefoxにインストールして、Sauce RCをローカルPCにインストール。まずはこれでいいっぽい?う~ん残念。Selenium RCをダウンロードしておかないとUnit TestをC#で実行できないみたいです。

Sauce IDEがテストレコーダーで、Sauce RCがテスト実行サーバー、Selenium RCがテストを実行させるのに必要なアセンブリ(Sauce RCサーバーとの通信を行う)を提供、って具合の理解でよろしかろうか。Sauce RCはJavaアプリで、Webベースの管理インターフェースもありそこでテスト実行ブラウザやその他設定の変更ができると。

sauce6

テスト実行する場合、ブラウザを起動してそのブラウザをコントロールすることでテスト実行と結果取得する仕組み?Sauce IDEが表示してくれる、C#のコードはNUnitを基本になってて、そこはMSTestでよしなに動くようにチョチョッと変更。大枠はTestInitializeでDefaultSeleniumを初期化するようにするのと、TestCleanupでDefaultSeleniumの停止処理、あとはひたすらテスト書く。

    private ISelenium selenium;
    private StringBuilder verificationErrors;

    [TestInitialize]
    public void Setup()
    {
      selenium = new DefaultSelenium(
"localhost",
4444,
"*firefox",
"http://localhost:20337/"); selenium.Start(); verificationErrors = new StringBuilder(); } [TestCleanup] public void TeardownTest() { try { selenium.Stop(); } catch (Exception) { // Ignore errors if unable to close the browser } Assert.AreEqual("", verificationErrors.ToString()); }

あと、ちゃんとSelenium RCのダウンロードしたアセンブリをテストプロジェクトでは参照設定しとこうね。

DefaultSeleniumの1個目と2個目のパラメータはなんじゃらホイ。3個目はブラウザっぽくて4個目がテスト対象アプリのアドレス。どうもこの1個目と2個目がSauce RCのサーバーらしい。RCの制御用Webインターフェースはhttp://localhost:8421/ でポートが8421。実際のアプリはポートが20337と、いろいろ仕組みを理解しておかないと~。Sauce OnDemandっていうクラウドサービスもあるからね~。

上記初期コードを書いておけば、あとはIDEの出してくれるC#コードをコピペっちょ!なんだけど、それだと思ったようなエレメントを指してくれない。実行結果のHTML全体に文字列が含まれてるかをチェックするIsTextPresentなんかを使うのはテストとして成立しないっすよね。

なので、エレメントの参照はCSS SelectorかID指定かXPath指定がオーソドックスな指定の仕方になるかと思うわけです。でね、Firebugってさ、前バージョンまでは別途Plug inを入れないとXPathの取得ってできなかったけど新しいバージョンになって”XPathをコピー”っていう機能が標準で実装されてるじゃないですか。

sauce7

↑エレメントのXPathが表示されてるから↓右クリックでコピーだ!

 sauce8

で、対象となるエレメントを特定するためのXPathは何の苦労もなく取得できちゃうので、テストコードのアサーション部分やClick対象のエレメントなんかもこれを使って一意に特定だぜ!

    [TestMethod]
    public void A_First_Entry()
    {
      selenium.Open("/");

      selenium.Click("link=Create New");
      selenium.WaitForPageToLoad("30000");
      selenium.Type("Title", "first");
      selenium.Type("Body", "最初のエントリ");
      selenium.Click("//input[@value='Create']");
      selenium.WaitForPageToLoad("30000");

      Assert.IsTrue(
        selenium.GetText("xpath=/html/body/div/div[2]/table/tbody/tr[2]/td[2]") 
        == 
        "first");

      // 最初のDetailsをクリック
      selenium.Click("xpath=/html/body/div/div[2]/table/tbody/tr[2]/td/a[2]");
      selenium.WaitForPageToLoad("30000");

      Assert.IsTrue(
        selenium.GetText("xpath=/html/body/div/div[2]/fieldset/div[4]")
        ==
        "最初のエントリ");
    }

↑こんな感じで入力値と入力結果を比較してちゃんとHTML的に想定通りの結果が生成されてることをテストコードに書けちゃうのが素晴らしい。無知って怖い。これまで自分は一体なにをしてたんだと言いたい。

これらテストを実行する時に、MSTest(かReSharperのテストのどっちがそういう仕様なのかは知らないけど)だとテストメソッド名順に実行されるので、テスト内容が直前の実行結果に依存するような場合は、プレフィックスを決めておく必要があったりしそう。テストクラスそのものを分けてしまうのもありだけどね。

実行すると、ブラウザが起動してあとは勝手に処理してくれるっす。

sauce9  sauce10

↑これがFirefoxで実行してるところ。

sauce11

↑こっちがIEで実行してるところ。

ブラウザの切り替えはTestInitializeの設定じゃなくて、Sauce RCの設定画面が優先、っぽい。何にせよ、初めてのWebアプリのユニットテストとしては好感触。今後も精進していきます。

今回のサンプルアプリとテストコードは↓こちらからどうぞ。

※ちなみにサンプルアプリ自体のテストコードはないのであしからず。

2010年2月13日土曜日

OptionalなRoutingパラメータ

ASP.NET MVC 2 Optional URL Parameters

Philさんのところで取り上げられてるUrlParameter.Optional。

RC2で既に実装されてて、標準のプロジェクトテンプレートが生成するGlobal.asaxのルーティング登録で使われてますね。どういう機能なのかはPhilさんが書かれてる通り、Routing登録時に初期値をセットせず、ルーティングデータにキー自体含めなくするためのモノですね。なるほど~。

      routes.MapRoute(
        "Default", 
        "{controller}/{action}/{id}",
        new { controller = "Home", action = "Index", id = "" }
      );

これまで通りのルーティング登録だと上記のように空文字列を指定したりするところをUrlParameter.Optionalを使って下記のように登録する。

      routes.MapRoute(
        "Default", 
        "{controller}/{action}/{id}", 
        new { controller = "Home", action = "Index", id = UrlParameter.Optional } 
      );

そうすると、デバッグでルートURLにアクセス(~/Home/Indexか何も無しか)すると以下のようなルーティングデータとなりました。

urlparam1

id=””とした場合は、ルーティングデータにidが含まれるのに対し、Optionalの場合はキーすら含まれず、controller名とaction名のみです。

      routes.MapRoute(
        "Default", 
        "{controller}/{action}/{id}", 
        new { controller = "Home", action = "Index" } 
      );

ちなみに↑これはダメっす。idがUrlに含まれない限り解決できないから、ちゃんとすべてのパラメータを指定しなくてもアクセスしたい場合には使えないデス。

なんとなくRoutingの機能が拡張されたのかな?と思えるところですが、MvcHandlerのProcessRequestInitで呼び出してるRemoveOptionalRoutingParametersがOptionalならRouteDataから削除してるんですね。賢い!

これをいじってる時に気がついたんだけど、HtmlHelperにValidation/ValidationForなんてのが追加されてるじゃないですか。RCからなのかな。いったい何に使うのかいまいちよくわからないな~。FieldValidationMetadataをどうしたいんだろう...。クライアントバリデーションに絡んでんのかな~。ViewContext.FormContext.FieldValidatorsに入るはずなのにViewで実行しても特に変化が見られない。使い方間違ってるのかの。FormContextってそもそも誰ぞな。DataAnnotationsModelValidatorのエラーメッセージが日本語なのはなんでっすかね。

2010年2月11日木曜日

お試しAdWords

1月の初め頃だったような、記憶が曖昧なんですけどGoogleからAdWordsのお試し券5000円分が送られてきました。個人的に全く使い道を思いつかなくてすておこうと思ったんだけど、ずいぶん前にamachang(http://d.hatena.ne.jp/amachang/20090113/1231827150 Google Adsense の件について - IT戦記)がAdWordsにブログを広告する実験をしてたのを思い出したので、マネッコマネマネして自分もここのブログを無駄に広告してみようと試してみた。

途中で一度キーワードを変更したり、上限クリック単価を上げてみたりと調整したものの、目的もなくやってるもんだからそれがどうしたと自分にツッコむ以外することもない。

adwords2

↑こんな感じの設定にしておいて、5000円使い切るまで放置!

にしたかったんだけど請求を後払いにしないとお試しできないらしく、使い切るまで完全放置にするのは難しいみたい。すごく長い期間表示するようにして気がついたら請求額が5000円超えなんてことになったら無駄使いにも程がありすぎるし。数日様子をみて1ヶ月くらいは大丈夫かなと期間は1/10~2/5にセット。

adwords1

2種類の数値クリック数と表示回数を表示したのが↑。まぁ、見事な相関関係があります。そりゃそうか。そもそも入札してるキーワードが良くないね。わざわざ広告をクリックしたいようなコンテンツじゃないし、こんな情報を探してる人なら普通に検索しそう。だって”ASP.NET MVC”で検索して出てくる広告でデベロッパーが欲しいのってコンポーネント的なモノだったり書籍だったり以外ないと思うし、そもそもそんなキーワードで検索するようなリテラシーの人が広告をクリックすること自体が疑わしい。

と、言う訳で全くもって試す内容に失敗したAdWords初体験の巻でした。

途中で、ナオキさんに見つかって”クリックしないで!”というやりとりが唯一面白かったところかの~。

  • クリック数 ー 119
  • 表示回数 ー 326,365
  • クリック率 ー 0.04%
  • ご利用金額 ー \4,763

結構ギリギリ金額じゃん。

2010年2月6日土曜日

動的なRouting登録の素敵な方法

MVC V2 RC2が出てるのに関係ない話です。

Editable Routes Using App_Code

少し前にエントリされてたけど、ずっと放置してたです。そもそもEditable Routesで最初の実装サンプルが出てた話。

何をしてるのかというと、動的なRoute登録をするのにいちいちビルドし直さずに、Routes.cs ファイルをBuildManager.GetCompiledAssemblyで実行時にコンパイル。Routes.cs自体にCacheDependencyでファイルの変更監視をさせておき、csを書き換えた時点で動的にコンパイルしなおす(上記BuildManagerで)。そこから取り出される IRouteRegistrar実装がルーティングを登録するようにしておくことで、Global.asaxにルーティング情報を保持しなくて良くなるし、アプリケーションの機能とルーティング情報を分離して、柔軟な環境にできる(Subtextがそうなってるのかね)んですね。

で、 CacheDependencyでファイルを監視させるときにFileSystemWatcherを中で使ってて、それがFullTrustじゃないと実行できないから不便だねっていうのがこのエントリの本題みたい。解決方法としてはシンプルに~/App_Code下にRoutes.csをおいて、自分では監視しないようにするところ。そうしとけば、MediumTrustでも動くんだよと。なるほど。

~/App_Code配下のファイルなら更新時にAppDomainがシャットダウン(CodeDirChangeOrDirectoryRename)して再起動されるから、その ASP.NETの仕組みに任せとかばいいじゃんと。素敵だす。その場合、コンパイルは勝手にされるから、 BuildManager.GetCompiledAssemblyじゃなくてBuildManager.GetTypeでAppDomainに読み込まれてるTypeを取得(もちろんIRouteRegistrarの実装)して実行すればよろしな流れ。