ラベル ASP.NET MVC の投稿を表示しています。 すべての投稿を表示
ラベル ASP.NET MVC の投稿を表示しています。 すべての投稿を表示

2011年10月11日火曜日

AllowHtmlの深淵

月刊たけはらブログ。と、なってしまいましたね。

Understanding Request Validation in ASP.NET MVC 3

唐突ですが、↑随分前のこのエントリ、ずっと気になってたんです。ValidateInputAttributeってあるじゃないですか、Ver1の時から。これを指定したControllerやActionは不正なリクエスト文字列が含まれるとHttpRequestValidationExceptionをthrowするやつです。

これ、もともとASPXのPageディレクティブのValidateRequestとしても使われてますね。Pageディレクティブですよ。ココ重要。

MVC3になってからAllowHtmlAttributeって言うモデルプロパティに指定できるやつが追加されたじゃないですか。これ、とても不思議に思ってたんです。でも、まぁいっか、思ったように動くし、と、見て見ぬふりしてたんですけど、どーも、気持ち悪くてですね。

どういうことかというと、PageディレクティブだとHttpRequest.ValidateInput()に直接連携するというのはすんなり受け入れられるけど、それってリクエストコンテキスト単位のチェック(Form,QueryString,Cookieが送信されてきたリクエスト自体)で、個々の要素っていうか値単位のチェックなわけじゃない、っていう仕組みじゃないですか。なのでAllowHtmlってソレまでの仕組みとは全然違うものを実装してるということになりましょう。そうなると、どういう仕組で実装されてるのかキニナル。気になる。

で、先のブログのエントリなんですけど、正直かなり難しそうだなー、と思ってずっと敬遠してました。

さっそく原点、ASP.NET MVC1のControllerActionInvoker見てみます。

[SuppressMessage("Microsoft.Performance", "CA1804:RemoveUnusedLocals", MessageId = "rawUrl",
  Justification = "We only care about the property getter's side effects, not the returned value.")]
private static void ValidateRequest(HttpRequestBase request) {
  // DevDiv 214040: Enable Request Validation by default for all controller requests
  // 
  // Note that we grab the Request's RawUrl to force it to be validated. Calling ValidateInput()
  // doesn't actually validate anything. It just sets flags indicating that on the next usage of
  // certain inputs that they should be validated. We special case RawUrl because the URL has already
  // been consumed by routing and thus might contain dangerous data. By forcing the RawUrl to be
  // re-read we're making sure that it gets validated by ASP.NET.

  request.ValidateInput();
  string rawUrl = request.RawUrl;
}

HttpRequestBaseのValidateInputを呼んで、RawUrlにアクセスするだけのシンプルな実装ですね。コメントから苦労が垣間見れます。

ASP.NET MVC3の同じ部分を見てみます。

internal static void ValidateRequest(ControllerContext controllerContext) {
  if (controllerContext.IsChildAction) {
      return;
  }

  // DevDiv 214040: Enable Request Validation by default for all controller requests
  // 
  // Earlier versions of this method dereferenced Request.RawUrl to force validation of
  // that field. This was necessary for Routing before ASP.NET v4, which read the incoming
  // path from RawUrl. Request validation has been moved earlier in the pipeline by default and
  // routing no longer consumes this property, so we don't have to either.

  ValidationUtility.EnableDynamicValidation(HttpContext.Current);
  controllerContext.HttpContext.Request.ValidateInput();
}

微妙に違いますね。Microsoft.Web.Infrastructure.DynamicValidationHelperのValidationUtility.EnableDynamicValidationですね。誰や!?この旅、ここから長いです。長い割に身が無いです。できることはわかってるんだから。

先のブログエントリを、ちらっと見てるという前提で話を進めますが、MVC3だとHttpRequestのForm,QueryStringへのアクセスでもHttpRequestValidationException発生しますね。<httpRuntime requestValidationMode="2.0"/>はweb.configに入れてません。入れてないとASP.NET4ではデフォルトで全てのコレクションに対する検証がOnになるからです。

ASP.NET 4 Breaking Changes - ASP.NET Request Validation

ASP.NET 2/3.0/3.5と同じ挙動にしたいときだけ。<httpRuntime requestValidationMode="2.0"/>を指定しましょう。必要ないと思いますけど。んで、なんでここに手を入れてるのかというと、検証処理自体細かく制御できるように拡張ポイントを追加したからですね。requestValidationModeともう一つrequestValidationTypeというのも指定できるようになってますが、こっちで検証クラス(検証とずっと言ってるけど通常の入力検証じゃなくてリクエスト検証のことなので誤解しないでね!)を指定してカスタム出来るようになってます。

標準テンプレートのまま1つソリューションを作成します。HomeControllerのIndexで以下のようにQueryStringにアクセスするとします。

public ActionResult Index()
{
  var p1 = Request.QueryString["p1"];

  ViewBag.Message = "Welcome to ASP.NET MVC!";

  return View();
}

なんの変哲もないコードですね。これを実行してブラウザのアドレス欄に”?p1=<a>a</a>”なんていうのを足してアクセスしなおすと、例のエラーでます。そりゃそうです。The XSSです。

rv1

rv2 rv3

今度はそこに↓こんなずるいクラスを追加してrequestValidationTypeに指定。

using System.Web;
using System.Web.Util;

namespace ReqValidate.MVC3
{
  public class PassRequestValidator : RequestValidator
  {
    protected override bool IsValidRequestString(
        HttpContext context,
        string value,
        RequestValidationSource requestValidationSource,
        string collectionKey,
        out int validationFailureIndex)
    {
      validationFailureIndex = -1;
      return true;
    }
  }
}

ひどい。

<httpRuntime
  requestValidationType="ReqValidate.MVC3.PassRequestValidator"/>

このまま先のURLにもう一度アクセス。

rv4

同じようにダメそうなQueryStringを指定してるのに、今度はエラーになりませんでした。より厳しい条件を指定したいときなんかはカスタムすればいいですね。同じ仕組でRawUrlのチェックで許可したい文字を増やす事や、拒否したい文字を減らすこともできるでしょう。残念ながら標準でできるようになってるので、そんなことする意味は無いですけどね。

ASP.NET 4 and Visual Studio 2010 Web Development Overview - Expanding the Range of Allowable URLs

ただ、この辺突き詰めていけば、AllowHtmlにたどり着くのかというと、そうでもない、っていうね。なんでしょーね。この話、必要なかったですね。

