2009年7月4日土曜日

T4MVC

べーさんも気になってるように、ASP.NET MVCでは文字列(マジックストリング)を指定して、リンクやURLを生成するのが普通ですね。

    <% using(Html.BeginForm("Index","Home")) { %>
      <% = Html.TextBox("Name") %>
      <input type="submit" value="ポスト" />
    <% } %>

例えば、↑こんな感じで書くと↓こんな感じの出力に。

    <form action="/" method="post">
<input id="Name" name="Name" type="text" value="" /> <input type="submit" value="ポスト" /> </form>

リンクの場合もそう。

    <% = Html.ActionLink("ホーム", "Index") %>

↑こんな感じでしょう。URL生成はコントローラとアクションを指定したり、ルーティング名を指定したりするのが、今までのオーソドックスな書き方。

そんな状況は誰も嬉しくないよ!だってアクション名変えたら、指定してる箇所全部検索して変更しなきゃ行けないし、1文字でも間違ってたらちゃんと出力してくれない。あぁ、誰か助けて。

そもそもの始まりは↓ここからでした。

Angle Bracket Percent : A BuildProvider to simplify your ASP.NET MVC Action Links

BuildProviderを使ってASP.NET実行時に、CodeDom使ったコンパイラを走らせてがんばる方法(自アセンブリをリフレクションで探索)。なるほど~、と。ただ、やっぱり応用しにくいっていうか、コードが書きにくい感じで、その時はそれほど便利でもないかな~、なんて油断してました。それでも、この時点でHtmlHelperを拡張して、文字列指定じゃなくて関数指定でリンク生成出来てました。この辺でPhilさんもエントリ上げてて、ActionNameで別名使ったときにこれだと対応出来ないから、その辺上手いこと処理出来るのを次のバージョンに向けて考えてます的なことを言ってた気がする。

その後、いろいろ試行錯誤があって、出てきたのが↓これですよ。

Angle Bracket Percent : A new and improved ASP.NET MVC T4 template

CodeDomじゃなくてT4でいいんじゃね?と気がついたんでしょうか。コード生成するならこういうテンプレートエンジン使った方が断然生産性が高いしね。このときはまだMvc-CodeGenっていう名前だったんですが、これを更にブラッシュアップして出てきたのが↓。

Angle Bracket Percent : T4MVC 2.2 update: Routing, Forms, DI container, fixes

ちょっとバージョンアップしたんだけど、T4MVCに名前変わってからはMVCのソースが公開されてるCodePlexに組み込まれました(アドインだからこういう言い方はおかしいかも?)。

ASP.NET - Release: ASP.NET MVC v1.0 Source

ドキュメントなんてかけらもないので、どういう物かはソースを見て判断しましょう。まぁ、TTファイルの最初に書かれてる内容がそのままなんですけどね。

とにかくまず、自分のMVCプロジェクトのルートにT4MVC.ttをコピー。準備はこれだけ。後はT4なんで勝手にコード生成してコンパイルしてくれるので、すぐに使い始められます(最初にApp_Codeに放り込んでみたら全然動かなくてビックリした)。

t4mvc1

大枠でMVCという静的クラスが生成されて、そこからたどっていく感じになります。ASP.NETでも自動生成されるグローバルクラスにASPっていうのがあるけどそれと同じような感覚ですね。とりあえず、最初に書いたサンプルをT4MVCで書くとどうなるか、ですが、↓こうです。

    <% using(Html.BeginForm(MVC.Home.Actions.Index, MVC.Home.Name)) { %>
      <% = Html.TextBox("Name") %>
      <input type="submit" value="ポスト" />
    <% } %>

    <% = Html.ActionLink("ホーム", MVC.Home.Index()) %>

リンクに表示する部分以外のマジックストリングが無くなりました。しかもこのMVCクラスは動的なので、アクションやコントローラの名前を変更したら、コンパイルエラーが起きるので、変更し忘れともおさらばですよ!素敵です。

これだけじゃなくてデスね、Linksというクラスも同時に生成されるんですが、そこにはContentフォルダとScriptsフォルダに含まれてるファイル達のリンクが生成されます。

    <% = Links.Content.Site_css %>
    <% = Links.Scripts.jquery_1_3_2_js %>

