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を作るほうに特別ポイントが!!ガッカリデス。ガッカリ過ぎてスネ毛が少し抜けたよ...。

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

WebActivatorでお手軽Bootstrapper

いろんなセッションビデオやサンプルコードによく登場してるWebActivator。属性指定のしかたから、.NET4で登場したPreApplicationStartMethod(AssemblyInfo.csに指定するやつ)に関する何かだろうと勝手に解釈して、ちゃんと見てなかったんだけど、なんか急に見てみたくなった。

Light up your NuGets with startup code and WebActivator - Angle Bracket Percent - Site Home - MSDN Blogs

解釈はおおむね間違ってないんだけど、PreAppricationStartMethodだと1個しか指定できない(よね?)のと、フックポイントが起動直後しかない(そりゃそうだ)のを拡張してしまおうというものでした。なるほど。

Pre/Post/Shutdowの3箇所に仕込めるんですね。

  • PreApplicationStartMethod
  • PostApplicationStartMethod
  • ApplicationShutdownMethod

この3種類。うまいこと考えてますね。PreとPostはAOPっぽい感じです。タイミングはPreはそのままPreなんだけど、PostはMicrosoft.Web.Infrastructure.DynamicModuleHelper.DynamicModuleUtility.RegisterModule を使って動的にロードする初期化HttpModule(これ自体ちゃんとWebActivatorに含まれてるので気にすることは無いですね)のInitイベントで実行。ShutodownはそのHttpModuleのDispose時に実行。

David Ebbo: Register your HTTP modules at runtime without config

面白いですね。賢いですね~。アセンブリ属性にしてるのは使い勝手を考慮してのことでしょう。属性宣言とクラス実装が近くにあるはずだから、クラス属性にしてもいいような気もするけど、使い勝手は大事でしょう!

[assembly: WebActivator.PreApplicationStartMethod(typeof(クラス), "実行メソッド名")]
[assembly: WebActivator.PostApplicationStartMethod(typeof(クラス), "実行メソッド名")]
[assembly: WebActivator.ApplicationShutdownMethod(typeof(クラス), "実行メソッド名")]

こんな感じでアセンブリ内に宣言します。いくつ宣言してもいいです。ActivationManager(これが標準のPreApplicationStartMethodで指定されてるよ)が実行時にbinフォルダのアセンブリから全部抽出してくれます。

davidebbo / WebActivator / overview – Bitbucket

オレオレBootstrapperを卒業するときがきましたね!

注意点として、HttpModuleを利用するPostとShutdownはAppDomainがロードされなおすたびに実行されるので、何度も実行されるのでその辺気をつけましょう。

とにかくNuGetで Install-Package webactivator とタイプしてインストールしてみて以下のコードで動かしてみました。

[assembly: WebActivator.PreApplicationStartMethod(typeof(Bootstrapper1), "Pre")]
[assembly: WebActivator.PreApplicationStartMethod(typeof(Bootstrapper2), "Pre")]
[assembly: WebActivator.PreApplicationStartMethod(typeof(Bootstrapper3), "Pre")]

[assembly: WebActivator.PostApplicationStartMethod(typeof(Bootstrapper1), "Post")]
[assembly: WebActivator.PostApplicationStartMethod(typeof(Bootstrapper2), "Post")]
[assembly: WebActivator.PostApplicationStartMethod(typeof(Bootstrapper3), "Post")]

[assembly: WebActivator.ApplicationShutdownMethod(typeof(Bootstrapper1), "Shutdown")]
[assembly: WebActivator.ApplicationShutdownMethod(typeof(Bootstrapper2), "Shutdown")]
[assembly: WebActivator.ApplicationShutdownMethod(typeof(Bootstrapper3), "Shutdown")]

namespace Mvc3
{
    public class Bootstrapper
    {
        public static void Pre(string name)
        {
            Console.WriteLine("Pre : "+name + " " + DateTime.Now);
        }

        public static void Post(string name)
        {
            Console.WriteLine("Post : " + name + " " + DateTime.Now);
        }

        public static void Shutdown(string name)
        {
            Console.WriteLine("Shutdown : " + name + " " + DateTime.Now);
        }
    }

    public class Bootstrapper1
    {
        private static string Name = "1番";
        public static void Pre()
        {
            Bootstrapper.Pre(Name);
        }

        public static void Post()
        {
            Bootstrapper.Post(Name);
        }

        public static void Shutdown()
        {
            Bootstrapper.Shutdown(Name);
        }
    }

    public class Bootstrapper2
    {
        private static string Name = "2番";
        public static void Pre()
        {
            Bootstrapper.Pre(Name);
        }

        public static void Post()
        {
            Bootstrapper.Post(Name);
        }

        public static void Shutdown()
        {
            Bootstrapper.Shutdown(Name);
        }
    }

    public class Bootstrapper3
    {
        private static string Name = "3番";
        public static void Pre()
        {
            Bootstrapper.Pre(Name);
        }