話を戻すと、requestValidationModeが2.0の時と、4.0(デフォルト)の時での挙動の違いとして、チェックが有効になるタイミングの違いがあるようです。2.0だとBegineRequestではまだ有効になってなくて、4.0では有効になってます。

Global.asaxでブレークを仕掛けるとわかります。なるほど。Request.ValidateInput()でチェックフラグがオンになるので、以降のForm/QueryStringへのアクセスでチェックがかかるという仕組みです。

requestValidationMode=”2.0”の時。

rv5

Microsoft.Web.Infrastructure.DynamicValidationHelper.ValidationUtility.IsValidationEnabled(HttpContext.Current)で確認しましょう。

falseです。

なので、このタイミングで上記スクリーンショットのQueryStringを参照すると、XSSな値が入っていても、このタイミングではエラーとなりません。

でも、Request.ValidateInput()でマークフラグをセットすると、同じものが今度は例外となります。

rv6

続いて、同じコードでValidateInput()を呼ばずに、requestValidationMode=”4.0”の時。

rv7

IsValidationEnabledもtrueで、例外も起きます。

面白いのは、同じコードを続けて実行すると二回目以降は例外が発生しないところです。これは興味深い挙動ですね。

rv8

不思議な仕様ですねー。だって、この状態でも、Request.Unvlidated().QueryStringでオリジナルを取得することはできるんだから。過去の遺産だったりするのかなー。

rv9

やっと、本題。AllowHtmlを成立させるために必要なのはモデルのプロパティ名毎に検証を実施しないとダメですが、ここまでの流れで分かる通り、マッピング対象のコレクションにアクセスした段階で検証が発動する仕組みなので、このまますんなり行くとは思えません。がしかし、Reques.QueryStringやRequest.Formのアクセスは要素に関係なくエラーになるにもかかわらず、Actionへの引数となるマッピングの場合はマッピングが発生しない要素のにたいする検証は実施されてません。

rv10 rv11

どうやって実現してるんでしょうね。Form/QueryStringへのアクセスはすべて引っ掛けられてるなら途中介入するなんてできそうな気がしないです。

エントリを読み進めるとどうやら、コレクションが内部で保持しているArrayListとHashtableを置き換えてるようです。何と置き換えてるかはエントリの通り、LazyValidationArrayListとLazyValidationHashtableです。Microsoft.Web.Infrastracture.DynamicValidationHelper配下。だからといって、コレでも要素アクセスに限り検証し、プロパティマッピングでは検証しないというルールに繋がらないです。ふむむー。

改めてMVC3のソースを確認すると、そこにはIUnvalidatedRequestValuesと見慣れないインターフェースを使ったValueProviderたちがいました。あれれ?これの実装クラスがUnvalidatedRequestValuesクラスのインスタンスを内包したクラス。UnvalidatedRequestValuesといえば、Request.Unvalidated()で取得できる検証スキップコレクション。

さらに各種ValueProviderはNameValueCollectionValueProviderを派生したもので、IUnvalidatedValueProviderを実装してます。

public interface IUnvalidatedValueProvider : IValueProvider {
  ValueProviderResult GetValue(string key, bool skipValidation);
}

ははーん。見えてきましたね。MVC3からのValueProviderたちはコレを実装した形になっているので、マッピング対象の値をForm/QueryString等のコレクションから取得する際に、こっちのGetValueを呼び出すことで判断してるんですね。この中を少し見てみるとValueProviderResultPlaceholderを各ValueProviderからの戻り値とする際に、検証コレクションと未検証コレクションを切り替えて返す。

検証されるべきコレクションはインフラストラクチャの値をそのまま利用し、検証をスキップしたい時のコレクションはUnvalidatedRequestValuesを利用する。それをModelMetadataのRequestValidationEnabledから判定(AllowHtmlはここをセットするための属性クラス)。すっきりした!

途中出てきたLazyValidationArrayList/LazyValidationHashtableがインフラストラクチャが実施する検証を遅延してくれてるんだろーか。その辺はリフレクションとかしまくっててわかりにくし。FormやQueryStringにアクセスするとエラーが起きるのはそのままだから、なにが遅延かわかりにくいです。どーしてなーん?

こんな感じの実装になってるからAllowHtmlが成立するというのがわかったので良しとします。こうすることでWebPagesとも検証コードが同一のものにできるわけですね(WebPageHttpHandler.ProcessRequestInternalでEnableDynamicValidation)。この辺はSystem.Web.UI.Page派生じゃないものに対しても正しく検証を行うために必要なところですよね。Pageディレクティブないし。

ちょっとスッキリ。

2011年8月14日日曜日

Controllerを名前から生成するしHTMLを書き換えたりもしてみる

Developer @ ADJUST : ASP.NET MVC のコントローラクラスを、コントローラ名から取得したいのだけれど...結局、ぜんぶ列挙!?

前回のエントリーからはや1ヶ月以上経過したこのブログ。エアコンが壊れてて、バリバリ節電に貢献できてる気はするけど、辛い毎日でブログどころではないですよ!

で、坂本さん、上記の件ですが以下のような方法はいかがでしょうか。単純に型が欲しいだけならインスタンスは不要なので無駄な感じはしなくもないですが。必要なのがインスタンスであれば一番いい方法だと思います。

var factory = ControllerBuilder.Current.GetControllerFactory();
var controller = factory.CreateController(Request.RequestContext, "Home");
var type = controller.GetType();
factory.ReleaseController(controller);

コレと言って、自信で発明したものはなく、やってることはMvcHandlerでやってることと同じです。MvcHandlerってあれです、MvcRouteHandlerが返してくるIHttpHandlerのことです。

内部でタイプキャッシュとかしてくれてるので、パフォーマンスには自信があります!あると思います!

ここから別件。

もう暑さで脳みそが働かない...。けど、これだけじゃ坂本さんも納得しないと思うので、全然関係ない面白い機能紹介。といっても、URLRewrite 2.0の機能です。ハイパフォーマンスWebサイトの構築には必須のOutputCacheなんですが、クローラーなんかはUser AgentがCookieに対応してないと言ってくるじゃないですか。browserファイルで対応するのが王道ですが、それだと追従していくのが大変面倒くさい。

Creating Outbound Rules for URL Rewrite Module : URL Rewrite Module 2 : URL Rewrite Module : The Official Microsoft IIS Site