↑こんな感じで。

さて、ここでActionNameAttributeをつけたアクションはちゃんと指定出来るのか気になるところなので、簡単に実験してみましょう。

    [ActionName("Index2")]
    public ActionResult KoukaiShitakunaiNamaeNoAction()
    {
      return View("Index");
    }

こんなのを作成してみる。保存したりすると、勝手にT4MVCが動く。で、ViewでIntelliSenceをきかせてみると~?

t4mvc2

残念!ActionNameまでは見てくれませんでした。Index2が出てくるのを期待したんだけど。実行結果も残念ながら。元の名前のままでした。がんばって自分でtt書き換えてActionName属性を見るようにするか、おとなしく新しいバージョンを待つか。どっちが男らしい?

ところで、このT4MVCを実行するとすべてのコントローラはpartialクラスになって、すべてのアクションはvirtualが自動でくっつきます。どうしてかというと、ActionResultを引数に受け取るHtmlHelper拡張に渡して、URLを生成しやすくするためですよね。内部で独自クラスに派生させたコントローラを生成し、アクションをオーバーライドしてるんですね。なんとも強引な方法。コントローラ書き換えられるのが気持ち悪い!って人にはお勧めしないですけど...。

お試しあれ!

わんまるとキャプテンわん

横浜開港150周年記念キャラクターのたねまる。最近まで知らなかったけどこんなのいたのか。電車の車体に貼られてるのを見て知った。

たねまるドットコム|横浜開港150周年マスターライセンシーオフィス公式サイト

tanemaru

そして、どうやら彼女がいるらしい...。

で、キャプテンわん。わんまるとか適当な名前で教えられたけど、これも横浜がらみ。ハマスポのキャラクターらしい。

wan

キャプテンわんコーナー

「落ち込んだら走れ!」って言われた。上のページでちょいちょいメッセージが切り替わって、熱血漢なところをアピール。さすがハマスポ。

写真

そして↑これがガッカリなキャプテンの写真(拡大してみるとガッカリ度さらにアップ)。あまりにもガッカリな状況だ...。こんな写真をアップしてたら訴えられるんじゃないかとヒヤヒヤする。前にも書いたかな~、この横浜市の体育協会の広報資料になぜかたけはらさんの写真が載ってるよ!ファンはゲットしときましょう。

そんな話はいいとして、今日は負けられないレギュラーシーズン最終戦。負けてもギリギリプレーオフには行けるらしいけど、あまりにもギリギリだと初戦から強豪と対決になって気が滅入るので、今日の試合は何が何でも勝っておきたいところ。とは言いつつも、最終戦の相手が一番の強豪だったりしないですかね。ファルコンズって...。ワカバヤシ兄弟で出るって空気読めてないゴールド戦士には出場をご遠慮願いたい。朝一8時30分開始の試合だからあわよくば来てくれるなと思ってたけど、ちゃんといた。「S40と木場マーボーズの試合だけは絶対出る」と本人談。君たちはアレだろ、消化試合というか、調整試合だろ。こっちはプレーオフかかってんだ。もうちょっと空気読んでくれよ。とりあえずケースケは来て無くていなくてホッとした。

ところで、前回の試合の感想を宿題にしといたんだけど、その提出がなかなか無くてさ。ミサキ姫に問い詰めたら、今回の試合と合わせて2試合分をまとめて提出すると、自分でハードル上げてきた。若さって怖い。平成生まれって強い。

試合内容はね~、あれっすよ、大接戦っすよ。試合開始早々相手のペナルティー(やられた身としてはそうでもなかったけど、ここはありがたくチャンスを頂戴しとく)で、パワープレー。シンゴ→タケ→オグでバシッと決めて、さい先のいい試合展開。聞いた話によると、相手ゴーリーはウォールがかかってるらしい。今日の試合次第でベストゴーリーになるほどの鉄壁君だって。それが、序盤そうそうに失点しちゃったもんだから、気持ちも折れるってもんですよ。やったね。そっから、取りつ取られつの試合展開で、あ!っという間に3分(早っ)。まだ同点だったけど、ここで点を取れればというところで、セト君が決めてくれて1点リードで逃げ切り体制。に、持って行きたかったけど、直後のフェイスオフでツトム君に真ん中から突破されてわずか8秒で再度同点。なんじゃそれ!でも、最後はオグさんがバシッと決めて見事な勝利。もう満足。ぶっちゃけプレーオフもういいじゃん、ってくらい出し切った。他の試合結果次第だけど、カネコさんがウォールになりそうなのもちょっと楽しみ。

