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

すません...。

dotnetConf2015 Japan

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