想定している状況を簡単にいうと、SessionのcookielessはuseDeviceProfile。PCサイトとケータイサイトを同一アプリケーションで実行。PCサイトは積極的にOutputCacheを利用し、ケータイサイトでは消極的(Viewでは使わない)に利用。この場合、PCサイトにクローラが来るとそのままではCookielessとしてセッションが生成されるので、OutputCacheにSessionIDを保持したHTMLが出力されてしまって都合悪い!

ということなんですが、伝わるでしょうか...。

OutputCacheをVaryByUserAgentにするのも手ですね。いろいろ方法はあると思うんですけど、こないだ試したのがPCサイトの場合、出力されるHTMLにSession IDが含まれることが正常系ではありえないので、PCサイトの場合、Response.FilterでURLにSession IDが含まれてたら消してしまうという方法。Response.Filterでやるよりももっと簡単なのがURL Rewrite 2.0の出力書き換えを利用するっていう方法。どうですか、面白そうじゃないですか?

ちなみにOutbound Rule用に独自Provider書いたりもできるけど、そこまでやることはあんまりないかなー、と思いますがどうでしょう。

<rewrite>
  <outboundRules>
      <rule name="Sessionless" preCondition="html" enabled="true">
          <match filterByTags="A, Area, Base, Form, Frame, Head, IFrame, Img, Input, Link, Script, CustomTags" customTags="All" pattern="(.*)/\(S\([0-9a-z]+\)\)(.*)" />
          <action type="Rewrite" value="{R:1}{R:2}" />
          <conditions>
              <add input="{REQUEST_URI}" matchType="Pattern" pattern="\)/mobile" ignoreCase="true" negate="true" />
          </conditions>
      </rule>
      <customTags>
          <tags name="All" />
      </customTags>
      <preConditions>
          <preCondition name="html">
              <add input="{RESPONSE_CONTENT_TYPE}" pattern="text/html" />
              <add input="{REQUEST_URI}" pattern="/mobile" negate="true" />
          </preCondition>
      </preConditions>
  </outboundRules>
</rewrite>
<urlCompression doDynamicCompression="false" />

だいたいこんな感じの設定です。これをWeb.configに書いておきましょう。system.webServer配下です。ケータイサイトが/mobile配下という前提です。

実験。MVCサイトをデフォルトのまま生成し、Web.configに上記設定を追加しないで動作を確認。

この状態で出力されるのは↓。

cookieless1

そりゃそうですね。中身は↓。

cookieless2

なんてことないですよね。

今度はsessionStateのcookielessをUseUriにしてみましょう。見た目は同じなのでソースだけ。

cookieless3

画像をクリックして大きくしてみるとわかりますが、Session IDを含んだURLですよね。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>Home Page</title>
    <link href="/(S(f4grns0hiatkwquk55qulbmd))/Content/Site.css" rel="stylesheet" type="text/css" />
    <script src="/(S(f4grns0hiatkwquk55qulbmd))/Scripts/jquery-1.5.1.min.js" type="text/javascript"></script>
    <script src="/(S(f4grns0hiatkwquk55qulbmd))/Scripts/modernizr-1.7.min.js" type="text/javascript"></script>
</head>
<body>
    <div class="page">
        <header>
            <div id="title">
                <h1>My MVC Application</h1>
            </div>
            <div id="logindisplay">
                    [ <a href="/(S(f4grns0hiatkwquk55qulbmd))/Account/LogOn">Log On</a> ]
            </div>
...以下省略

今度は先のURLRewrite設定をOnにした場合。

cookieless4

あら素敵。ウソじゃないよ!URLにはちゃんとSession ID入ってるでしょ?

これでクローラが来てもOutputCacheにへんてこなものが仕込まれることないですね!あと、動的圧縮をオフにしてるのは既知です。設定いいんですけど今回はこれで。

役に立つやら立たないやらな情報でした!

2011年6月30日木曜日

全角数値を半角数値に変換するModelBinder

拝啓、まゆきっつぁん

いつも、The Shodoにて筆の練習をさせていただいてます。ただ、いつまで経っても筆ぺんでは上手にかけるようになれません。本物の習字道具を使わないと練習の成果が出ないのでしょうか?The Fudepenで練習すると効果が期待できるかもしれないですね。是非、考慮いただければと思います。

先日のmvcConf @:Japan懇親会での一件について。ふと、思い出したので書いてみました。あの時はValueProviderがどうのこうのという話になったような気がしないでもないですが、ValueProviderではレイヤ低すぎて型は意識されてないのダメですね。

こんな感じでいかがでしょうか?整数に限定してますが、応用すると他にもいろいろできると思います。

public class IntegralModelBinder : DefaultModelBinder
{
  private readonly List<Type> _integralTypes = new List<Type>
  {
    typeof (sbyte),
    typeof (byte),
    typeof (char),
    typeof (short),
    typeof (ushort),
    typeof (int),
    typeof (uint),
    typeof (long),
    typeof (ulong)
  };
  private const string WideIntegrals = "1234567890一二三四五六七八九零壱弐参肆伍陸柒捌玖零";
  private const string NarrowIntegrals = "123456789012345678901234567890";

  protected override void SetProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value)
  {
    if (!_integralTypes.Contains(propertyDescriptor.PropertyType) || value != null)
    {
      base.SetProperty(controllerContext, bindingContext, propertyDescriptor, value);
      return;
    }

    var providerResult = bindingContext.ValueProvider.GetValue(propertyDescriptor.Name);
    value = providerResult.AttemptedValue;

    var narrow = string.Join("",(value + "").Select(c => WideIntegrals.Contains(c) ? NarrowIntegrals[WideIntegrals.IndexOf(c)] : c));
    var converter = TypeDescriptor.GetConverter(propertyDescriptor.PropertyType);
    try
    {
      value = converter.ConvertFrom(narrow);
      bindingContext.ModelState.Remove(propertyDescriptor.Name);
    }
    catch{}

    base.SetProperty(controllerContext, bindingContext, propertyDescriptor, value);
  }
}

試しに以下のようなモデルを定義してみました。

    public class Person
    {
        public string Name { get; set; }
        public int Age { get; set; }
        public DateTime? Birthday { get; set; }
        public byte Rank { get; set; }
    }

AgeとRankが整数型なので処理対象となります。Global.asaxでDefaultBinder(ModelBinders.Binders.DefaultBinder = new IntegralModelBinder();)を差し替えて実行した結果は↓こんな感じになります。