そもそも、今日もうちはゴーリーがいないはずで、順番で自分がやるはずだったんだけど、どうしてもやりたくなくてタクちゃんに無理を言ってお願いしてきてもらったんだよね。しかも2週間前から連絡を取って、なんとか都合をつけてもらうという周到ぶり。おかげで勝てたよ!タクちゃんのグレートセーブで何度も助けられたし、シンゴの必死の守りで後半2失点で抑えられたのがよかった。と、言うことは前半の自殺点2点が無ければもっと楽に勝てたってこと?誰だ自分たちのゴールに蹴り込んだの!

...すいません。シンタロも一緒に謝っとけ。

久しぶりに7得点で7ポイントの大活躍な自分に拍手。

2009年6月28日日曜日

こんな週もあるか

いや~、疲れた。朝からずっとスケート履きっぱなしはいつ以来だろ。レフェリー自体がずいぶん久しぶりだった気がするな~。なによりノブヒコにあうのは2年ぶり?でも、久しぶりに一緒にホッケー出来て楽しかった。そんな久しぶり続きで箸も持てないほど疲れたけど、1試合も勝てなかったっていうのが一番の驚きかも。全然負けてる気がしないのに、結果2敗2分け?すんごい面白かったから、いいんだけどさ。なんか、こう、あれれ~、な感じだよね。

Google Page Creatorの移行をさっさとすませろと督促メールも来たことだし、サーバー引越中...。なんかいろいろやることあるな~。

2009年6月21日日曜日

RouteHandlerはシングルインスタンス

いや~、サブジェクトの件、全然知りませんでした。

ASP.NET MVCはControllerクラスのAction実行でViewを返すじゃないっすか。ControllerはDefaultControllerFactoryがインスタンスを作るんですよね。で、IControllerさえ実装してればそれはつまりControllerであると。なので、IController.Executeだけを実装したサンプルを書いて、まずはそれを確認。

using System;
using System.Web.Mvc;

namespace RouteHandler.Controllers
{
  public class SimpleController : IController
  {
    private DateTime _createTime = DateTime.Now;
    public void Execute(System.Web.Routing.RequestContext requestContext)
    {
      requestContext.HttpContext.Response.Write(string.Format(@"<html>
<body>
<h1>Simple Controller</h1>
アクションやら完全無視!<br />
{0}
</body>
</html>", _createTime));
    }
  }
}

簡単なコードです。特にこれと言って変なところは無いですね。ただ、こういう使い方をするとActionなんて無いし、この中に定義したとしても、RouteDataにアクションへの情報を参照してInvokeするコードが無いので無視するだけです。ようはActionInvokerに相当する機能がないって事です。

わざわざ_createTimeを取っているのは、この後の確認作業で理由が判明する予定。SimpleControllerという名前なので、デフォルトのルーティング設定だけで、このコントローラは実行まで行くことが出来ます。動かしたのが↓この画面。

route1

画面を拡大してみると分かると思うけど、URLは単純に/simpleです。で、ですよ、Controllerは単純にIHttpHandlerなんだから今度はIHttphandlerを実装してしまえば、DefaultControllerFactoryを経由させないでも出来るってことデスよ。でも、その為にはRouteHandlerが必要になります。いきなりの展開で話が分かりにくいっすね。

ルーティングモジュールがFrontControllerとしてすべての要求を全部受け入れてくれるじゃないですか(そういう言い方しないですかね)。で、その中で登録されてるルーティング情報に従ってRouteDataに色々なデータを突っ込んでくれる。RouteCollectionExtensions.MapRouteが色々セットアップしてUrlRoutingHandler(UrlRoutingHandler クラス (System.Web.Routing))を使ってそこから先はMVCの方で好きなようにやりたまえと責任が委譲されます。MvcRouteHandlerとMvcHandlerがその後を引き継いで、ごにょごにょと進んでいくけど、UrlRoutingHandlerの所を自分で実装したものに差し替えちゃえば、ControllerFactoryなど経由せずIHttpHandlerを直に使えるっていう流れなんですが、こうなるとMVCじゃなくてただのRouting利用っすね...。まぁ、いいや。UrlRoutingHandlerはIRouteHandlerの実装なので、それを実装。

using System;
using System.Web;
using System.Web.Routing;

namespace RouteHandler.Libraries
{
  public class SimpleRouteHandler : IRouteHandler
  {
    private DateTime _createTime = DateTime.Now;