        public static void Post()
        {
            Bootstrapper.Post(Name);
        }

        public static void Shutdown()
        {
            Bootstrapper.Shutdown(Name);
        }
    }

}

VS2010SP1だとIIS Expressで実行ができるけど、あえて外部コマンドでIIS Expressを指定して実行してみます。

wa

コンソールのスクリーンショットを取りたかったからデス!これって統合されたらどこで見ればいいんだろか。出力ウィンドウだとちょっと違うじゃん?ん~。まぁ、いいか。おいおい分かるでしょう。

残念ながらShutdownは表示するまで待つのも設定変えるのも面倒(IIS ExpressだとidleTimeoueでシャットダウンしない?)なので未確認!雰囲気が伝わればいいかな、なんて。

2011年3月5日土曜日

ぐっと来てますか?

http://guttokita.cc

@jsakamotoさんからとてもナイスなフィードバックをTwitterでもらったので早速実装しました。

ODataによるデータ出力

http://guttokita.cc/feed.svc

上記URLでOData形式でデータを出力しました。後はUIを組み込めば過去のつぶやきを見放題!ぐへへ。UIをどうやってつくるかは考え中。

guttokitacc1

guttokitacc2

購読しやすい形式にはマッピングしてないので、購読用途には向いてないです。

OData と AtomPub - WCF Data Services を使用した AtomPub サーバーの構築

↑この辺で直接購読しやすくマッピングをしてみようかとも考えて動かしてみたんですが、なんか用途が違う気がしてきたのでやめました。購読用Feedは需要があれば別途用意します。

つぶやいた内容を直接見れる(パーマリンク化)

続いて、つぶやき内容をブラウザから直接見れるようにするパーマリンク化。これまではiPhoneでの利用を想定してたので、トップページからリンクをたどって見る方法しかなかったですが、ブラウザにURLを入れて直接アクセスできるようにしました。ODataと合わせて使うのがいいかも。

IMG_0171 IMG_0172 IMG_0173 IMG_0174 IMG_0175

トップからたどっていくと、つぶやきページの一番下にPermalinkというリンクがあるので、そこに指定されてるURLがパーマリンクです。

permalink1

permalink2

PCで見ると↑こんな感じです。

上記の場合 http://guttokita.cc/Tweets/27 がパーマリンク。

技術的な話

そもそもEF CodeFirstでデータアクセス層を実装してるので、すんなりとはODataになってくれませんでした。ちょびっとだけ手を加える必要があります。というのも、CodeFirstのデフォルトの挙動がプロキシクラスを生成するので、WCF Data Servicesから見たときにIQueryableに見えないといわれて怒られます。

odata

サーバーで要求の処理中にエラーが発生しました。例外メッセージは 'データ コンテキスト型 'SiteContext' に、要素型がエンティティ型でないトップレベルの IQueryable プロパティ 'Accounts' があります。IQueryable プロパティがエンティティ型であることを確認するか、データ コンテキスト型に対して IgnoreProperties 属性を指定して、このプロパティを無視してください。

EF CTP4 Tips & Tricks: WCF Data Service on DbContext « RoMiller.com

CTP4ですがここを参考にObjectContextのオプションでプロキシを生成しないように抑制しておく必要があります。

CTP5での書き方は↓こう。

protected override ObjectContext CreateDataSource()
{
  var db = new SiteContext();
  ((IObjectContextAdapter)db).ObjectContext.ContextOptions.ProxyCreationEnabled = false;
  return ((IObjectContextAdapter)db).ObjectContext;
}

SiteContextっていう名前でDbContextを作ってます。

リレーショナルなテーブルだとこれでもちょっと使いにくくて、フラットで十分じゃないかと思うわけです。であれば、ViewをDBに定義してそれをFeedにしたほうがいいと思ったんですが、CodeFirstでViewってどうするんでしょうね。どうやら普通にエンティティクラス書いてDbSetでDbContextに定義すればいいみたい。でも、それだとテーブル作られちゃうじゃないですか。テーブルをつくるな宣言ってどうするんでしょうね。よくわかんないな~。NotMappedはテーブルには適用されない。しょうがないのでInitializerのSeedでDrop TableとCreate Viewをするという暴挙にでました。何かしら方法があるといいんだけど。

ちなみにODataということは、いろんなフィルターや射影し直せるというわけで、自分のつぶやきの最新3件取得とかも簡単です。

http://guttokita.cc/feed.svc/Tweets?$filter=UserName eq 'takepara'&$orderby=CreateAt desc&$top=3

odata3

うっひょ~い!

URLに指定できるクエリオプションは以下のページをどうぞ

データ サービス リソースへのアクセス (WCF Data Services)

2011年3月4日金曜日

IIS Expressの隠しコマンド

隠してはないですけど、あまり情報のないコマンド。

mvcConf 2 - Vaidy Gopalakrishnan: IIS Express | mvcConf | Channel 9

↑このビデオですべてが語られてます。英語のなので何を語ってるのかは知りませんが。