mb1

@model ZenBinder.Models.Person
@{
    ViewBag.Title = "ホーム ページ";
}

<h2>@ViewBag.Message</h2>

@using (Html.BeginForm())
{
    @Html.EditorForModel()

    <button type="submit">送信</button>
}

まずはフォームを出すでしょう。簡単にEditorForModelを使います。

mb2

普通に半角だけで試して送信してみると、ちゃんと動きます。同じものを全角にしてみても結果は同じになります。

mb3

変換出来ない場合はDefaultModelBinderの挙動になります。十とか百とか千も変換するためのマッピングを用意すれば、もう少しオシャレさが増すかもしれないです。

mb4

いかがでしょう。ケータイでの入力に是非応用してみてください。

2011年6月18日土曜日

mvcPhotosの出来るまで

ちょっと書いてみます。あんまり面白く無いですよ。

ASP.NET MVCをメインにした企画をやってみたいとチャックから連絡があって、喋る人が決まったところで各々にテーマがふられました。んで、たけはら担当として「クライアントサイドのテクノロジをメインに扱うセッションを」というリクエストから始まるんですが、ぶっちゃけ「それMVC関係ないじゃない...」と愕然としたものです。

とはいえ、テーマを無視するのもあれだからと、まずはセッション概要を伝え(もちろんこの時にはまだ何をするのか決めてないので、ぼかしまくった感じで)募集開始。そろそろ真面目に考えないとね~、と思いつつ仕事も忙しかったしで、ほっといたらあっという間に5月中旬。そろそろスライドだけでも書かねばと書き始めるものの、何を作るかは全く決まらず...。とりあえずの方向性としては

  1. 作ったものに参加者もその場でアクセスできる(だけど会場内のネットワークでは自マシンに参加者がアクセス出来ないはず)
  2. アーキテクチャを意識する
  3. MVC使いつつクライアントサイドモリモリ
  4. コードは参加者へのプレゼント

の4点。

※ネットワークについては、昨年のTechEDでダメだったのを経験してたので。

はてさて、どうしたものか。Twitterは他の人が絶対利用するだろうからパス。単純に外部サーバーにWebアクセスしてみんなでワイワイする感じで何がいいかな~、って考えるとどうしてもMvcGraffiti(みんなでお絵かき)とかぶる。となると、手堅い方法はメールか。ケータイなら絶対繋がるはず。メールということはPOP3の実装は必要だな~。メールなら写メがいいかも。という流れでとりあえずPOP3の実装と画像のリサイズ部分だけ先行コーディング。

いろいろ悩んだ挙句、普通に写真共有っていうところに落ち着くんだけど、どうやってクライアントサイドモリモリを達成するか。そこは後回しにしてアーキテクチャ。この時点でAppHarborをプラットフォームにして、ストレージはGoogle Storage使ってみようかと実験。でもGoogle Storageが思ったようにいかないからS3にチェンジ。モデルはCodeFirstで、ストレージは置き換えられるようにProviderとして実装。MVCでのサーバーサイドはシンプルにAPI的なものと、UA切り替え出来るような仕組みをどうするか考えつつ、ワーカーによるバックグラウンド処理でメールとストレージのつなぎをやろうと決める。UAに合わせてViewを切り替えるのはいろんなやり方があるけど、出回ってるやり方を実装してもつまらないので、随分悩んだ末にRoutingとViewEngineのコンビネーションで行う方法を思いつく。さすがオレ。

クライアントサイドはこのころすごく気になってたknockout.jsとModernizrを使うことで、うまいことやろうと思いつつ具体的なことは決めずにサーバーサイドをゴリゴリ作る。あと、それっぽくテストコードも用意することで、スタックというかレイヤというか、その辺を意識しやすいようにしておこう。

ちょっと横道にそれますが、ControllerのテストをしやすくするためにFormCollectionをパラメータに使う例をよく見ますね。でも意味ないですよね。そこは普通に入力モデルを渡せばいいじゃないっていうふうに思うわけですよ。だって、FormなりのValueProviderからModelBinder経由した入力モデルへのマッピングって、Controllerのテストじゃ無いからですよ。通常のFormを想定した場合、FormCollectionからUpdateModelをするなら、それはもうMVCが提供してくれるModelBinderのテストをするようなもんでしょう。意味無いじゃん!なので、ModelBinderのテストはMVCの開発チームに任せて、自分たちの書いたコードに対するテストを書きましょう。ただ、今回Controllerのテストは書いてないですけどね!てへ。

ここまで全然コード解説じゃないですけど、セッションで話したいのはそもそも製品の紹介じゃなくて「アプリケーションの作り方」。何をどういう設計で作るかを決めることで詰みです。「拳(コード)で語る」のがプログラマーですよ。だって、ASP.NET MVCも1~3へと進み、4以降になったら製品の使い方、コードの書き方なんて変わるわけですよ。Razorなんてものも出てくるし。だけど、考え方とか適用の仕方ってMVCっていうアーキテクチャスタイルとか、デザインパターンとかって基礎として使い続けるじゃん?そこ意識すること大事だと思うんす。

ただでも、このやり方は諸刃で、聞きに来てくれる層によってはドン引きされるんです(経験あり)。だって「お前の作ったアプリケーションを見に来たんじゃないんだよ!」とか「そのアプリでオレのプロジェクトは解決できない!」という意見もあるからね。いろいろです。どんな意見も、それぞれの人のコンテキストでは正論っす。ただ、自分にとっては、目の前の問題の答えを提示する局所最適じゃなく、全体最適を目指すほうが楽しい。なので、これからもこのやり方は変わらないでしょう。

はっ!熱くなってた!しっけいしっけい。

サーバーサイドが概ね出来たところで、クライアントサイドの実装に入るわけですが、PC,iPad,iPhone、そして日本が誇る超精密パーソナル通信機器、通称ガラケー対応も無視できない。モダンブラウザ向けにはjQuery Mobile使おうと思ってたんだけど、Azure担当大臣のだいちゃんが「jQuery Mobileでモテモテになるっす!」とか言い出しやがって、かぶるじゃねーかよ、的なね。まぁ、いいか。んじゃ、オレ適当に実装する、ということになりました。なので、見た目かなりしょぼくなったけど、オレのせいじゃないから!エロ大臣のせいだから!!Azureチーム金持ってるからって可愛いキャラ多すぎなんだよ。ASP.NET界隈では緑のキグルミしかいないっつーの。羨ましくなんか..うっぐ。泣かない。

