ラベル WebMatrix の投稿を表示しています。 すべての投稿を表示
ラベル WebMatrix の投稿を表示しています。 すべての投稿を表示

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年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

WebMatrixで初めてのヘルパー

ASP.NET MVC3 RTM出ましたね。やったね。やりたい事いろいろあるんだ!

WebMatrixもでたね。WebPagesじゃん?Razorじゃん?ヘルパーじゃん?

WebMatrixやらなんやらの細かい説明とかはよくわからないのですっ飛ばして、早速ヘルパーを作ってみようかな。

ヘルパーといえばHelperResultを受け取る@helperで定義される者たちなんだろうけど、@functionsに定義するものもUtilityメソッドという意味でヘルパーっていう扱いにしても、それはそれで許されるんじゃないかと勝手に定義。だめ~?いいっしょ!

Webアプリケーションの基本といえばキャッシュでしょ!でもとりあえず今回はOutputCacheじゃなくてデータキャッシュね。System.Runtime.Caching!と言いたいところだけど、もっとお手軽なHttpContextBase.Cache。いろいろまとめてくれてるし、System.Web.Caching.Cache互換なインターフェイスだし。

まずは、WebMatrixでスターターサイトを作成。

ルートのdefault.cshtmlをひらいて以下のように書いてみる。

@{  
    Layout = "~/_SiteLayout.cshtml";
    Page.Title = "マイ Web サイトへようこそ";

    dynamic cache = Cache["test_page"];
    if(cache==null){
        cache = DateTime.Now;
        Cache.Insert(
            "test_page",
            cache,
            null,
            DateTime.Now.AddSeconds(3),
            System.Web.Caching.Cache.NoSlidingExpiration
        );
    }
}
<p>
    ASP.NET Web Pages を使用すると、Web 用の優れた .NET ベースのアプリケーションを簡単に作成できます。
</p>
<p>
    @cache<br/>
    @cache.GetType().Name
</p>

んで、動かしてみる。

helper1

出ますわね。3秒キャッシュ。ホームをなんどもクリックしてみたら分かるわね。

毎回キャッシュするたびにこんなコード書くのも面倒だよね!これをヘルパーにしてみよう!

ルートにApp_Codeって名前でフォルダ作成。んで、その中にUtility.cshtmlファイルを作成。

helper2

中身は↓。

@functions {
    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;
    }
}

staticメソッドなのでHelperPage.Cacheなんて見れないからContextを渡しましょう。もちろんHelper内で

var context = new HttpContextWrapper(HttpContext.Current);

って書くのもありだとは思うけど、まぁ、ね。

んで、default.cshtmlを以下のように変更。

@{  
    Layout = "~/_SiteLayout.cshtml";
    Page.Title = "マイ Web サイトへようこそ";

    dynamic cache = Cache["test_page"];
    if(cache==null){
        cache = DateTime.Now;
        Cache.Insert(
            "test_page",
            cache,
            null,
            DateTime.Now.AddSeconds(3),
            System.Web.Caching.Cache.NoSlidingExpiration
        );
    }
}
<p>
    ASP.NET Web Pages を使用すると、Web 用の優れた .NET ベースのアプリケーションを簡単に作成できます。
</p>
<p>
    @Utility.Cache(Context, "test_helper",3,()=>DateTime.Now)<br/>
    @Utility.Cache(Context, "test_helper",3,()=>DateTime.Now).GetType().Name<br/>

    @cache<br/>
    @cache.GetType().Name
</p>

太字の部分追加。

キャッシュしたい値を返すFunc<T>を渡しましょう。

@Utility.Cache(Context,”text_helper”,3,()=>{
  return DateTime.Now
})

でもいいよ。真面目な話をすると、ちゃんとレキシカルスコープを維持してるので、ローカル変数を参照しててもOKです。...。どんまい。

helper3

ちゃんと3秒キャッシュしてるよね!例えば、外部のサイトのデータを毎回取得するなんてばからしいからキャッシュしたり、データベースから毎回取得するなんてばからしいからキャッシュしたり、いろいろキャッシュすることでパフォーマンスは劇的にあがるっしょ!

dynamicだから

@Utility.Cache(Context, "test_helper",3,()=>DateTime.Now).Second

って書いてもちゃんと動くもんね!

まずは1 helper。

dotnetConf2015 Japan

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