ビデオだと15分あたりから解説になりますが、長いし何言ってるかわからないので、ボディーランゲージを読みといてみました。

通常は

  • httpでlocalhostなら非予約ポート
  • httpsでlocalhostなら44300~44399ポート

が、IIS Expressで利用可能です(たぶん)。

だけど、

  • http://localhostやhttps://localhost で予約ポート使いたい
  • IPアドレスや、ホスト名変えてアクセスしたい
  • localhostのSSLで44300~44399以外を使いたい
  • カスタム証明書を使いたい

の時のおまじないを教えよう。

IISExpressAdminCmd

これだ!

という感じの内容です。それっぽい感じが伝わればいいんです!Documents\IISExpress\configに自分用のIIS Expressの設定が入ってますよね。これをメモ帳でいじるのもいいけど、コマンドでやればhostsファイルも一緒にいじってくれるみたいです。

コマンドプロンプトで実行してみると

C:\Program Files\IIS Express>IisExpressAdminCmd.exe
使用法: iisexpressadmincmd.exe <command> <parameters>
サポートされているコマンド:
      setupFriendlyHostnameUrl -url:<url>
      deleteFriendlyHostnameUrl -url:<url>
      setupUrl -url:<url>
      deleteUrl -url:<url>
      setupSslUrl -url:<url> -CertHash:<value>
      setupSslUrl -url:<url> -UseSelfSigned
      deleteSslUrl -url:<url>

例:
1) フレンドリ ホスト名 "contoso" の "http.sys" および "hosts" ファイルを構成しま
す:
iisexpressadmincmd setupFriendlyHostnameUrl -url:http://contoso:80/
2) フレンドリ ホスト名 "contoso" の "http.sys" 構成および "hosts" ファイル エン
トリを削除します:
iisexpressadmincmd deleteFriendlyHostnameUrl -url:http://contoso:80/

と、表示されます。なるほど!分かりにくいですね!

iisexadm

ビデオだとPowerShell(VS2010のNuGetのやつ)で進むのでiisexpressadmincmd自体の説明は皆無なんですが、PSの実行結果をみるとちゃんとsetupUrlを呼び出してるのが見れます。PSのコマンドの方がいろんなコマンドを使わなくてすむから便利っすね。

とにかくsetupUrlとsetupSslUrlでlocalhost以外でもIIS Expressにアクセス出来るようになるという事ですね!

IIS Express : Microsoft Web Platform : The Official Microsoft IIS Site

2011年2月26日土曜日

jQuery 1.5.1 リリースが嬉しい

jQuery: » jQuery 1.5.1 Released

出ましたね!待ちに待った1.5.1。近々jQuery Mobile alpha 4もくるみたいですね!

1.5.1になってIE9での不具合が解消されたおかげで、とうとう機能するようになりました。

ぐっと来た.cc
http://guttokita.cc

1.5.1にするまではひどいもんですよ。ロードした時点でjQueryさんが起こるもんだから、画面真っ黒。まだまだjQuery Mobileが本気じゃないみたいなので、Webkitバリのレンダリングにはならないけど、これでクマさんも使ってくれるようになることでしょう。ね!

http://razordo.it

も、同じ問題を抱えてたので、こっちは1.4.4を使ってたんですが、試してみたところ、大丈夫そうだったのでそれぞれ1.5.1にバージョンアップです!

嬉しさあまってThinkpad x201sも買っちゃいました。てへ。情報くれた@wakakoo サンクス!

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月10日木曜日

お手軽Inspector

@using System.Text
@using System.Collections
@using System.Reflection
@functions {
    public static string Inspector(object model)
    {
        if (model == null)
            return "";
        var type = model.GetType();
        if (type == typeof(string))
            return model.ToString();
        
        var properties = type.GetProperties();

        var inspect = new StringBuilder();
        inspect.Append("{");

        var vals = new List<string>();
        foreach (PropertyInfo property in properties)
        {
            var val = property.GetValue(model, null);
            var name = property.Name;

            if (val != null)
            {
                if ((property.PropertyType.IsArray || val as IEnumerable != null) && property.PropertyType != typeof(string))
                {
                    // ちょっとズル。
                    val = "["+string.Join(",", (from object v in (IEnumerable)val where v != null select Inspector(v)).ToArray())+"]";
                }
                else if (property.PropertyType.IsClass && property.PropertyType != typeof(string))
                {
                    var descendants = Inspector(val);
                    if (descendants != null)
                        val = descendants;
                }
                else
                {
                    val = val.ToString();
                }

                val = string.Format("{0}:{1}", name, val);
            }
            vals.Add(val.ToString());
        }
        inspect.Append(string.Join(",", vals));
        inspect.Append("}");

        return inspect.ToString();
    }
}
@{
    var model = new
    {
        Name = "オレがルールだ!",
        FavoriteSong = "明日があるさ",
        List = new List<string> { "First", "Second", "Third" },
        Sub = new { Name = "昔ね", Description = "そんなブログもあったよね" }
    };
}
@Html.Raw(Inspector(model))