はいはい。クライアントサイドでどういう感じに動かすのか、図に書いて最初に実装したのが、↓こんなやつ。

sample

受信画像をタイル状にランダム表示。緑も意識してみた。だけどコレがまたダサいのなんのって。イメージ通りに作ったはずなのに。自分のセンスに絶望。

sample2

ランダムがだめなのかと思って順番に表示するようにしたりしたけど...。根本的におかしい。マジやべー。

sample3

色がだめなのか!?と思って黒くしてみた。

sample4

で、最終的には↑こうなるんだけど、みんな知ってた?ウィンドウサイズに合わせてサムネイルの画像サイズは100→50→25と収まりよくなるようにリサイズするんだよ?

http://mvcphotos.takepara.com/

試してみてね。

ソースはこちら。

http://mvcphotos.codeplex.com/

Source Codeタブをクリックして右端のLatest VersionにあるBrowseで見たり、Downloadで取得してね。

source

書き疲れた...。もういい?仕様書もマニュアルもなしで、コードを追いかけるのも大変だと思うので、今回作成したmvcPhotosの実装をザックリ書きだしておきます。

  • POP3でメール受信
  • クラウドストレージとローカルストレージを切り替えやすくするためのストレージプロバイダ化
  • 画像のリサイズ
  • EF CodeFirstによるDAL
  • データベースアクセスをRepositoryにより抽象化
  • サーバーサイドでもDAL用のモデルと、入出力用のモデルを分けることでレイヤ分離と検証ルールの明確化
  • 動的画像リサイズを行うためのコンカレント制御と非同期Controller
  • 複数のUserAgentを同一Controllerで処理するためのViewEngineとRouting制御
  • モダンブラウザの判定と、動的スクリプト読み込みにModernizr
  • knockout.jsによるクライアントサイドでのMVVM実装
  • 自作Service Locatorと、DependencyResolver実装
  • DIを3パターン実装(探してみてね!)

こんな感じです。

ちなみにパネルディスカッションの最後でゴニョゴニョ言ってたことなんですけど「大事なのはMVCの心を理解しようとし、SoC - Separation of Concerns - 関心の分離を意識すること」。つまりきちんと役割を分離して、実装も可能なかぎり分離して参照関係を単純にしましょうね、と言いたかったですが言葉足らずのドヤ顔で失礼しました。

あと、一色さん、変なやりとりでスマセンした。失礼ぶっこいてスマセンした。ホントはすごいシャイボーイなんです。自分で言うのもなんですが、草食系男子なんです。型は古くて時化には強いタイプですけどシャバいやつなんす。

今後ともご贔屓に~。

2011年6月13日月曜日

メモ帳ですいません

mvcConf @:Japanでしゃべりましたね。あまりにも大雑把な説明っぷりに自分でもビックリです。オレ、こんなに雑だったんだ...もっと繊細だと思ってた。なんつっ亭たけはらです。

雨の中、足を運んでくれたたくさんの参加者の方々に感謝の気持ちでいっぱいです。

mvcConfといえば、知る人ぞ知るマニアにはたまらないイベントです。それを日本で日本人だけで、ASP.NETバカ集合させてしゃべらせようと企んだチャックの度量には度肝を抜かれました。初めての試みだし、人が集まるのか、どのような背景の人が参加してくれるのか全く未知数で、ランチも交通費もでないという低予算のなか、よくもまぁなんとかなったもんですね。

個人的には全然言いたいことがいえずに、あっという間に終わってしまったので、ブログを通して言い足りなかった部分、特にサンプルに盛り込んでいるテクノロジや設計方針なんかを、ちょびっとだけ書き残しておこうと思います。

まず当日、スライドの内容が思い出せるきがしなかったので、メモを書きました。それがコレ↓です。

mvcPhotos

  http://mvcphotos.takepara.com
  http://mvcphotos.takepara.com/mobile

  mvcphotos@takepara.com

サイトURL設計

  Home
    Index
  Photos
    Index - Jsonable
    Tags – Jsonable
    Create (GET/POST)
    Image
  Tags
    Index – Jsonable
※/mobile配下も同じ

アーキテクチャとしてみるWeb Stack

  • ViewEngineでのView切り替え
    同一コントローラを利用する
  • knockoutjsを使ってMVVMなクライアントサイド実装
  • Modenizrでのブクライアントサイド機能判定
    Modernizr.load活用
  • クラウド利用によるスケール、可用性の確保
    AppHarborとAWS
  • メールを利用したデータ入力
  • サーバーサイドを極力API化
    ODataを出力しdatajsで取得する。
  • テスト
    少しずつでいいからテストも書いていこうね
    Mockじゃなくても、StubやFakeを用意する。
  • razordo.it / guttokita.ccもよろしく!

参照ページ

ココまで。

Jsonableは渾身の仕込みだったけど、響かなくて残念でした。

リンク多いけど、参考になるサイトばかりで紹介しておきたかったのでメモに書いてました。気が向いたらのぞきに行ってみてはどうでしょう。

パネルディスカッションで「Page Controller」がどうのこうの言ってたんですけど、なんとなくPageとControlと聞き間違いされてる気がしたので改めて「Page Controller」デス!

実装サンプルを用意して適用方法を解説しようというセッションなので、最初からセッション資料なんてこれだけでよかったかも。今度からそうしようかな。技術的な解説よりもライブコーディングをいれたほうがピンと来やすいかもしれないしねー。「御託はいいよ・・・拳で語ってくれ」と誰もが思ったことでしょう。

次のエントリはコード解説を書こうと思います。が、途中でギャー!ってなってポイってしても、そこは笑って許してね!

2011年5月4日水曜日

ぷっすぷすにしてやんよ、Glimpseで。

Glimpse - A client side look at whats going on in your server

なんと素敵なツールでしょうね!

glimpse1

↑ChromeでGlimpseをオンにしたうえに、デベロッパーツールもオンにした状態。かぶせまくり。

これ何?っていうのは上記Glimpseサイトのビデオか、MIX11でのHanselmanさんのセッションビデオ見れば一発でわかります。と言ってしまうとブログに書く必要が無くなってしまうので、少し説明すると、Trace.axdやelmah.axd、RouteDebuggerを全部ひっくるめて組み合わせてさらにサーバーサイドの情報をひと通り参照できるようにするツールデス。分かりにくいっすね。