    public IHttpHandler GetHttpHandler(RequestContext requestContext)
    {
      var routeData = requestContext.RouteData;
      routeData.DataTokens["routeTime"] = _createTime;

      var httpHandler = new SimpleHttpHandler(requestContext);
      return httpHandler;
    }
  }
}

ただGetHttpHandlerを実装するだけデス。返すハンドラはSimpleHttpHandler。

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

namespace RouteHandler.Libraries
{
  public class SimpleHttpHandler : IHttpHandler
  {
    private DateTime _createTime = DateTime.Now;
    private DateTime _routeTime;
    
    public SimpleHttpHandler(RequestContext requestContext)
    {
      var routeData = requestContext.RouteData;
      _routeTime = (DateTime)routeData.DataTokens["routeTime"];
    }

    public void ProcessRequest(HttpContext context)
    {
      context.Response.Write(string.Format(@"<html>
<body>
<h1>Simple RouteHandler</h1>
HttpHandlerを直接使う<br />
RouteHandler : {0}<br />
HttpHandler : {1}<br />
</body>
</html>", _routeTime, _createTime));
    }

    public bool IsReusable
    {
      get { return false; }
    }
  }
}

ProcessRequestを実装しただけですが、せっかくRouteHandlerは自前なのでRouteDataを参照したいじゃないですか。なので、コンストラクタでRequestContextを受け取るようにしておいて、RouteDataを取り出しておきます。このRouteHandlerをRouteTableに登録。

