kazuk は null に触れてしまった

C# / .NET 系技術ネタ縛りでお送りしております

タグアーカイブ: WebMatrix

ASP.NET WebPages って System.Web.Routings とまた違うルーティングメカニズムもってるよね!


この間の WebMatrix の管理ページを拡張する話、昨日たどりついた ASP.NET MVC で WebMatrix の管理ページを出す方法、どっちも ApplicationPart ってのが VirtualPath をアセンブリにマップして、アセンブリ内に置かれたページを出してるわけでございまして。

その _admin を出すようにした ASP.NET MVC アプリケーションのルートテーブルをダンプしても _admin へのルートなんて存在しないわけでありまして。

同様に WebMatrix のサイトでは Routing に “r.ashx/{module}/{*path}” しかないわけで。

どう考えてもなんか怪しいですよねってのが今回のネタ。

cshtmlのExecuteにブレークポイント貼ってみた

この間の WebMatrix の管理ページをカスタムしてみたページに System.Diagnostics.Debugger.Break() 仕込んで MVC アプリケーションに _admin 出す細工をしてカスタムアドミンページに飛び込んだ時のスタックトレースが以下。

image 

スタックトレースを見ると2回の WebPageBase.ExecuteHierarcy の呼出しがある。これは _admin の内側(自分がカスタムした部分)と外側(レイアウト部)と考えられるのでWebPageHandlerのProcessReqest.AnonymouseMethod あたりから実際のページの処理になっていますねと。普通に IHttpHandler として実行されていますってだけしか解らず。これはルーティングが解決された後の話で Handler がnew される瞬間を捕まえてるわけじゃないんでルーティングがらみを追うとしては手遅れ

cshtml のctorにブレークポイント貼ってみた

じゃぁハンドラをnewしてる所をって事でコンストラクタにブレークを置いた結果が以下。

image

んで、これを元手にソースを codeplex から取ってきたソースを追ってみましょうって話。

まずスタックトレースを眺めて入口を探す

System.Web.HttpApplication.IExecutionStep.Execute までは実質 ASP.NET ランタイムが HttpApplication をドライブしてる所なんで無視して良いとして WebPageHttpModule.OnApplicationPostResolveRequestCache という事で、リクエストキャッシュの後処理の中でハンドラを new しているようですね。(System.Web.HttpApplication.PostResolveRequestCacheイベント)

Context.RemapHandler はハンドラマッピング=ルーティング

次の段階が WebPageRoute.OnPostResolveRequestCache ですね。はい、確かにここで Context.RemapHandler 呼んでるし、ハンドラの解決をしてるっぽいですね。

で、そのクラス内の MatchRequest が返した WebPageMatch で決まるページが暗黙にDisabledでなければ CreateFromVirtualPath してスタックトレースの通りに流れに飛び込みます。

って事は実質のルーティング作業は MatchRequest がやってますね。

MatchRequestは FileExists ってアセンブリ内のクラスだからファイル無いとか安心できずパラメータ名の virtualPathFactoryManager からして VirtualFile の可能性が怪しいんでと潜ってみると、VirtualPathFactoryManager.PageExistsに行ってやっぱりすぐ隣で VirtualPathProvider とか触ってるしで

            if (_virtualPathFactories.Any(factory => factory.Exists(virtualPath)))
                 return true;

でnew List<IVirtualPathFactory>()のスキャンと。

IVirtualPathFactoryの向こう側

実装がインターフェースの向こう側に行ってしまったので闇の中って感じなんですが、ここでReSharperにFind Usages Advanced すると DictionaryBasedVirtualPathFactory しか実装無いよとおっしゃるので、本当ですかそうですか。確かにスタックトレースでも DictionaryBasedVirtualPathFactory.CreateInstance の中飛び込んでるんでそこに行ってるんですね。

ApplicationPartが居た

調子にのってReSharperで DictionaryBasedVirtualPathFactory をFind Usages Advanced すると

image

ビンゴで ApplicationPart につながったと。

登録側から追う

ApplicationPart.Register が ApplicationPartRegistory.Register に行って、ApplicationPartRegistory.Register が…

                // Get all of the web page types
                var webPageTypes = from type in applicationPart.Assembly.GetTypes()
                                   where type.IsSubclassOf(_webPageType)
                                   select type;
                // Register each of page with the plan9
                foreach (Type webPageType in webPageTypes) {
                    RegisterWebPage(applicationPart, webPageType, registerPageAction);
                }

Assemblyの中のおいしそうなTypeを RegisterWebPage してて、RegisterWebPageが

            // Register a page factory for it
            _virtualPathFactory.RegisterPath(virtualPath, pageFactory);

_virtualPathFactoryに…で _virtualPathFactory が目的のDictionaryBasedVirtualPathFactoryですよっと。

private readonly DictionaryBasedVirtualPathFactory _virtualPathFactory

繋がった結果のまとめ

で、結果眺めると、やっぱり System.Web.Routing つかってねー!!!!

System.Web.Routing でのハンドラマッピングは PostResolveRequestCache だとどっちが先かっていう事ですが…UrlRoutingModule Class (System.Web.Routing) のメソッド PostResolveRequestCache、一緒かよ!