とにかく、NuGetでInstall-Package Glimpseと入力してプロジェクトに入れて見ましょう。

その状態でデバッグを開始すると、おや?ナニも変化ないですね。なので、URLに/Glimpse/ConfigといれてGlimpse機能をOnにしまいしょう。Onにした後はURLをもとのページに自分で戻すことを忘れずに。

glimpse2

そうすると画面右下に変な目玉アイコン(Glimpseアイコンね)が表示されるのでそこをクリック!

glimpse3

そうすると出てきます。

glimpse4

すごいねー。ちなみにブラウザに表示されてるのはプラグインとかじゃなくて100%JavaScriptだけで作られたものです。

glimpse5 glimpse6

ソース表示すると</html>の後にデータがレンダリングされて、最後にglimpseClinet.jsがロードされてるのが確認できます。

あとは、見たまんまです。各タブにそれぞれ情報が表示されます。

  • Binding
    まだ開発中~。Binderの情報がでる予定(だと思います)
  • Config
    Web.Configの設定内容をセクション別に表示
    config
  • Environment
    サーバーの実行環境の情報
    Application Assemblies/System Assembliesが見れるのは嬉しいですね
    emvironment
  • Execution
    ActionInvokderの実行内容を表示(だと思う)
    Filterの実行も見れるよ!
    execution
  • Glimpse Warnings
    Glimpseの何か
  • MetaData
    どこぞのMetadata
  • Remote
    リクエスト履歴っぽい
  • Request
    HttpRequestBaseの中身からCookie,Form,QueryStringを表示
    request
  • Routes
    表示内容(アクション)に到達するルーティングの解決順とマッチしたルート
    route
  • Server
    ServerValiablesの内容
  • Session
    Sessionの内容
  • Trace
    System.Diagnostics.Traceに出力した内容
  • Views
    表示内容(View)の解決に至る過程
    view
  • XHRequest
    まだっぽい

全部スクリーンショットをとるのが面倒だったから動かして確認してみてね!

んじゃ、そもそもGlimpseの仕組みはどうなってるの?って気になるよね。NuGetからインストールすると使うのは楽チンだけどソースが確認できないから、改めてソースをダウンロードしましょう。

Glimpse/Glimpse - GitHub

真っ先に気になるのが、PreApplicationStartCode。当たり前のようにスタートアップコードが書かれてる。んで、その中身はMicrosoft.Web.Infrastructure.DynamicModuleHelper.DynamicModuleUtility.RegisterModuleを使ったカスタムIHttpModuleの動的登録。その中で各種プラグイン(タブで表示する情報を取得したりするもの)をMEFで登録。

Moduleでハンドリングするタイミングは以下の4つ。

context.BeginRequest += BeginRequest;
context.EndRequest += EndRequest;
context.PostRequestHandlerExecute += PostRequestHandlerExecute;
context.PreSendRequestHeaders += PreSendRequestHeaders;

ここから結構気になってるExecutionで表示してるのがホントにActionInvokerに介入した結果なのかチェックしてみよう。

ExecutionプラグインのSetupInitでいろいろ仕込んでますね。ControllerFactoryとDependencyResolverをGlimpseのものに置き換えてます。GlimpseControllerFactory.CreateControllerでIControllerExtentions.TrySetActionInvokerを呼んでCastleのInterceptorを利用して、プロキシを生成しそれをActionInvokerにセットしてました。これでプロキシが必ず呼び出されるから、実行をインターセプトして実行情報を取得できるということですね。うほ。

DependencyResolverを利用した場合にも大丈夫なようにGlimpseDependencyResolverが用意されてて、こっちでも同じくProxyを生成するように抜かりなし。

他のプラグインもIGlimpsePlugin実装になってて、SetupInitしてGetDataを呼ぶことで各種データをHttpContext.Itemsに入れることで、最終的にGlimpseRespondersがJsonにシリアライズ。まるっとApplication編集に保存してGlimpseResponders.StandardResponseがResponse.Writeしてる。

Glimpseの仕組み

MEFを使ったプラグイン構造でContext.ItemsからApplication変数にデータを保持(configのsaveRequestCountはこの履歴保持数ということみたい)し、それをレスポンス(リクエスト毎にGuidを発行してコンテキストがきちんと判定できるよう保持)。各種情報はプラグインが自立して取得するけど、実行状態のインターセプトはCastleがProxyを生成して介入。

イロイロなプラグインを作っていけば、データの取得と表示はベースの機能でまかなえる素敵設計であるのと、.NET4、MVC3の機能もめいいっぱい利用したお洒落実装。これは萌える。萌え萌え。ぷっすぷすにされちまったぜ!

2011年3月12日土曜日

ASP.NET MVCでDataAnnotationsのエラーメッセージをカスタム

ちょっとこの質問見てみてくださいよ!

Asp.Net MVC 2 - Changing the PropertyValueRequired string. - Stack Overflow

ASP.NET MVCのDefaultModelBinderにはResourceClassKeyっていうプロパティがあって、そこに自作リソースを指定して、文字列リソースのキー名にPropertyValueInvalid/PropertyValueRequiredっていうのを作っておくと、アプリケーション全体にその文字列が適用される。でもInvalidは動作するけどRequiredがぜんぜん適用されません!っていう内容なんですね。

これ質問もなかなか面白いからか、特別ポイントが付与される質問だったんです。なので、張り切って思ってソース追っかけたりしながら動作を見てみたわけですよ。そしたら確かにInvalidはメッセージの置き換えができるのにRequiredは置き換えがおきない。

public class Person
{
  [Required]
  public string Name { get; set; }

  [Required]
  [Range(0,100)]
  public int? Age { get; set; }

  [DataType(DataType.Date)]
  public DateTime Birthday { get; set; }

  [DataType(DataType.EmailAddress)]
  public string Email { get; set; }
}

msg1

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

  RegisterGlobalFilters(GlobalFilters.Filters);
  RegisterRoutes(RouteTable.Routes);

  DefaultModelBinder.ResourceClassKey = "Messages";
}

msg2

なんでかな~、と調べてみたらDataAnnotationsのRequiredAttributeなんかは内部で参照するリソース名を固定で保持してて、DefaultModelBinderのほうの設定を参照しないんですね。分かってしまえば、そりゃそうなんですけど(日本語のエラーちゃんと出るし、GACに入ってるリソースを参照するのが正しい挙動な感じするしね)、それでもASP.NET MVCならできる方法がありそうな気がするんですよね。これだけ拡張ポイントたくさんあるんだから。

