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

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

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。

2011年1月3日月曜日

Razor Hosting

Hosting the Razor Engine for Templating in Non-Web Applications - Rick Strahl's Web Log

いや~、参っちゃった。年末にRazor Templating Engine出てたじゃないですか。で、Instapaper見てみたらこっちも”後で読む”扱いになってたので、そろそろ読むかと読んでみたわけですよ。

そしたらこっちはハナからAppDomainを別にしてコンパイルするのを前提に作られてるじゃない。もちろんプロセスのAppDomainと同じところでコンパイルも出来ますよ。その辺はぬかりないデスヨ。同じ目的でもこんなにも設計が違うんだね~、っていう感じです。ちなみにRickさんのRazor HostingではVB.NETコンパイラは入ってないです。

はい、Hello World!

var engine = new RazorEngine<RazorTemplateBase>();
dynamic context = new ExpandoObject();
context.Name = "World!";
var result = engine.RenderTemplate("Hello @Context.Name", new string[] { }, context);

匿名クラスじゃダメだったからdynamicなExpandoObjectで。まぁ、その辺は適当でね。

続いて、HtmlHelperを使うようにして見ましょう!RTE(Razor Templating Engine)にはちゃんとサンプルがあったからそのまま簡単にできたけど、今回は無いのでしょうがないから自分で書く。と、言ってもRTEのをままもってきて、RazorEngineのジェネリックに基底クラスとして指定です。この辺ちょっとあれっすね。まぁ、いいや。

internal class HtmlHelperFactory
{
    #region Methods
    /// <summary>
    /// Creates a <see cref="HtmlHelper{T}"/> for the specified model.
    /// </summary>
    /// <typeparam name="T">The model type.</typeparam>
    /// <param name="model">The model to create a helper for.</param>
    /// <param name="writer">The writer used to output html.</param>
    /// <returns>An instance of <see cref="HtmlHelper{T}"/>.</returns>
    public HtmlHelper<T> CreateHtmlHelper<T>(T model, TextWriter writer)
    {
        var container = new InternalViewDataContainer<T>(model);
        var context = new ViewContext(
            new ControllerContext(),
            new InternalView(),
            container.ViewData,
            new TempDataDictionary(),
            writer);

        return new HtmlHelper<T>(context, container);
    }
    #endregion

    #region Types
    /// <summary>
    /// Defines an internal view.
    /// </summary>
    private class InternalView : IView
    {
        #region Methods
        /// <summary>
        /// Renders the contents of the view to the specified writer.
        /// </summary>
        /// <param name="context">The current View context.</param>
        /// <param name="writer">The writer used to generate the output.</param>
        public void Render(ViewContext context, TextWriter writer) { }
        #endregion
    }

    /// <summary>
    /// Defines an internal view data container.
    /// </summary>
    private class InternalViewDataContainer<T> : IViewDataContainer
    {
        #region Methods
        /// <summary>
        /// Initialises a new instance of <see cref="InternalViewDataContainer{T}"/>.
        /// </summary>
        public InternalViewDataContainer(T model)
        {
            ViewData = new ViewDataDictionary<T>(model);
        }
        #endregion

        #region Properties
        /// <summary>
        /// Gets or sets the view data dictionary.
        /// </summary>
        public ViewDataDictionary ViewData { get; set; }
        #endregion
    }
    #endregion
}

/// <summary>
/// Provides a base implementation of a template base that supports <see cref="HtmlHelper{T}"/>s.
/// </summary>
/// <typeparam name="T">The model type.</typeparam>
[RequireNamespaces("System.Web.Mvc.Html")]
public class HtmlRazorTemplateBase<T> : RazorTemplateBase
{
    private readonly HtmlHelperFactory factory = new HtmlHelperFactory();

    /// <summary>
    /// Initialises a new instance of <see cref="HtmlTemplateBase{T}"/>.
    /// </summary>
    public HtmlRazorTemplateBase()
    {
        CreateHelper(Context);
    }
    /// <summary>
    /// Gets the <see cref="HtmlHelper{T}"/> for this template.
    /// </summary>
    public HtmlHelper<T> Html { get; private set; }