inspector

http://razordo.it/now/wxvs

WebMatrixにはデバッガがないのでデバッグやテストの時に不便ですよね。なので、お手軽Inspector。

確か、NuGetにちゃんとしたのがあった気がするけど気にしない。最初はTypeDescriptorでPropertyDescriptorで取得しようとしたけど、Medium Trustでは駄目だったからReflectionで。

試してみたい場合はもちろんRazor Do It。

今日は違うことする予定だったんだけど、なんかどうしても試したくなったっす。やむなしっす。

2011年2月9日水曜日

7notes

普段から紙のノートにペンで書きながら考える癖がついてるから、情報をデジタル化しておくのが得意じゃなかったんだけど、これはちょっと衝撃的。

写真

思わずブログに書きたくなるくらいスゴすぎる。

iPadもってるなら使ってみないと!ペンも一緒にね!

Amazon.co.jp: プリンストンテクノロジー iPad/iPhone/iPod touch専用タッチペン(ブラック) PIP-TP2B: 家電・カメラ

といか、7notesを先に買っておいて、ペンが届くまで使ってなかったんだけど、ペンだからこそのしっくり感。ん?しっとり感。んん?まぁ、いいか。

少し気になるのがパフォーマンス。ちょっとモッサリ感。なれるかな。

7notes

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のほうがそれっぽくないッスか?どっちでもいいか。

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

2011年1月19日水曜日

WebMatrixでAmazon検索

今回はAmazon.co.jpの商品を検索するためのHelperを作ってみたよ。

今まで同様、WebMatrixでスターターサイトがある前提で。今まで作ったヘルパーもちょいちょい拡張しつつ載せていきます。

~/App_Code/Utility.cshtml

@functions {
    public static new dynamic Cache(string key, int expireSeconds, Func<dynamic> thunk){
        var context = new HttpContextWrapper(HttpContext.Current);
        return Cache(context, key, expireSeconds, thunk);
    }
    
    public static new dynamic Cache(HttpContextBase context, string key, int expireSeconds, Func<dynamic> thunk){
        dynamic value = context.Cache[key];
        if(value == null) {
            value = thunk();
            context.Cache.Insert(
                key,
                value,
                null,
                DateTime.Now.AddSeconds(expireSeconds),
                System.Web.Caching.Cache.NoSlidingExpiration
            );
        }
        
        return value;
    }

    public static string WebRequest(Uri uri)
    {
        string response;
        using (var client = new WebClient())
        {
            client.Encoding = System.Text.Encoding.UTF8;
            response = client.DownloadString(uri);
        }
        return response;
    }
}

UtilityヘルパーにはキャッシュだけじゃなくてHTTP GETを実行するメソッドを追加してます。なので郵便番号検索なんかもこっちを呼ぶようにリファクタリングしたほうがいいね。

~/App_Code/Aws.cshtml

@using System.Text
@using System.Security.Cryptography
@using System.Web.Routing;
@using System.Xml.Linq;
@using System.Dynamic;