って訳でモジュールの登録順依存で Routing が勝つかもしれんし、ApplicationPartでのマッピングが勝つかもしれないねぇと。(後に MapRequestHandler した方が勝つ、いわゆる後勝ち)

で、どうなのよ。というのに答えてくれるのはやっぱりエース。 無聊を託つ: WebMatrixの認証設定とヘルパー の中ほどでModuleのリストしてるねっ!

はい、 ApplicationPart の勝利でございました。

まとめのまとめ

ReSharperとエース、これでまず間違いない。

WebMatrix で管理ページをカスタムする方法


うぇぶまとりっくすたん側で書くネタ探しに挑戦し始めたのですが、全然WebMatrix単体で完結せず、しかもVisual Studio標準機能では完結しなかったという訳で、VS拡張を一個作る羽目に…という事でうぇぶまとりっくすたんのスコープ(WebMatrixユーザー、開発初心者向け)から落ちたという形で、こっちのブログに書く事にしました。

今回カスタムするのはWebMatrixでテンプレートからサイトを作成、実行し _admin へ遷移してログインをすると表示されるページ、デフォルトでは Library Package Manager として NuGetパッケージの参照およびインストール機能が入ってる所です。

ここに Library Package Manager から Publish Settings Generator をインストールすると展開に使用する設定の生成に関するページが表示されるという事で、ここにカスタムをNuGetパッケージから入れる事ができるというのは見た目から解ってまして、それを実装に落としていく上で必要になった事柄を説明していきたいと思います。

まず、この管理画面を提供している物、こいつがなんなのかですが、System.Web.Pages.Administration 配下でこの管理画面が提供されており、 SiteAdmin クラスが今回の主なお話相手になります。

んで、管理ページの一覧に出す方法ですが、以下のコードになります。クラスライブラリプロジェクトで以下を実装してください。

using System.Web;
using System.Web.WebPages;
using System.Web.WebPages.Administration;
[assembly: PreApplicationStartMethod(
        typeof(CustomAdministrationHelper.PreApplicationStartCode),
        “Start”)]

namespace CustomAdministrationHelper
{
    public static class PreApplicationStartCode
    {
        public static void Start()
        {
            SiteAdmin.Register(“~/customAdmin/”, “test custom admin page”, “custom admin page”);
            var appPart = new ApplicationPart(
                    typeof(PreApplicationStartCode).Assembly,
                    SiteAdmin.GetVirtualPath(“~/customAdmin/”));
            ApplicationPart.Register(appPart);
        }
    }
}

単純には PreApplicationStartMethodアセンブリ属性でマークされた public static void Start メソッドで SiteAdmin オブジェクトに項目を登録します。そしてApplicationPartオブジェクトに該当アセンブリを virtual path prefix を指定して追加します。

これの最初の SiteAdmin.Register を指定した時点で、WebMatrixの管理ページには以下のように項目が表示されます。アプリケーションの bin 配下にコンパイルされたアセンブリを配置してください。

image

続く ApplicationPart の追加によって、追加したアセンブリ内で定義されるページの表示へのURL解決が設定されます。

要するに該当アセンブリに cshtml があり、それがコンパイルされた結果があり、URLの解決がうまくいけばページが表示されるわけです。

ここで第一の問題 cshtml のコンパイルされた結果を得る方法とは?

これを解決するには cshtml を C# コードに変換し、C#コンパイラにアセンブリにクラスを含めてもらうという事です。

ASP.NET MVC 3 Razor の場合にはコードギャラリーの Razor Single File Generator for MVC を利用する事で解決します。

そうでないノーマルな cshtml の場合にはこの元となった Razor Single File Generator があるのですが、このジェネレータは残念な事に @Helper な cshtml にしか対応していません。

という事でこの Razor Single File Generator を改造し、 cshtml の一般ページ向けのジェネレータを作成しました、現物は私の SkyDrive のpublicに、説明ページはこちらにありますのでインストールしてください。

cshtml ページを作成し、カスタムツールに RazorPagePreprocessor を指定するとC#なコードが得られます。

次に、URLの解決です。cshtmlを元に生成されるクラスに System.Web.PageVirtualPathAttribute による属性修飾が必要です。

この属性修飾ですが、ジェネレータで一括対応する事もできたのですが、本来表示したくないページをURLにマップしてしまう可能性も無いわけではないので( @inherits の継承元ページにURLをマップしてしまうとまずいです)、コード生成を partial で行う事で、「別の partial パートで属性修飾すればいいじゃない」という逃げを打っています。

なので partial part を付与します。

namespace CustomAdministrationHelper
{
    [System.Web.WebPages.PageVirtualPath(“~/Default.cshtml”)]
    partial class _Page_Default_cshtml
    {
    }
}

これにより、アセンブリにマップされた ~/customAdmin/ の配下の ~/Default.cshtml が _Page_Default_cshtml クラスであるという属性修飾が行われます。

ここで、簡単な以下の cshtml を Default.cshtml としてみました。

<html>
<head><title>てすとです</title>
</head>
<body>ぼでぃー</body>
</html>

表示させた結果が以下。

image

ちゃんと出ましたね。

という訳で、 WebMatrix の管理ページをカスタムするサンプルでした。

サンプルプロジェクトは SkyDrive にあげてあります のでそちらもどうぞ。