    private void CreateHelper(T model)
    {
        Html = factory.CreateHtmlHelper(model, Response.Writer);
    }
}

そのままですみませんね!

後は、テンプレートを書いてテイッ!と実行。

static string Template =
@"
@using System.Web.Mvc
@using System.Web.Mvc.Html
Hello @Context.Name
@AppDomain.CurrentDomain.FriendlyName
Email: @Html.TextBoxFor(m => m.Email)";

public void ExecuteParse()
{
    ExecuteParse(-1);
}

public void ExecuteParse(int counter)
{
    var model = new PageModel
                    {
                        Name = "World", 
                        Email = "someone@somewhere.com"
                    };
    var asms = AppDomain.CurrentDomain.GetAssemblies().Where(a => !a.IsDynamic ).Select(a=>a.Location);
    asms = asms.Concat(typeof(System.Web.Mvc.HtmlHelper)
                        .Assembly
                        .GetReferencedAssemblies().Select(an => Assembly.Load(an.FullName).Location)).Distinct();
    var refs = asms.Where(a => !a.EndsWith("System.dll")
                    && !a.EndsWith("System.Core.dll")
                    && !a.EndsWith("Microsoft.CSharp.dll")).ToArray();

    for (var i = 0; i < (counter != -1 ? 1 : Repeat); i++)
    {
        var sw = new Stopwatch();
        sw.Start();
        var engine = new RazorEngine<HtmlRazorTemplateBase<PageModel>>();
        var result = engine.RenderTemplate(Template, refs, model);

        sw.Stop();

        Console.WriteLine(result);
        Console.WriteLine(
            "{0} - {1} {2} ms {3:0,###} bytes",
            AppDomain.CurrentDomain.FriendlyName,
            counter != -1 ? counter : i,
            sw.ElapsedMilliseconds,
            Process.GetCurrentProcess().PrivateMemorySize64);
    }
}

なんですが、ここでかなりはまりました。

Template Execution Error: Security Transparent メソッド 'System.Web.Mvc.TypeDescriptorHelper.Get(System.Type)' がセキュリティ上重要なメソッド 'System.ComponentModel.DataAnnotations.AssociatedMetadataTypeTypeDescriptionProvider..ctor(System.Type)' にアクセスしようとして失敗しました。

アセンブリ 'System.ComponentModel.DataAnnotations, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' は条件付きの APTCA アセンブリであり、現在の AppDomain では有効化されていません。このアセンブリを有効化して部分信頼コードまたは Security Transparent コードで使用できるようにするには、AppDomain の作成時に、アセンブリ名 'System.ComponentModel.DataAnnotations, PublicKey=0024000004800000940000000602000000240000525341310004000001000100B5FC90E7027F67871E773A8FDE8938C81DD402BA65B9201D60593E96C492651E889CC13F1415EBB53FAC1131AE0BD333C5EE6021672D9718EA31A8AEBD0DA0072F25D87DBA6FC90FFD598ED4DA35E44C398C454307E8E33B8426143DAEC9F596836F97C8F74750E5975C64E2189F45DEF46B2A2B1247ADC3652BF5C308055DA9' を PartialTrustVisibleAssemblies リストに追加してください。

なんすかこのエラー。コエー。入っちゃいけない領域に入っちゃった系のエラーなんじゃ...。

ASP.NET アプリケーションでコード アクセス セキュリティを使用します。

なんかDataAnnotationsも含まれてるね~。透過的モデルに移行したアセンブリ。

だいたいが

[assembly: System.Security.AllowPartiallyTrustedCallers()]

これ書けって書いてるんだけど、これ何さ。ってなもんで、いろいろよくわかんない。そもそもRTEでは動くんだから何か違いが有るんだろうと、参照アセンブリとかいろいろ比較してみたわけですよ。