@functions {

    public static class AwsSetting
    {
        public static XNamespace Namespace = "http://webservices.amazon.com/AWSECommerceService/2010-11-01";
        public static readonly string Host = "ecs.amazonaws.jp";
        public static readonly string Path = "/onca/xml";
        public static readonly string Service = "AWSECommerceService";
        public static readonly string Version = "2010-11-01";

        public static readonly string AWSAccessKeyId = "アクセスキーID";
        public static readonly string SecretAccessKey = "シークレットアクセスキー";
    }

    /*
     * Percent-encode (URL Encode) according to RFC 3986 as required by Amazon.
     * 
     * This is necessary because .NET's HttpUtility.UrlEncode does not encode
     * according to the above standard. Also, .NET returns lower-case encoding
     * by default and Amazon requires upper-case encoding.
     */
    private static string PercentEncodeRfc3986(string str)
    {
        str = HttpUtility.UrlEncode(str, Encoding.UTF8);
        str = str.Replace("'", "%27").Replace("(", "%28").Replace(")", "%29").Replace("*", "%2A").Replace("!", "%21").Replace("%7e", "~").Replace("+", "%20");

        StringBuilder sbuilder = new StringBuilder(str);
        for (int i = 0; i < sbuilder.Length; i++)
        {
            if (sbuilder[i] == '%')
            {
                if (Char.IsLetter(sbuilder[i + 1]) || Char.IsLetter(sbuilder[i + 2]))
                {
                    sbuilder[i + 1] = Char.ToUpper(sbuilder[i + 1]);
                    sbuilder[i + 2] = Char.ToUpper(sbuilder[i + 2]);
                }
            }
        }
        return sbuilder.ToString();
    }

    public static string GenerateSettingQueryString(RouteValueDictionary parameters)
    {
        var pairs = new SortedDictionary<string, string> { 
            {"AWSAccessKeyId",AwsSetting.AWSAccessKeyId},
            {"Service",AwsSetting.Service},
            {"Version",AwsSetting.Version}
        };
        parameters["Timestamp"] = DateTime.Now.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.000Z");
        foreach (var kv in parameters)
            pairs.Add(kv.Key, (string)kv.Value);

        var qs = from kv in pairs
                 select string.Format("{0}={1}", kv.Key, PercentEncodeRfc3986(kv.Value));
        return string.Join("&", qs);
    }

    public static Uri GenerateUri(object parameters)
    {
        return GenerateUri(new RouteValueDictionary(parameters));
    }
    
    public static Uri GenerateUri(RouteValueDictionary parameters)
    {
        var values = GenerateSettingQueryString(parameters);
        var unsigned = string.Join("\n", new[] { "GET", AwsSetting.Host, AwsSetting.Path, values });
        
        byte[] key = Encoding.UTF8.GetBytes(AwsSetting.SecretAccessKey);
        byte[] data = Encoding.UTF8.GetBytes(unsigned);
        var hmac = new HMACSHA256(key);
        var hash = hmac.ComputeHash(data);
        var signature = PercentEncodeRfc3986(Convert.ToBase64String(hash));

        return new Uri(string.Format("http://{0}{1}?{2}&Signature={3}", AwsSetting.Host, AwsSetting.Path, values, signature));
    }

    private static dynamic DynamicExpandoObject(Action<dynamic> init)
    {
        dynamic model = new ExpandoObject();
        init(model);
        return model;
    }
    
    private static string ElementValue(XElement parent, string name)
    {
        var ns = AwsSetting.Namespace;
        if (parent == null)
            return string.Empty;

        var element = parent.Element(ns + name);
        if (element == null)
            return string.Empty;

        return element.Value;
    }
    
    public static IEnumerable<dynamic> ItemSearch(string keywords)
    {
        var uri = GenerateUri(new { 
            Operation = "ItemSearch", 
            SearchIndex = "Books", 
            Keywords = keywords 
        });
        var response = Utility.WebRequest(uri);
        var ns = AwsSetting.Namespace;
        var xml = XDocument.Parse(response).Descendants(ns + "Item");
        
        return from item in xml
               let attrs = item.Element(ns + "ItemAttributes")
               select DynamicExpandoObject(model =>
               {
                   model.ASIN = ElementValue(item,"ASIN");
                   model.DetailPageURL = ElementValue(item, "DetailPageURL");
                   model.Author = ElementValue(attrs, "Author");
                   model.Manufacturer = ElementValue(attrs, "Manufacturer");
                   model.Title = ElementValue(attrs, "Title");
               });
    }
}

サンプル実装を参考にした(というか、もうそのまま持ってきた)のはUrlEncodeするコードだけで。RFC3986に準拠させるための文字列置換だからロジックなんて無いのでいいかなと思いまして。後はちゃんとスクラッチ。無駄にスクラッチ。いや、もう、ホント、だからなに?

アクセスキーIDとシークレットアクセスキーは自分でAmazonから取得したものを使ってね!

取得の仕方にかなり戸惑ったけど、AWSに登録してアカウントページからセキュリティ証明書のページにいけば載ってたよ。APIの詳細はDocumentation Archive : Amazon Web Servicesからドキュメントダウンロードして確認しつつ作ったけど、改行コードの部分が"\n"だけでいいことに気がつくのにすげー時間かかった。

とりあえず、BooksカテゴリのItemSearch APIだけを実装してるけど、いくらでも追加しやすいように作ったから、他のAPIも実装してみるといいかも。あと今回はキャッシュも使ってないけど、キャッシュ組み込みはしたほうがいいかな~、どうかな~。同一キーワードで何度も検索する必要がほとんど無い、よね?

~/amazon.cshtml

@{  
    Layout = "~/_SiteLayout.cshtml";
    Page.Title = "Amazon書籍検索";
}
<form method="post">
    キーワード:
    @Html.TextBox("keywords")
    <input type="submit" value="検索" />
</form>

@if (IsPost)
{
    var keywords = Request["keywords"];
    <ul>
    @foreach (dynamic item in Aws.ItemSearch(keywords)) { 
        <li>
            <a href="@item.DetailPageURL">@item.Title</a>
            <div>@item.Author / @item.Manufacturer</div>
        </li>
    }
    </ul>
}

んで、表示ページのコード。特に変わったことはしてないです。

“ONE PIECE”と入れて実行したのが↓コレ。

aws

もちろんASINでも検索できる。API仕様がそうだから。

"ワンピース"で検索してみる。

aws2

ちゃんと動いてるね。よしよし。これで4 Helpers。あ、WebRequestをヘルパーにしたから5個ってことでいいっすかね。

もちろんASP.NET MVCでも動くよ。

ちなみにヘルパーはロジックが入るので、Visual Studio 2010で開発したほうが楽かも。使う分にはコピペしちゃえばいいからWebMatrixで十分かな?

今の気持ちを表現するならこれしかないね!

20100105_1650081