      routes.Add(new Route("route", new SimpleRouteHandler()));

Global.asaxのRegisterRoutesに追加するだけです。

これを実行したのが↓これ。

route2 

時間2箇所だしてるので分かるかな~、とは思うんですが、RouteHandlerが生成された時間と、HttpHandlerが生成された時間が少しずれてます。もう少し時間をおいて試したのが↓こっち。

route3

HttpHandlerは毎回違う時間(IsReusable=falseかどうかは見ずに毎回newしてる)になってるけど、RouteHandlerは1個前の写真の時間と同じです。ピンと来た?ちなみに最初のSimpleControllerは毎回時間が更新されます。

そうなんですよ、RouteHandlerのインスタンスは使い回されるんですよ。しかも沢山のスレッドから同時に。なので、RouteHandlerの中でスレッドセーフじゃないクラスを使ったりするとちょっと切ない思いをすることになります。データベースにアクセスしようとかしたらダメですよ...。ロックなんてしたら、これまたスケールしなくなるし。

RouteHandlerにはシンプルなロジックだけを書くようにしましょう、という話でした。

2009年6月20日土曜日

暑すぎだね~

何となく、最近週末にしかエントリ出来てない気がするな~。新人だからそれもやむなし。ボスには好きなだけ(情報漏らさない限り)書いていいと言われてるけど、なかなか眠しなウィークデイっす。今日は珍しく午後から試合ってことで、朝はのんびり過ごしてたんだよね。そろそろ行くかと外に出たら、アレだね、夏だね。オレ in サマーだね。

前回の試合でウィールがひどくて滑れないと、さんざん言い訳をかましてたけど、今回はウィールも新調し、ブレードもおニューで、もう道具のせいには出来ないっていう状況を用意しました。なんせ今回の相手はヤマちゃんもいるチームだしね!うひゃひゃ。この時期恒例の首都高での気温チェックをしてみたら28度になってて、途中行くのやめてマジで帰ろうかと心が折れかかった。

そういえば、iPhone OS 3.0出てるじゃないですか。速攻でアップグレードしたんですよ。いろいろ新機能があったりしてるみたいだけど、やっぱり機械を新しいのにしないとベストな状態で使えてる気がしない。なんとなくレスポンス悪かったりしてる感じが否めない。日本語入力の反応(変換候補の選択が特に反応悪し)悪くないですか?それでも、コピペが出来るのは嬉しいし、曲順が日本語順だったり、細かい表示が改善されてたりして、凄くいいのは確かっす。テザリングが標準で使えないのが残念だけど、最近はめっきりノートPC持ち歩かないから、そこはまぁ、いっか、みたいな。出来るに越したことはないけど、調べたりしてまで使いたいかっていうとそれほどでもないか。

最近、電車通勤してて気がついたんだけど、今履いてるNIKE FREEは少しでも雨が降るとすぐにしみてきて、靴下がびちょびちょになる。もう半年くらい履いてるのに、全然知らなかった...。長靴買おうかな。

そうだ、ホッケーの試合の話だった。なんだかんだヤマちゃんもショウ君もいなくて、ありゃりゃだったけど、ゴーリーが鬼のようなセービングで全然点が取れず、まさかの敗北かと思ったけど、なんとかオーバータイムで勝利。危なかった。53本うって5点っすよ...。どんだけ止めれば気が済むんですか。で、この試合の内容はミサキ選手が後日感想文を送ってくることになってるので、詳細はこうご期待!

試合後、サヨちゃんがいたから「スズキ君は~?」って尋ねたら「10日も仕事休んだから今日は仕事に行ってる~」だって。スゴイよね、世界選手権だもんね。日本代表だもんね。「じゃ~、無事帰ってきたんだね。ケガもなく。」って言ったら「いや、それが...」と、どうも大変な事になってたみたいで、軽く記憶が飛んだりするような危ない感じの脳しんとうを起こしたらしい(とても危険な反則をされたみたい)。昨日は元気に戸塚でまたホッケーしてたらしいけど。今度ちょっと話を聞かせてくれたまえ。

2009年6月13日土曜日

全然意識したこと無かったけど

ASP.NET MVCだからどうのこうのっていう話ではないんだけど、ちょっと衝撃。

ASP.NET MVC Routing vs. Reserved Filenames in Windows - Stack Overflow

↑ここで初めて目にした時は"へぇ~"くらいだった。

bitquabit - Zombie Operating Systems and ASP.NET MVC

↑ここで2回目に目にした時は"マジやべ~"に変わった。デフォルトルート設定のままCom1Controllerを作ってアクセスしてみた。

ng1

ぬふ。404ですね。普通に開発してたら、Com1Controllerなんて作ったりしないから平気だろう、なんて甘いこと考えてたらダメですよ!

続いて、HomeControllerのIndexアクションを以下のようにしてみましょう。普通です。

    public ActionResult Index(string id)
    {
      ViewData["id"] = id;

      return View();
    }

で、Home/Index.aspxにこれまた以下のようなコードを書いたとしましょう。

  <p>
  id="<%= Html.Encode(ViewData["id"]) %>"
  </p>
  
