2010年6月22日火曜日

千の夜をこえて

いやはや、以前100を超えた時に次は500を目標にする!と言ってからほぼ1年?

stackoverflow

長かった。やっと500を超えた(いつマイナス評価がついて500を切るかヒヤヒヤするけど)デス!

嬉し~よ~。

んで、仕事で面接したりしてるんだけど、興味がある方は採用情報 | 株式会社クロスワープここから応募してみてね!意地悪なこと聞かないよ~。コーディングのテストはあるよ~。また社員募集やってますと同じ募集要項ですよ~。テスト問題はたけはらが考えましたよ~。なので「リンクリストの循環参照検出」なんていう難しい問題ではないですよ~。ぜひどうぞ~。

2010年6月18日金曜日

yield break

最近HaackさんとこでChecking For Empty EnumerationsNull Or Empty CoalescingでEmptyなIEnumerableに対する拡張メソッドで盛り上がりを見せてるますね。面白いですよね。

でね、ふと思ったんですけどね、IEnumerable<T>の空イテレータってどう表現するのがオシャレなのかな、と。

  public IEnumerable<T> Empty<T>()
  {
    return Enumerable.Empty<T>();
  }

  public IEnumerable<T> EmptyZero<T>()
  {
    return new T[0];
  }

  public IEnumerable<T> EmptyYield<T>()
  {
    yield break;
  }

んん~。個人的にはyield breakなんだけど。

2010年5月29日土曜日

ラテアート

いったいいつまでブログ放置をし続ければ気が済むのかね。ズボラにも程がありますよ、まったく。かといってコレといって面白いことも無く。あ、最近Code Contractsが面白そうか。

と、いいつつも最近テニス始めたんですよ。

IMG_2552

最近と言っても、今年に入ってからなんでもう半年位たちますね。いやはや。でも月1くらいしか出来てないんだけど、やり始めるとテレビとかでもテニスみちゃうもんですね。フランスのレザイが要注目やで!

雨の日にランチを食べに行くカフェでたまに素敵なカフェアートをしてくれるんですけど、これがちょっとカワイイ。

IMG_2568

いきなり3Dですよ!アリス イン ワンダーランドも真っ青ですよ。

IMG_2580

4匹のクマちゃん?たぶんクマちゃん。

IMG_2586

で、iPad。あ、間違えちゃった。ラテアートと間違えちゃった。てへ。

なんせ本を読むのとビデオを見るのをメインの用途に考えてるんだけど、本ってどうやったら見れるんだろ。Kindle入れとけばいいのけ?それともCloudReader?

ってことで、↑の写真はCloudReader入れてるところ。KindleってAmazon.comのアカウントじゃないとダメなんすかね。登録してとりあえず無料の本を買ってみたけど...。

もっと気軽に普通にPDFをみるのもいいかと思うも、そもそもPDFをどうやってiPadにコピーしたらいいのかよくわかんないじゃないか。Dropboxが入れてあるからそっちからPDF開いてみたら普通に見れたけど、なんかそんな面倒なハズない!と調べてみたらなんてことなかった。iTunesからD&Dで転送出来た。まずはPDF(いろんな公開ドキュメント)が見れるようになるだけでも超便利。iPhoneで見るとか小さすぎてありえない。見れるのと、ちゃんと読めるっていうのとこんなにも違うのかと電子書籍の衝撃ですよ(この本とても面白いのでおすすめ。あとアーキテクトの審美眼も面白かった)。

来週からの通勤が楽しみです。

そうそうそうそう。社員募集してるみたいデス。興味があればどうぞ。

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アプリのユニットテストとしては好感触。今後も精進していきます。

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

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

dotnetConf2015 Japan

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