いろいろ見ていくとちゃんと用意されてるのを発見しました。

Reusable Validation Error Message Resource Strings for DataAnnotations

DataAnnotationsModelValidatorProvider.RegisterAdapterでValidationAttributeの型ごとにAdapterを指定できるんです。このAdapterのコンストラクタにはValidationAttributeクラスのインスタンスがわたってくるんですが、このAdapterを経由させてから、MVCはエラーメッセージを生成したりするので、AdapterのコンストラクタでインスタンスプロパティのErrorMessageResourceTypeとErrorMessageResourceNameを書き換えてあげれば、アプリケーション全体のメッセージを変更できるっていう仕組みです。

分かりやすいやり方としてはね、カスタムValidationAttributeを作って、ErrorMessage~の設定を上書いておくか、Modelに指定するときに属性プロパティに指定する方法なんだと思うけど、それだと属性を指定する箇所がすごい数になっちゃうじゃないですか。それでもいいけど、そうじゃなく一括して変更したいっていうのが質問の内容じゃないですか。

んで、ちゃんとサンプル書いて返信したんですよ。

public class MyRequiredAttributeAdapter : RequiredAttributeAdapter
{
  public MyRequiredAttributeAdapter(ModelMetadata metadata, ControllerContext context, RequiredAttribute attribute) : base(metadata, context, attribute)
  {
    attribute.ErrorMessageResourceType = typeof (Messages);
    attribute.ErrorMessageResourceName = "PropertyValueRequired";
  }
}

アダプター用意して、Global.asaxに

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

  RegisterGlobalFilters(GlobalFilters.Filters);
  RegisterRoutes(RouteTable.Routes);

  DefaultModelBinder.ResourceClassKey = "Messages";
  DataAnnotationsModelValidatorProvider.RegisterAdapter(
    typeof(RequiredAttribute),
    typeof(MyRequiredAttributeAdapter));
}

↑こう書けばカスタムできるよ!って。

msg3

なのに!なのになのに!カスタムValidationAttributeを作るほうに特別ポイントが!!ガッカリデス。ガッカリ過ぎてスネ毛が少し抜けたよ...。

こういう一括していの方法があることを知らなかったので、勉強になりました。

2011年2月24日木曜日

マニフェスト Tips

RADWIMPSじゃなくてHTML5のキャッシュ。

やっとmanifestファイルを使ったキャッシュを組み込めました。

Offline resources in Firefox - MDC Doc Center
Safari Client-Side Storage and Offline Applications Programming Guide: HTML 5 Offline Application Cache

前回のエントリでjQuery Mobileを使ったぐっと来た.ccの話をしましたが、その時はmanifestが簡単に組み込めなくて断念してたんですが、移動中だとやっぱり遅さが気になるんです。

どうすれば簡単にmanifestを使ったキャッシュを使えるのか、ASP.NET MVCのアプリケーションに組み込めるのか、悩ましいところでしたが、なんとかかんとかキャッシュできました。

ローカルストレージを使ったりした、送信データのキューイングはまだ実装してないので、常時オンラインである必要があるのでOffline Applicationではないんですけどね。

gu

↑これ。

アプリの作り方として、ヘッダ部が認証状態に合わせて変化する+コンテンツ部も同じく認証状態によって内容が変化するので、Ajaxでそれぞれの部分を取得するようにメインページの実装を変更しました。こうすることで、メインページ(manifestを指定しているHTML)がキャッシュされてても、認証により変化する部分は都度リクエストがとんでキャッシュされずに済むようになります。ここまでは結構普通なんですけど、ここからjQuery Mobileに合わせた実装が必要になります。

と、いうのも動的にDOMを生成したとしても、それだけではjQuery Mobileのスタイルが適用されないから、です。はてさて困ったものです。いろいろ試して見つけたのが $.mobile.page() という関数。これを呼ぶことで対象のエレメントに対して強制的にjQuery Mobileのレンダリングを適用させることが出来ました。

JQM FAQ

javascript - jQuery Mobile - Dynamically creating form elements - Stack Overflow

素晴らしいですね。

ただ、素直にAjaxで取得したPartial htmlに対してpage()適用したりしちゃうと、ChromeでjQueryのコンフリクトエラーが発生!だめか~と思ったけど$(document).page()とすればあっさり解決。

今回の実装だと2つのPartialを取得するので、それぞれのリクエストの完了を待ち合わせが必要。リクエストのたびにレンダリングすればいいんだけどカッコ悪いじゃないっすか。そんな時にjQuery 1.5で追加された便利な関数があることを思い出した。

$.when(…).then(…);

jQuery.ajax() – jQuery API

新しく実装されたDeferred Object。jqXHR素敵すぎ。whenに同期したいDeferred Objectを並べて($.get()の戻り値とか)おくと、すべてが完了した時点でthenに渡したfunctionが実行される優れもの。

あとUnobtrusive validationをクライアントサイドでの動的フォームに適用するには $.validator.unobtrusive.parse(document); を呼び出しておくことも注意事項ですね。

Brad Wilson: Unobtrusive Client Validation in ASP.NET MVC 3

他にも面白そうなTipsがあったらエントリします~。

2011年2月19日土曜日

ドッグイアとjQuery Mobile

コンピュータ業界は進むのが早いっていう話じゃなくて、本を読んで気になるページに折り目をつける話です。

専門書でも漫画でも小説でも本なら何でも折り目をつけてしまう癖があります。最悪なのは借りた本に折り目をつけてしまうところ。マジすいません。読んだ本で気に入ったフレーズや大事なことだと思った部分を簡単に残しておきたいと思って、ブログに書いたりもするけど、なるべくその瞬間に書き残しておければより記憶に残るっていいなーと思ったわけです。ブクログとかいろいろ便利なサイトが世の中にはありますが、もっともっと特化してしまってもいいと思いませんか!手軽にささっと、となるとモバイルデバイスが利用対象になりましょう。と、いうか自分にとって使いやすいのは何かを考えたらiPhoneだなっていう。

iPhoneといえば最近何かと話題のjQuery Mobile。面白いですよね。まだまだコレだという作り方を見つけることが出来てないのですが。ASP.NET MVCとの相性も悪くない感じです。

そこで本やDVDを見てぐっと来た部分を簡単につぶやいて記録に残せるものを作ってみました。