すません...。

2011年1月16日日曜日

WebMatrixのWebPagesで拡張子が無くてもアクセス出来る理由

タイトル長い。地獄のミサワネタが他人ごとに思えない今日この頃。

WebMatrixでごそごそ遊んでると、何気にページアクセスする際の拡張子指定が無くてもいけることに気が付きますよね。

スターターサイトで試すよー。みんなー、用意はいいかーい?ちなみにWebPagesのソースコードを追いかけてるのでチャラさが足りないエントリですいません。

例えば以下のページ。アドレスバーには”/default.cshtml”って入れて表示した場合ですね。普通です。

ext1

続いて"/default"。同じように表示されますね。拡張子cshtmlは指定してないですね。

ext2

なぜでしょ~か?答えはソースコード中にあり。System.Web.WebPages\WebPageHttpHandler.csとSystem.Web.WebPages\WebPageRoute.csを覗いてみましょう。

WebPageHttpHandlerにGetRegisteredExtensions()とRegisterExtension()が有りまして、これが拡張子を登録したり取り出したりできるやつです。んで、WebPageRouteのほうでそれを見つつ、VirtualPathProvider経由でファイルの存在確認をして、無ければ補完するという処理をしています。これらHttpHandlerとRouteの指定はPreApplicationStartCodeだったり、WebPageHttpModuleだったりです。詳しくはDeep Diveになっちゃうのでソースを見てみるといいかな~、なんて思います。

ソース見ただけじゃよくわんな~い、というかわいこちゃんにお勧めなのがテストプロジェクトを動かしてみるという方法。VS2010必要だけどね。System.Web.WebPages.Test.WebPageRouteTestを眺めつつ、デバッグ実行して追いかけてみると、挙動が分かりやすくていいです。なぜProviderが必要なのか、なぜDIなのか、なんていうのもテストするとよく分かりますよね。

ext7

DynamicModuleUtility.RegisterModuleて誰やねん!的な。ソースないし。まぁ、普通にASP.NETの仕組みだということです。

ext6

つまり、cshtmlとvbhtmlはURLに指定しなくても勝手に補完してくれるので、安心して省略してしまってもいいですよ、っていうことです。VirtualPathProviderがちゃんとキャッシュしてくれてるので2回目以降はちょっと早そう。そのくらい誤差の範囲でネットのほうが桁違いに遅いんだしいいんじゃないの、って思うけど妥協無しデス。

続きまして、ちょっと変わり種。今度は”/”だけ。それでもちゃんと表示されますね。

ext3

なぜかというと、これもまた補完機能が働くからです。今度はどこで?いったい誰が!?

WebPageRouteです。一緒です。はい。

ファイル名指定が無い場合(サブフォルダも含むよ)、”default”→”index”の順に補完しつつ、拡張子も”cshtml”→”vbhtml”で補完されることで見つかります。

試しに以下のように”~/index.cshtml”を作成してみましょう。

<!DOCTYPE html>

<html lang="en">
    <head>
        <meta charset="utf-8" />
        <title>インデックス</title>
    </head>
    <body>
        <h1>index.cshtmlなんだぜ!</h1>
    </body>
</html>

“/index”と入れてアクセスすると普通に見れます。

ext4

この状態で、"default.cshtml”を違うファイル名変えて見ましょう。何でもいいです。

んで、"/"にアクセスすると、ちゃんと”default.cshtml”じゃなくて”index.cshtml”の内容が表示されました。

ext5

つまり、初期表示したいファイル名はdefault.cshtml(vbhtml)かindex.cshtml(vbhtml)固定ということですよ。もちろんRoutingとかに手を出せばいけるかもしれないけど、どーだろね。タイミング的に間に合わないかもね。相手はModuleだし。

Thoughts on WebMatrix

そんなことを思ってたら上記エントリを発見。そうだね。URL Rewriteモジュール使えば何とでもなるね、きっと。たぶん処理も間に合う気がする。試してないけど。

WebMatrixでサイトを作成する場合、こんな仕組みで動くのを覚えておくといいと思うよ~。

20100218_1708335

@functionsと@helperの組み合わせ

いろいろ作りたい物があったりするんだけど、お試しする時間も必要でしょう。前回までで@functionsと@helperそれぞれを作ってみたんですが、今回はそれを組み合わせたサンプル。

もちろんASP.NET MVC3でもWebMatrixでも動くわけです。

最初に作ったキャッシュヘルパーにHttpContextBaseを渡さない版のオーバーロードを用意したので、まずはそちらを再掲。

~/App_Code/Utility.cshtml

@functions {
    public static new dynamic Cache(string key, int expireSeconds, Func<dynamic> thunk){
        var context = new HttpContextWrapper(HttpContext.Current);
        return Cache(context, key, expireSeconds, thunk);
    }
    
    public static new dynamic Cache(HttpContextBase context, string key, int expireSeconds, Func<dynamic> thunk){
        dynamic value = context.Cache[key];
        if(value == null) {
            value = thunk();
            context.Cache.Insert(
                key,
                value,
                null,
                DateTime.Now.AddSeconds(expireSeconds),
                System.Web.Caching.Cache.NoSlidingExpiration
            );
        }
        
        return value;
    }
}