  <div></div>
  <dl>
    <dt>ダメなパターン</dt>
    <dd>
    <%
      foreach (var id in new[]{"COM","LPT"}.SelectMany(
              l=>Enumerable.Range(1, 9).Select(r=>l+r))
                           .Union(new[]{"CON","AUX","PRN","NUL"}))
        Response.Write(Html.ActionLink(id, "Index", new { id }) + " ");
    %>
    </dd>
    <dt>いいパターン</dt>
    <dd>
    <%
      foreach (var id in new[] { "COM0", "COM1029", "id", "PRINTER", "NULL" })
        Response.Write(Html.ActionLink(id, "Index", new { id }) + " ");
    %>
    </dd>
  </dl>

ワクワクしますな。ドキドキが止まりませんな。

ng2

こんなのが表示されますね。もうあからさまに怪しいのが見てとれます。早速リンクをクリックしてみようじゃないですか。

まずは、下段の"COM0"。普通上段からだよ...。まぁ、いいでしょう。

ng3

ちゃんと出ます。続いて"COM1209"、"NULL"も試してみたけどちゃんと出ます。

今度は上段の"COM1"。

ng4

ぎゃふーん!!

でも、変な値が出るわけじゃないから、まぁいいか。そんなURL入れるのが悪い!なんて思ってたら変な障害出たりしてあたふたすることになりかねないですよ!だって、IDに文字列入力許すような設計って普通じゃないですか。んで、ルーティングのパラメータにID含むようなURL設計って当たり前すぎるじゃないですか。でも、ID入力時に"COM1"やら"NUL"やら"AUX"やらはじくような処理を入れるのが当たり前なわけじゃないじゃないですか。当たり前なんですか?そーだったらすいません。

何となくRouteCollection.RouteExistingFiles プロパティ (System.Web.Routing)でtrueにしてみたけど、ダメだったから、そういうもんだとして入力値のチェックをちゃんとやろうと思うところです。

2009年6月7日日曜日

Json.NETとStringTemplateでお気楽HTML出力

暇だったんですよね。で、暇つぶしにJSONからStringTemplateを通してHTMLをはき出させてみたんです。何となくですけど、適当にデータを定義しておいて、テンプレートに当てはめてHTMLを出力するっていうツールがないものかと探してみたんだけど、どーにも楽ちんそうなのが見あたらなくて。あ、ちなみに5月の話です。

データはXMLでも良かったんだけど、書くのが面倒になるのもやだし、テンプレート解釈とかは作りたくないし、ってことで、Json.NET - James Newton-KingStringTemplate Template Engineで書いてみたっす。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Antlr.StringTemplate;
using System.IO;
using Newtonsoft.Json.Linq;

namespace STHtml
{
  public class STGenerator
  {
    private const string INPUT_PATH = "input";
    private const string ROOT_PROPERTY = "templateItems";
    private const string FILENAME_PROPERTY = "output_path";
    private const string FILENAME_FORMAT = "output{0}.txt";

    private STSetting _setting;

    public STGenerator(STSetting setting)
    {
      _setting = setting;
    }

    public void Execute(IOutput output)
    {
      var group = new StringTemplateGroup("htmlTemplates", INPUT_PATH);
      var json = LoadJSON();

      if (json == null)
        return;

      // 
      var list = (from item in json[ROOT_PROPERTY]
                  select item).ToList();

      foreach (var item in list)
      {
        // テンプレートの読み込み
        var st = group.GetInstanceOf(_setting.TemplateFileName);

        // ファイル名は?
        var filePath = GetOutputFilePath(item, list.IndexOf(item));

        // 生成
        var generated = GenerateFromJSON(st, item);
        if (generated == null)
          return;

        // 出力
        output.Print(filePath, generated);
        Console.WriteLine("output -> " + filePath);

      }
      Console.WriteLine("{0} file(s) template generated.", list.Count());
    }

    private string GetOutputFilePath(JToken item, int itemIndex)
    {
      // ファイル名は?
      var fileName = (from t in item
                      select item[FILENAME_PROPERTY]).FirstOrDefault();
      var filePath = Path.Combine(_setting.OutputPath,
        fileName != null ?
          fileName.ToString().Replace("\"", "") :
          string.Format(FILENAME_FORMAT, itemIndex)
      );

      return filePath;
    }

    private JToken LoadJSON()
    {
      string jsonText;
      JObject json = null;

      try
      {

        jsonText = File.ReadAllText(Path.Combine(INPUT_PATH, _setting.JsonFileName));
        json = JObject.Parse(jsonText);
      }
      catch (Exception e)
      {
        Console.WriteLine("データファイルの書式が間違ってるか、ファイルがないか...");
        Console.WriteLine(e.Message);
        json = null;
      }

      return json;
    }

    private string GenerateFromJSON(StringTemplate st, JToken json)
    {
      try
      {
        // JsonからDictionaryに変換
        // JObjectからそのままはStringTemplateが無理さ
        var dict = JsonToDictionary(json) as Dictionary<string, object>;
        foreach (var kv in dict)
        {
          st.SetAttribute(kv.Key, kv.Value);
        }
      }
      catch (Exception e)
      {
        Console.WriteLine("変換に失敗したよ。テンプレートがおかしいと思われる。");
        Console.WriteLine(e.Message);
      }

      return st.ToString();
    }