IMG_0142 IMG_0143 IMG_0144

ぐっと来た.cc

jQuery MobileなのでiPhone/Android等スマートフォン専用です。PCでもそれっぽく動作しますが、まともなのはChromeだけ。Firefoxでも3.6系だと見た目だけはそれっぽいけど、動作(アニメーション)はしょぼしょぼです。HTML5+CSS3だからといってIE9はまるで対象外。パッチを当てても全然ダメだった。

gu1 gu2

左からChrome、Firefox、IE9。背景を黒ししてるのでひどいもんですよ。

使い方は簡単です。最初にサイトにアクセスすると「みんなのぐっと来た」が表示されます。

gu3 gu4

赤い枠の部分をクリックすると、Amazonの商品詳細ページへ。緑の枠の部分をクリックするとつぶやきの全文が表示されます。

右上の「Sign in」をクリックするとTwitterアカウントを使ってログイン。最近お気に入りのOAuthです。http://razordo.it も同じです。

ログインするとHome(検索と最近つぶやいた商品リスト)とPeople(みんなのぐっと来た)を切り替えるタブが出てくるので、初めてつぶやく商品は検索して探しましょう。んで、つぶやく!そしたらTwitterにつぶやき内容とAmazonへのリンクが一緒にポストされます。

That's it!

それだけの機能なんですが、ASP.NET MVC3+Razor+EF CodeFirst+jQuery Mobile+ExpressWeb(宣伝?)です。HTML5とはいえ別に新しいエレメント使ってなくてdata-*属性を指定するくらいなのお手軽実装です。

jQuery Mobileの特徴的な部分はすべてのページは最初に表示されるHTMLに従属する、的なところです。ページ遷移は基本的にAjaxでHTMLを先取りしておいたものを自ページ内にDIVエレメント(data-role="page")として展開して、それを表示するためにアニメーションするというものです。表示されてるのはui-page-activeクラスが指定されてる部分になります。

gu5

なのでページやドキュメントのロード時というのは基本的に最初のページロードの時になりますね。しかも、ページ自体をDIVエレメントで1ページ内に収めるし、キャッシュのためにエレメントは毎回削除されるわけじゃないので、ID属性が同じエレメントが多数発生して$(“#id”)がたくさんいるじゃないか、みたいな状況が出来上がります。

その辺はclassでのセレクタでアクティブなpageからの相対参照で書くようにとコツが必要になるので、慣れが必要でしょう。おや~?と思う動きをするときはだいたいこの辺の設計が関与してます。jQuery MobileっていうのをjQuery UIみたいにとらえると痛い目見るぜハニー。

全く別のフレームワークとして、ちゃんと認識しておくことが必要です。作り方が変わってくるからね。もちろん専用のイベントも用意されてるので、ページのロードじに何か処理をするとかは普通にかけます。コレ以外にもタッチ前提のスマートフォンだからkeydownなんかも発生しなくてびっくりした。そりゃそうなんだけど。

jQuery Mobile Docs – Events

あとはcache manifestがちょっと難しい。manifestを指定してるHTML自体が必ずキャッシュされるという理屈になかなか気がつかなかったです。この辺もうまいこと調整していけば晴れてスマートフォンでも最適なモバイルアプリが出来上がるわけですね。涙ぐましい。

なにはともあれ、それっぽく動くようになったので使ってみてね!あと、Amazonへのリンクは設定で自分アソシエイトIDを登録しておくとそれを使うようになってるので、ID持ってる人は登録しておくとチャリンチャリンと聞こえるかもね!

2011年2月6日日曜日

Razor Do It

出来ることはコード共有&実行。Razorの。

Razor構文で書いたコードをコピペして実行出来るアプリケーションです。

WebMatrixでの開発や、ASP.NET MVC3でRazor使うじゃないですか。んで、いろいろヘルパーとか書いたりするじゃないですか。それを共有したらおもしろいんじゃないかな~と思って作ってみました。

http://razordo.it

コードの共有だけじゃなくてサーバーサイドでビルドして動かせるようにすればその場で動きの確認もできるから、なお面白いかも~。なんて言い出したのが始まり。サーバーサイドで実行するからフロントのアプリケーションとは別のAppDomainをサンドボックスとして用意してそのなかで動かさないと、アプリケーション自体がハッキングされたり、いろいろ悪さ出来てしまってまずいじゃないですか(メール無限送信とか)。

すんなり出来ると思ってたんですよね、正直。でも、結構面倒だった。最初は普通にAppDomainを分離するだけで実装してみたんだけど、それだと動的ビルドが失敗。GACに入ったものからじゃないとダメ。Assembly自体がAppDomainを行き来できないから、外でビルドする必要もあるし。こりゃ駄目だと思って次に試したのがSimpleWorkerRequest。でもでもこれもちょっとダメみたい。ASP.NET on ASP.NETだと結局ホスト環境に依存する箇所があるみたい。よくわかんないや。これもたぶんGACがからむのかな?なんせ情報が少なかった。普通のアプリとしては難しくないんだけどね。Cassiniのソースとかめっちゃ見た。結局動やったかというと...。まぁ、細かい話はいいですね。興味ある人がたくさんいるならコードとか仕組みを詳しく公開します。

razordoit razordoit2

動かすだけならコードを貼りつけて"Execute"を押すだけ。簡単ですね。

ちなみにASP.NET MVC3で使われるWebViewPageではなく、WebMatrixでホストするときに使われるWebPageViewを基底クラスにしています。これもまた@inheritなどではなくガッチリBuildProvider!さらにカスタムIPermission指定でMedium trustよりも厳しい実行環境。

書いたコードを共有するには右上にある"Sign in with Twitter"でTwitterにログイン(OAuthで認証)しましょう。そうすると"Save”ボタンが出てくるので、保存出来るようになります。

保存するとURLが変わるので、そのURLをTweetするとか、いろんな方法で友達に知らせちゃいましょう!

razordoit3 razordoit4

↑こんな感じです。

http://razordo.it/now/b446

サンプル。

Razor do it nowですよ。最初思いつきでshowにしちゃったけどnowのほうがそれっぽくないッスか?どっちでもいいか。

保存したコードは、さらにコピーして改良して保存して、というふうにいじっていくといいかも~。かも~。コードのレーティングつけてすげー人とか見れるような仕組みになるのも面白いかも?かも~。

dotnetConf2015 Japan

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