続いて、このキャッシュヘルパーを使うものを考えてみたんだけど、郵便番号検索なんてどうかな~。

ゆうびんホームページ - 日本郵便

↑ここにある検索フォームを使って、返ってくるHTMLを解析だ!なんてことをやろうとするとちょっとコード量もかさばる(正規表現でtd class=”data”とればいいだけなんだけど)し、今風じゃない。とてもグレートな郵便番号検索サービスを見つけたのでそちらを使ってみます。

郵便番号データのダウンロード – zipcloud

Google App Engine上にホストされてて、日本郵便のデータを綺麗に正規化してくれてるグレートサービスです。一応規約はチェックしてみたけど、使ってみる人も確認してみてね!エロとかグロとかダメだからね!

ここのサービス実際にはJSONPでも取得できるのでクライアントサイドのテクノロジだけで見事に完結してしまうわけですが、そこをあえてサーバーサイドで行ってみよう(今風じゃないね...)。

~/App_Code/ZipCroud.cshtml

@functions{    
    private readonly static string _api = "http://zipcloud.ibsnet.co.jp/api/search?zipcode=";
    public static dynamic Search(string zipCode){
        var key = string.Format("zipcroud:{0}",zipCode);
        var expire = 60 * 60 * 24; // 1日
        return Utility.Cache(key,expire,()=>{
            var url = string.Format("{0}{1}",_api, zipCode);
            string response;
            using (var client = new WebClient()){
                client.Encoding = System.Text.Encoding.UTF8;
                response = client.DownloadString(url);
            }
            return Json.Decode(response);
        });
    }
}

@helper Render(string zipCode){
    var sw = new System.Diagnostics.Stopwatch();
    sw.Start();
    var result = ZipCroud.Search(zipCode);
    sw.Stop();
    <div>@zipCode<text>の検索結果(@sw.ElapsedMilliseconds ms)</text></div>
    if (result == null || result.status != 200 || result.results == null) {
        @:見つかりませんでした。
    } else {
        <ul>
        @foreach(var zip in result.results){
            <li>@zip.address1 @zip.address2 @zip.address3</li>
        }
        </ul>
    }
}

とにかく↑これをコピペしてZipCroud.cshtmlをApp_Codeフォルダ配下に作成だぜ!

データを取得するSearch(こっちが@functions)と、レンダリングするRender(こっちが@helper)。要するに表示に関するものの場合は@helperにしとけばよろしっていう感じです(んじゃApp_Codeに入れるなよ、っていうツッコミは勘弁してね)。

Json.Decodeヘルパーとか超絶便利なヘルパーが標準搭載されてるので、楽チンぽんですよ。

続いてページの方。こっちはもちろんWebMatrixを使ってWebPagesとして作ります。

~/zip.cshtml

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8" />
        <title>郵便番号検索</title>
    </head>
    <body>
    郵便番号を入れてみてね!
    <form action="zip" method="post">
        <input type="text" name="zipCode" />
        <input type="submit" value="検索" />
    </form>
    @if(IsPost){
        var zipCode = Request.Form["zipCode"];
        @ZipCroud.Render(zipCode)
    }
    </body>
</html>

IsPostとかRequest["zipCode"]っていう部分が気になるけど、まぁ、いいでしょう。MVCの時はちゃんとModelBindしてね。

zip1

起動するとそっけないページが出てきました。

zip2

例えば"0288401"が複数の住所を保持しているらしいので、これを入力した結果が↑これ。

いっぱい出てきた。

ここからキャッシュヘルパーの威力を発揮します。画像拡大してみると分かるんだけど、検索結果の横に634msと表示されてるのが分かると思います。これは、まぁ、だいたいブレるんでこの値そのものにはそれほど重要な意味はないんですよ。なんとなく3桁ミリ秒くらいかかるんだね、っていうのが伝わればそれで良し。

ここのページはPOSTで値を送信してるので、F5を押すか同一の郵便番号を押して再度検索してみましょう。

zip3

今度は驚異の1ms!

そりゃそうですよ。なんせ実際には検索しに行かないでキャッシュから結果を取り出してるだけなんだから。今回はちなみに1日ほどキャッシュするようにしてみました。実際にはAppDomainの再起動なんかでもキャッシュはクリアされるので、こんなもんでしょう。郵便番号の値なのでそれほど頻繁に更新されるものでもないしね!

郵便番号毎にキャッシュするようにしてるので、一度検索してしまうと2回目以降はちょっぱや。

そして、ヘルパー三連投するオレ。つまりこういうことだよ。

20100609_1842417

2011年1月14日金曜日

WebMatrixの認証設定とヘルパー

