2010年7月6日火曜日

DeserializingModelBinder

モダンチョキチョキズを知ってるのはどのくらい少数派なのかが気になる今日この頃。

ASP.NET MVC 2 Futuresに含まれているHtml.SerializeとDeserializeAttributeをクラスのプロパティに対して適用するのにお悩みな方へ。DeserializeModelBinder自体がなぜかDeserializeAttributeクラスのprivate sealedクラスと定義されてしまっているので、全く同じものを以下のように定義することで、ModelBinderAttributeを指定して比較的簡単に出来るようです。

  public class DeserializingModelBinder : IModelBinder
  {

    private readonly SerializationMode _mode;

    public DeserializingModelBinder() : this(SerializationMode.Plaintext) { }
    public DeserializingModelBinder(SerializationMode mode)
    {
      _mode = mode;
    }

    public object BindModel(ControllerContext controllerContext, 
ModelBindingContext bindingContext) { if (bindingContext == null) { throw new ArgumentNullException("bindingContext"); } var vpResult = bindingContext.ValueProvider
.GetValue(bindingContext.ModelName); if (vpResult == null) { // nothing found return null; } MvcSerializer serializer = new MvcSerializer(); string serializedValue = (string)vpResult.ConvertTo(typeof(string)); return serializer.Deserialize(serializedValue, _mode); } }

↑これはソースからコピペです。MvcSerializerが使えなかったらどうしようかと思いましたが、こちらは平気ですね。

  public class Division
  {
    public string Name { get; set; }
    public Person Boss { get; set; }
    public People People { get; set; }
  }

  [Serializable]
  [ModelBinder(typeof(DeserializingModelBinder))]
  public class People : List<Person>
  {}

  [Serializable]
  public class Person
  {
    public string Name { get; set; }
    public DateTime MemorialDay { get; set; }
  }

↑このようなモデルクラスたちを定義してみました。あえてPeopleクラスを定義しているのはModelBinderAttributeがプロパティに指定できないからです。DeserializeAttributeにいたってはParameterにしか指定できないし、ModelMetadataProviderらへンに手を入れる必要がある気がしなくもなく(たぶんメタデータを見てModelBinderを切り替えるような仕組みでしょうか)、難しそうだったので使っていません。

このようにクラスを定義した上で、ModelBinderAttributeでModelBinderを指定する方法が比較的簡単でスマート(?)じゃないかと思います。こうしておくとアクションでは何も意識する必要なく以下のようにアクションパラメータを生成してくれるようになります。

  public ActionResult Division()
  {
    var model = new Division
    {
      Name = "ブチャラティチーム",
      Boss = new Person { Name = "ブローノ・ブチャラティ", 
MemorialDay = new DateTime(2010, 1, 1) }, People = new People { new Person {Name = "ジョルノ・ジョバァーナ",
MemorialDay = new DateTime(2010, 2, 1)}, new Person {Name = "レオーネ・アバッキオ",
MemorialDay = new DateTime(2010, 3, 1)}, new Person {Name = "グイード・ミスタ",
MemorialDay = new DateTime(2010, 4, 1)}, new Person {Name = "ナランチャ・ギルガ",
MemorialDay = new DateTime(2010, 5, 1)}, new Person {Name = "パンナコッタ・フーゴ",
MemorialDay = new DateTime(2010, 6, 1)} } }; return View(model); } [HttpPost] public ActionResult Division(Division model) { return View(model); }

Viewは以下のようにシンプルです。

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<MvcApplication1.Models.Division>" %>
<%@ Import Namespace="Microsoft.Web.Mvc" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
  Division
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

    <h2>Division</h2>

  <% using(Html.BeginForm("Division","Home")) { %>
    <%= Html.EditorFor(m=>m.Name) %>
    <h3>Boss</h3>
    <%= Html.EditorFor(m=>m.Boss) %>

    <h3>People</h3>
    <% foreach (var person in Model.People) { %>
      <%= Html.DisplayFor(m=>person) %>
    <% } %>

    <%= Html.Serialize("People",Model.People) %>
    <input type="submit" value="送信" />
  <% } %>
</asp:Content>

これを実行するとこのようにきちんと復元してくれます。

serialize1 serialize2 serialize3

@jsakamotoさん、いかがでしょうか?

2010年7月4日日曜日

Razorのポテンシャル

Introducing “Razor” – a new view engine for ASP.NET - ScottGu's Blog

↑これ読みました?

Razor View Syntax

↑これも。

これね、よくよく考えたらかなり凄いことに取り組んでるんだと思います。そもそもASP.NET MVCはASP.NETのフレームワークにのっかったものですよね。それはどういう事かというと

  • HttpApplicationのパイプラインで処理
  • HttpContextで現在のリクエストコンテキストに関する全ての情報にアクセス可能
  • System.Web.UI.PageがIHttpHandlerの実装としてPageパイプラインを処理しつつレンダリング

ですね。ASP.NET MVCのViewでは、このフレームワーク(Pageに関する部分)の使い方で変わった部分はほとんどないんですが、構造上使われなくなったものといえばコントロールツリーの構築とポストバックです。コントロールツリーに関しては「runat="server"なコントロールを使ってる場合には作られるじゃないか」と言われればそうなんですが、そこはPageクラスがそうだからで、Viewの性質として必須な機能じゃないのでWebFormとの対比という意味では使ってないと言うことにしましょう。ポストバックですが、これは特にMVC2になってから大きく変化のある部分で例えば以下の単純なViewでもIsPostBackはtrueになりません。

<%@ Page Language="C#" 
MasterPageFile="~/Views/Shared/Site.Master"
Inherits="System.Web.Mvc.ViewPage" %> <script runat="server"> void Page_Load() { if(IsPostBack) { // PostBack eventはいずこ?
// ここには決して入ってこない } } </script> <asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server"> </asp:Content> <asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server"> <form runat="server"> <asp:Login ID="Login1" runat="server"> </asp:Login> </form> </asp:Content>

Viewのレンダリングの仕様が変更になってるのでWebFormViewにはイベントがイベントとして認識できない形になったから(TextWriter渡しのRender呼び出し)。ようはControllerに対するPOST/PUT/DELETE/GETのHttp MethodがViewに影響を及ぼすことを是としない、という意志の表れなんじゃないか~、なんてな。

話がずれましたがRazorはこのPageクラスに依存しないViewEngineとして造られてるので、Page Pipeline(イベント)は処理されない、コントロールツリーも構築されないという事になって、ASPX構文解析→C#生成の部分と、Razorの準備コスト(テンプレートエンジンとしてのコスト)が同程度だとしても、処理コストは低くなるのでよりスケールするフレームワークになるんじゃないかと思います。

Pageクラス(WebForms)におけるPostBackという発明はPage Controllerの素晴らしい実装だと思いますが、MVCとなればControllerは役割の違うものとして分離されてるので、PostBackは不要だし、それを実装してるPageクラスはリッチすぎるでしょう。DataSetからRepositoryへのスタイルの変化も同じような考え方からきてるんじゃないでしょうかね(メモリバウンドよりCPUバウンド的な)。Pageクラス、Page Pipelineからの脱却。凄くないですか?

と、なんの根拠もないですが、妄想したりしてます。

2010年7月3日土曜日

ふぉー!フォー!デレシシシ。

なんかね~、そっけない箱がね~、あったんですよ。

IMG_0005


でも、開けるとピカピカな箱なんですよ。

IMG_0006


長かった。オンラインショップで予約した負け組で始まった今回の機種変。サイトダウンにめげずF5アタックを繰り返すも当日予約は途中で断念。翌朝7:30にはすんなり予約できたから、これは当日入手期待大だと妄想。それまでiPhone 3GにiOS4を入れて気分だけでも味わってやろうとしたら信じられないくらいパフォーマンス悪くてなんどもたたき壊してやろうかと思う辛い日々。だけど発売日を過ぎてもなんの連絡も無く。ちなみに受付番号下6桁は818XXX。一体全体この放置プレーはなんだ。そんなこんなで6/30にメールが来た。遅すぎるし放置しすぎ。遅れております、のメール1つで全然違うのになぜ放置。まぁ、いいか。

で、箱。長かった。

さて、アクティベーションしようかとしたところ、9:00~19:00が受付時間らしく何も出来ず。敗北感。仕事変えようかと本気で思った。

IMG_0009

もろもろ設定完了して使ってみると3Gとは比べものにならないくらいのパフォーマンスで操作が快適。さすがですぜ!

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分で話すのはさすがに無理ですね。詰め込みすぎました。

dotnetConf2015 Japan

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