そしたらですね、ちゃんと違いがありました。

Razor Templating Engine

↑ここに行って、RazorEngine.Templates.csprojを見てみてください。

rh1

衝撃!分かります?普通さ~、Razorっつったら、MVC3を想定するじゃないっすか。だから、HtmlHelper使うために参照するアセンブリもMVC3のものにしちゃうじゃないっすか。そこだった...。ショック。MVC2で動かしてやがった。なんか意味があるんだろうね。いつかちゃんと調べるとしてですね、とりあえずRazor Hostingの場合もMVC2のアセンブリを参照してHtmlHelperを呼び出すとちゃんとできた。

rh2

はぁ...。ぜんぜんわかんなかった。いや、今でも理由はよく分からない。ATPCAってなにさ。部分信頼ってなんだね。AllowPartiallyTrustedCallersつけたら↓こう。

rh3

型 'ConsoleApplication1.HtmlRazorTemplateBase`1<T>' で継承セキュリティ ルールの違反が発生しました。派生型は、基本型のセキュリティ アクセシビリティと一致するか、それより低いアクセスが設定されている必要があります。

CASが廃止。.NET 4のセキュリティはどうなるのか? - @IT

ふんふん、そっか!なるほど!

...

よし、がんばろう。

で、コンパイル済みのものを呼び出すときは

var engine = new RazorEngine<HtmlRazorTemplateBase<PageModel>>();
var compiledId = engine.ParseAndCompileTemplate(refs, new StringReader(Template));

var result = engine.RenderTemplateFromAssembly(compiledId, model);

こうね。はいはい。

続いて、違うAppDomainで実行するときはこう。

var host = new RazorStringHostContainer<HtmlRazorTemplateBase<PageModel>>
{
    UseAppDomain = true,
    ReferencedAssemblies = refs.ToList()
};

host.Start();
var result = host.RenderTemplate(Template, model);
host.Stop();

ちゃんとHost.Start()とStop()しようね。これがCreateAppDomainとUnload。

ちなみにこのコードはこのままじゃRazor Hostingでは動きません。何でかというと、RazirStringHostContainerはジェネリックじゃないから。基底クラスの指定方法がHost派生しかなかったから、それじゃ面倒なので、RazorStringHostContainer自体を書き換えてます。