WebMatrixを使ってWebPagesの認証機能を利用する場合、WebSecurityヘルパーを使う事になりますね。詳しくは塚田さんがエントリ書いてたのでそちらをどうぞ。

WebMatrix ASP.Net Razorでログイン認証を作る- 1.WebSecurity Helperの概要 « IIS+αでいってみよう

↑ここから何個か続きますね。

基本的にASP.NETの仕組みで認証(Authentication)・承認(Authorization)させてるのでFormsAuthentication周りを知っていることが前提なのかもしれないけど、その辺はヘルパーを使うということでまるっと隠しきろうという感じでしょうね。

WebMatrix入れるとスターターサイトを作れますが、そこにも認証系の機能はひと通り含まれてます。それを参考にするのがいいかなと思うんだけど、1点とても分かりにくいところがある。

それは「ログインページを指定」する方法です。

WebSecurityの規定値は”~/Account/Login”です。これはFormsAuthenticationSettingsクラスにstatic readonlyで宣言されてます。readonlyですよ!変更できないじゃん!

普通にASP.NETの作法通りなら以下の設定をしますよね。
<system.web>
<authentication>
  <forms loginUrl=”…”>

でもWebSecurityはそれを利用しません。さすがカスタム。あと<authorization>設定(承認)は設定すればちゃんと機能します(でも使わないのがWebMatrix流?)。

FormsAuthenticationModule自体は読み込まれてるので、web.configを作成してそれらのセキュションを書いてしまえばよさそうな気がするでしょ?だがしかし!loginUrlは見てくれないんですよ。なぜかというとアプリケーション起動時にFormsAuthenticationの設定をWebMatrix.WebData.ConfigUtilが上書きするからです。

なので、ログインURLを自分で指定したい場合、ソースを追いかけるとわかりますが、以下のようにappSettingsに指定しましょう。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>
    <add key="loginUrl" value="~/Members/LogOn"/>
  </appSettings>
</configuration>

これで、標準のログイン先が変更できます。
なんで、こういう設定が必要かというとHttpのステータスコードを見てFormsAuthenticationModuleがリダイレクトするのがASP.NETのForm認証フローだからです。これを理解してないと自分でSessionやらCookieやらで適当につくろうとしちゃうでしょ?それはとてもよろしくないです。愚行ですよ!

なので、authorizationで認証が必要なパスを指定するか、認証が必要なすべてのページの最初にWebSecurity.RequireAuthenticationUser()を書いておくことです(実際は_PageStartとか_Layoutとかで一括処理ね)。そうするとHttpUnauthorized 401なレスポンスを返そうとするので、FormsAuthenticationModuleがそこに反応してログインページに自動でリダイレクトします。

分かりにくい?

@{  
    Layout = "~/_SiteLayout.cshtml";
    Page.Title = "マイ Web サイトへようこそ";
}
<p>
    ASP.NET Web Pages を使用すると、Web 用の優れた .NET ベースのアプリケーションを簡単に作成できます。
</p>
<p>
    <ul>
    @foreach(var moduleName in Context.ApplicationInstance.Modules.AllKeys){
        <li>@moduleName : (@Context.ApplicationInstance.Modules[moduleName].GetType())</li>
    }
    </ul>
    認証済み:@WebSecurity.IsAuthenticated<br/>
    ログインURL:@FormsAuthentication.LoginUrl
</p>


↑こんな感じでdefault.cshtmlに書いておくと読み込まれてるすべてのモジュールと認証状況を表示します。web.configに何も書かない場合、ログインURLには"/Account/Login"と表示されますですよ。

auth1

↑一番下の部分ね。

続いてweb.configファイルをルートに作成し設定してみましょう。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>
    <add key="loginUrl" value="~/Members/LogOn"/>
  </appSettings>
</configuration>

auth2

ちゃんとログインURLが変化してますよね。この状態でログイン必須なChangePasswordページに行ってみる。

auth3

"/Members/LogOn”というページはないので、404エラーになってますが、ちゃんと"/Members/LogOn”にリダイレクトされてるのが確認できます。ぐっじょぶ!
難しい話はこのへんにして、今日のヘルパー。

@helper Show(string message){
        <script type="text/javascript">
            alert('@Html.Raw(message.Replace("'","\\'"))');
        </script>
    }
}
/App_CodeにMessageBox.cshtmlを作成し上記のコードをコピペ。
Default.cshtmlに以下のように書いてみよう。
@{  
    Layout = "~/_SiteLayout.cshtml";
    Page.Title = "マイ Web サイトへようこそ";
}
<p>
    ASP.NET Web Pages を使用すると、Web 用の優れた .NET ベースのアプリケーションを簡単に作成できます。
</p>
<p>
    @MessageBox.Show("Hello World! '10")
    @MessageBox.Show("Hello World!")
</p>

あら不思議!サーバーサイドでMessageBox.Showを書いたつもりがクライアントサイドにメッセージボックスが表示されました。デバッガのないWebMatrixならではのちゃらいヘルパーの完成です。

auth4

dotnetConf2015 Japan

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