    private object JsonToDictionary(JToken token)
    {
      Dictionary<string, object> result = new Dictionary<string, object>();

      // なんか美しくないね。
      // 値セットと値戻しが同列だしな~。まぁ、いっか。
      foreach (var node in token)
      {
        if (node is JProperty)
        {
          // プロパティ型(name:value)なら取得
          var prop = node as JProperty;
          // 配列([...])かオブジェクト({...})なら再帰
          if (prop.Value.Type == JsonTokenType.Array ||
              prop.Value.Type == JsonTokenType.Object)
          {
            result[prop.Name] = JsonToDictionary(prop);
          }
          else
          {
            // その他の値型なら文字列化
            var value = prop.Value.ToString()
                                  .Replace("\"", "");

            result[prop.Name] = value;
          }
        }
        else if (node is JArray)
        {
          // 配列型なら戻す(再帰の時にしか処理しないもん)
          var arr = node as JArray;
          var list = new List<object>();
          foreach (var item in arr)
          {
            list.Add(JsonToDictionary(item as JToken));
          }
          return list;
        }
      }

      return result;
    }
  }
}

説明するのが面倒なんでソースです。こんな適当な感じでもそれなりに動くよ~。JsonからHTMLへの埋め込み時にEncodeかかってないからその辺は気をつけましょう。

使い方なんですけど(使う人がいるとは思えないけど)。

まずはinputフォルダのdata.jsonファイルにデータを書き込んでいきます。例えば↓こんな感じです。

{templateItems:
  [
    {
      output_path:'test1.html',
      title:'ページ1',
      subject:'うぎょぎょ<br />ぼへ',
      gallery:[
        {src:'1.jpg', alt:'', title:'説明文を書きましょう'},
        {src:'2.jpg', alt:'', title:'説明文を書きましょう'},
        {src:'3.jpg', alt:'', title:'説明文を書きましょう'}
      ]
    },
    {
      output_path:'test2.html',
      title:'ページ2',
      subject:'うぎょぎょ<br />ぼへ',
      gallery:[
        {src:'1.jpg', alt:'', title:'説明文を書きましょう'}
      ]
    },
    {
      output_path:'test3.html',
      title:'ページ3',
      subject:'うぎょぎょ<br />ぼへ',
      gallery:[
        {src:'1.jpg', alt:'', title:'説明文を書きましょう'},
        {src:'2.jpg', alt:'', title:'説明文を書きましょう'}
      ]
    }

  ]
}

全体が一つのObjectです。で、ルートではtemplateItemsっていう名前の配列を定義がルールです。templateItems配列に入れる各オブジェクト(アイテムオブジェクト)の書式は気をつけて統一する必要あり。こんな時にJSONスキーマが役立つんだろうな~。面倒なので気をつけるっていうルールで。アイテムオブジェクトはどんな形式でもOK、のはず。最初に提示したソースのJsonToDictionary関数が再帰でその辺上手くやってくれるようにしてます。でも、JArray型とJProperty型以外は想定してないので、まぁ、その辺は雰囲気で。

あ、アイテムオブジェクトに絶対に入れなきゃ行けないのが出力ファイル名をセットしたoutput_path。上記の例だとoutputフォルダにそのまま出す感じなんですが、例えば"output_path:’folder1/default.html’"とかってしておくと、outputフォルダ内にfolder1フォルダを作成して、その中にdefault.htmlを出力します。

次にテンプレートファイルとして↓こんなのを用意。

<!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>
  <meta http-equiv="content-type" content="text/html; charset=utf-8" />
  <meta name="keywords" content="">
  <title>$title$</title>
</head>
<body>
<h1>$title$</h1>
<div>
  <h2>$subject$</h2>
  <ul>
    $gallery:{g|
    <li><img src="$g.src$" alt="$g.alt$" title="$g.title$" /></li>
    }$
  </ul>
</div>
</body>
</html>

後は実行するだけ。そうすると↓こんなのが出力されました。

sthtml1 sthtml2

またしてもプロジェクトファイルは添付しておくのでご自由に。

dotnetConf2015 Japan

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