といっても、定義だけ。

    public class RazorStringHostContainer<T> : RazorBaseHostContainer<T>
        where T : RazorTemplateBase, new()
    {        

rh4

ちょいちょいAppDomainを生成するのはやっぱり遅すぎ。もうちょっと賢くやんないとね。

と、言うわけでRazorのテンプレートエンジン2作品を取り上げてみたけど、HtmlHelperを使ったりするようにWebで使う場合はAPTCAのところをちゃんとクリアしてからじゃないとダメですね。

ここまで書いてて気がついたんだけど、MVC3ってどうやってるのか見るといいのかな。BuildManagerだと違ったりするのかな。うぬ~。なぞは深まるばかり。

2010年12月29日水曜日

Razor Templating Engine

Razor Templating Engine

CodePlexで新しいのが出てたよ~。面白いね~。テンプレートエンジンっていろいろ用途があるけど需要はどーなんだろね。まぁ、いいや。Razor記法でWeb以外にも使えるって楽しいっすよね。だっていろいろ覚えなくて済むし。ちなみにASP.NETの<%...%>とか<%$…%>とか<%#…%>なんかの記法も用途特化したテンプレートエンジンが解釈してクラスファイルを生成してますよね。

んでね、Razorは純粋にテンプレートエンジンって言ってるくらいだからWebのコンテキストや実行Hostに依存しないわけですね。つまりConsoleアプリでも使えるってことです。もちろんHtmlHelperとかWeb関連のアセンブリに依存するものはあるとしても、それ自体はRazorの機能じゃなくてHelperの機能。TemplateBaseクラスが生成されるクラスの親クラスになるって言う仕組みなので、親クラスにいろいろWeb関連の機能を保持するっていう、Razorとは直接関係なっしんぐ。

サンプル見てみるとこんな感じですね。

rte1

string template = "Hello @Model.Name! Welcome to Razor!";
string result = Razor.Parse(template, new { Name = "World" });

Console.WriteLine(result);
Console.ReadLine();

簡単ですね~。お気楽です。

同じくサンプルのHtmlHelperを使うパターン。

rte2

Razor.SetTemplateBaseType(typeof(HtmlTemplateBase<>));
string template =
@"<html>
    <head>
    <title>Hello @Model.Name</title>
    </head>
    <body>
    Email: @Html.TextBoxFor(m => m.Email)
    </body>
</html>";

var model = new PageModel { Name = "World", Email = "someone@somewhere.com" };
string result = Razor.Parse(template, model);

Console.WriteLine(result);
Console.ReadLine();

はい、これも簡単ですね。イメージ通りです。

PageModelは自分で作ってね。あと、RazorEngine.Coreだけじゃ足りなくて、RazorEngine.Templatesアセンブリも必要なので、ソースのダウンロードはここからどーぞ。

これらは全て実行時にクラスファイル(特に指定が無い限りc#)をオンメモリで生成してコンパイルします。そのタイミングでAppDomain内からはクラスのインスタンスを生成できるようになるって言う流れはASP.NETのパイプラインそのもの。

試しに最初のサンプル(Hello World!のほう)をステップ実行していくと、どういうクラス名で生成されるのかというのを追いかけていくと...。

rte3

雑ですね。Guid.NewGuid()から数字とハイフンを除外したのがクラス名。それにNamespaceをくっつけてます。で確認。

rte4

ちゃんといます。で、AppDomainの仕様を見てみるとこんなことがかかれてます。

既定のアプリケーション ドメインにロードされたアセンブリは、プロセスの実行中にメモリからアンロードすることはできません。 しかし、アプリケーション ドメインをもう 1 つ開いてアセンブリをロードおよび実行すると、そのアプリケーション ドメインがアンロードされたときに、アセンブリもアンロードされます。 このテクニックを使用すると、大きな DLL を使用する場合のある、長時間実行されるプロセスのワーキング セットを最小限に抑えることができます。

AppDomain クラス (System)

ん~、どーだろ。どうなるんだろね。

rte6

rte5

めちゃめちゃ分かりにくいですね。2個目のサンプル(HtmlHelper使うほう)を使ってRazor.Parseを繰り返してる途中です。

Razor.SetTemplateBaseType(typeof(HtmlTemplateBase<>));
var model = new PageModel { Name = "World", Email = "someone@somewhere.com" };

for (var i = 0; i < (singleExec ? 1 : Repeat); i++)
{
    var sw = new Stopwatch();
    sw.Start();
    var result = Razor.Parse(Template, model);
    sw.Stop();

    Console.WriteLine(
        "{0} - {1} {2} ms {3:0,###} bytes",
        result.GetHashCode(),
        i,
        sw.ElapsedMilliseconds,
        Process.GetCurrentProcess().PrivateMemorySize64);
}

少しずつメモリ使用量と動作時間が長くなっていきます。最初は200msとかで処理。すごく単純で1種類だけの生成なのにね。ちなみに一度コンパイルしたものをその後何度も使用する場合は超早い。

rte8

rte7

10000回実行してもすぐ終了。

これってつまり、ASP.NETでも同じでaspxを何度も書き換えてると処理時間はどんどん増加。いつか破綻?でもリサイクルも走るし、そもそも運用環境でそんなことしないから問題にはならないか。

AppDomainをアンロードしない限りアセンブリもアンロードされないってことはつまり常時動いてるようなシステムで頻繁にコンパイルを繰り返すのはよろしくないね!ってなりましょう。それをひっくり返すのがAppDomainを別に作ってそっちで動かす方法。

リファレンスにもちゃんとかかれてますね。ボスもタイムリーに(MEFだけど)、そんなことをブログに書いてました。

MEFでディレクトリカタログを追いかける(C# Advent Calender jp:2010 12/02) « kazuk は null に触れてしまった

なのでAppDomainを作って実行するように試してみた。

rte9

けど無理!遅すぎ!

var sw = new Stopwatch();
sw.Start();

AppDomainSetup ads = new AppDomainSetup();
ads.ApplicationBase = Environment.CurrentDirectory;
ads.DisallowBindingRedirects = false;
ads.DisallowCodeDownload = true;
ads.ConfigurationFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile;

AppDomain executeAppDomain = AppDomain.CreateDomain("AD#" + i, null, ads);
var razor = executeAppDomain.CreateInstanceAndUnwrap(
    Assembly.GetEntryAssembly().FullName,
    typeof(RazorTemplateTest).FullName
) as RazorTemplateTest;

razor.ExecuteParse(i);

AppDomain.Unload(executeAppDomain);

sw.Stop();

Console.WriteLine(" {0} : {1} ms",
    AppDomain.CurrentDomain.FriendlyName,
    sw.ElapsedMilliseconds);

でも、まぁ、思ったような結果が出なかった残念な調査でした。ホントはもっとメモリ開放されないことを想定してたんだけどな~。

オチがない...。

using System;
using System.Diagnostics;
using System.Reflection;
using RazorEngine;
using RazorEngine.Templating;

namespace ConsoleApplication1
{
    public class PageModel
    {
        public string Name { get; set; }
        public string Email { get; set; }
    }

    public class RazorTemplateTest : MarshalByRefObject
    {
        private static int Repeat = 10000;
        static string Template =
@"<html>
    <head>
        <title>Hello @Model.Name</title>
    </head>
    <body>
        Email: @Html.TextBoxFor(m => m.Email)
    </body>
</html>";

        public void ExecuteParse()
        {
            ExecuteParse(-1);
        }

        public void ExecuteParse(int counter)
        {
            Razor.SetTemplateBaseType(typeof(HtmlTemplateBase<>));
            var model = new PageModel { Name = "World", Email = "someone@somewhere.com" };

            for (var i = 0; i < (counter!=-1 ? 1 : Repeat); i++)
            {
                var sw = new Stopwatch();
                sw.Start();
                var result = Razor.Parse(Template, model);
                sw.Stop();

                Console.WriteLine(
                    "{0} - {1} {2} ms {3:0,###} bytes",
                    AppDomain.CurrentDomain.FriendlyName,
                    counter != -1 ? counter : i,
                    sw.ElapsedMilliseconds,
                    Process.GetCurrentProcess().PrivateMemorySize64);
            }
        }

        public void ExecuteCompile()
        {
            Razor.SetTemplateBaseType(typeof(HtmlTemplateBase<>));
            var model = new PageModel { Name = "World", Email = "someone@somewhere.com" };

            Razor.Compile(Template, typeof(PageModel), "test");

            for (var i = 0; i < Repeat; i++)
            {
                var sw = new Stopwatch();
                sw.Start();

                var result = Razor.Run(model, "test");

                sw.Stop();

                Console.WriteLine(
                    "{0} - {1} {2} ms {3:0,###} bytes",
                    result.GetHashCode(),
                    i,
                    sw.ElapsedMilliseconds,
                    Process.GetCurrentProcess().PrivateMemorySize64);
            }
        }

        public void ExecuteAnotherAppDomain()
        {
            for (var i = 0; i < Repeat; i++)
            {
                var sw = new Stopwatch();
                sw.Start();

                AppDomainSetup ads = new AppDomainSetup();
                ads.ApplicationBase = Environment.CurrentDirectory;
                ads.DisallowBindingRedirects = false;
                ads.DisallowCodeDownload = true;
                ads.ConfigurationFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile;

                AppDomain executeAppDomain = AppDomain.CreateDomain("AD#" + i, null, ads);
                var razor = executeAppDomain.CreateInstanceAndUnwrap(
                    Assembly.GetEntryAssembly().FullName,
                    typeof(RazorTemplateTest).FullName
                ) as RazorTemplateTest;

                razor.ExecuteParse(i);
                
                AppDomain.Unload(executeAppDomain);

                sw.Stop();

                Console.WriteLine(" {0} : {1} ms",
                    AppDomain.CurrentDomain.FriendlyName,
                    sw.ElapsedMilliseconds);
            }
        }
    }

    class Program
    {

        static void Main(string[] args)
        {
            //new RazorTemplateTest().ExecuteParse();
            //new RazorTemplateTest().ExecuteCompile();
            new RazorTemplateTest().ExecuteAnotherAppDomain();

            Console.ReadLine();
        }
    }
}

こんなコードで~す。

2010年12月18日土曜日

Razor Helper的なForEachでテンプレート

今年も終わりですね。タイトル意味分かんないですね。

来週から夏休み&年末年始休暇に入るので、今年の仕事終了です。てへ。ブログサイトがあるのをすっかり忘れるくらい追い込まれる毎日。よくもまぁ、こんな状態が続くもんだと感心するばかりです。え?きれてねーよ。

そんなことはいいんですけどね、ちょっとここ見てみてくださいよ。

Razor, Nested Layouts and Redefined Sections - Marcin On ASP.NET - Site Home - MSDN Blogs

ナイスエントリーにも程があるデスヨ。

Layout用のビューを直接指している、子供ビューのセクションはみえるけど、多段Layoutになってて孫ビューのセクションはスコープの範囲外でエラーになっちゃうっていうエントリー。

実際に試してみたらたしかに動かない。Section not defined。そ

foreach

りゃそうだ。それを克服するために中間LayoutでRedefineSectionで孫のセクションを生成(WebPageBase.DefineSection)してしまおうよっていう強引さには度肝を抜かれますがなによりそこじゃないんです。

その部分より何よりセクション初期値をヘルパーに渡す部分ですよ!

Func<object, HelperResult> defaultContent

これを知っておくと何が出来るかというと...

@{
    Layout = "~/Views/Shared/_Layout.cshtml";
}
@helper ForEach(IEnumerable<int> values, Func<object, HelperResult> template){
    var partialHtml = new System.Text.StringBuilder();
    foreach(var value in values){
        partialHtml.Append( template(value));
    }

    Write(MvcHtmlString.Create(partialHtml.ToString()));
}
<div>
<ul>
@ForEach(Enumerable.Range(1,5),
    @<li>@item</li>
)
</ul>
</div>

↑こんなコード書けるってことですよ。ヘルパーにRazor構文のテンプレートを渡してるんですよ。いいでしょ?ん~、もんどりうつ。

追記:もっと簡単でよかった。

@helper ForEach(IEnumerable<int> items, Func<object, HelperResult> template){
    foreach(var item in items){
        Write(template(item));
    }
}

@helperだとGenericメソッドに出来なさそうなので、もうちょっと調べてみたら、普通に@functionsで実装するサンプルをAndrew君が...。

VibrantCode - Inside Razor - Part 3 – Templates

この場合は↓こうね。

@{
    Layout = "~/Views/Shared/_Layout.cshtml";
}
@functions{
    static IHtmlString ForEachGeneric<T>(IEnumerable<T> items, Func<T, HelperResult> template)
    {
        var sb = new System.Text.StringBuilder();
        foreach(var item in items){
            sb.Append(template(item));
        }
        
        return MvcHtmlString.Create(sb.ToString());
    }
}

<div>
<ul>
@(ForEachGeneric<int>(Enumerable.Range(1,5),
    @<li>@item</li>
))
</ul>
</div>

 

foreach2

小っちゃ!
クリックして拡大してみてね。

Razorのコンパイル結果を見てみると分かるけど、ヘルパーに渡すテンプレートブロックにはobject型のitemが必ずわたりまする。

dotnetConf2015 Japan

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