CodeZine(コードジン)

特集ページ一覧

ASP.NETで再利用可能なページテンプレートを作成する方法

XMLとリフレクションを利用したページテンプレート作成のテクニック

  • ブックマーク
  • LINEで送る
  • このエントリーをはてなブックマークに追加
2005/08/25 12:00

本稿ではXMLとリフレクションを利用して、再利用可能なページテンプレートを作成する方法を解説します。この方法を利用すると、テンプレートのUIをWebアプリケーションの全ページに継承させたり、UIを普通のHTML/ASP.NETファイルを編集するような手軽さで変更したりすることができます。

はじめに

 コードの再利用は、オブジェクト指向プログラマの目指すべき大目標の1つです(少なくとも、そうあるべきです)。再利用可能なコードには、開発にかかる時間の短縮と、アプリケーションに新しいバグが持ち込まれる危険の減少というメリットがあります。幸い、ASP.NETというフレームワークが出現し、クラシックASPの時代に比べて、再利用可能なコードを容易かつエレガントに作成できるようになりました。コードを再利用可能にするためのすばらしい技術の数々(継承、ASP.NETサーバーコントロール、ユーザーコントロールなど)を、私自身、非常にありがたく思っています。

 ただ、ASP.NETにも改善の余地がないわけではありません。UIレベルにおけるASP.NETページの再利用可能性も、そうした弱点の1つです。この問題に立ち向かうための技術はいくつか存在します。なかで最もよく使われるのが、ベースページのOnRender()メソッドをオーバーライドする方法です。Webアプリケーションの全ページをベースページから継承すれば、当然、アプリケーション全体のルック&フィールの一貫性が保証されます。

 しかし、この方法には2つの大きな欠点があるように思えます。ベースページのOnRender()メソッドは、アプリケーションのネスト構造の深いところに存在します。とすれば、アプリケーションの配備後、UIの何かを変更したくなったときに何が起こるでしょうか。もちろん、ベースページのOnRender()メソッドを編集しなければならず、コードを編集すれば再コンパイルが必要になり、再コンパイルすれば再テストと再配備が待ち受けています。どれもあまり楽しそうな作業には聞こえません。

 もう1つの難点は、この方法がコードファイル(C#であれ、VBであれ)に100%依存していることです。つまり、ベースページと関連付けられているASP.NETページ(私自身は「コードビハインド」に対して「コードフロント」と呼んでいます)がありません。Webアプリケーション全体に渡って配置したいUIコンポーネントがあれば、それをプログラム的に作成し、ベースページに付け加えなければなりません。これは不便です。

 欲しいのは、Webアプリケーションの全ページにベースページの「機能」を継承させるだけでなく、UIまでも継承させる方法でしょう。さらに、ベースページのUIを、普通のHTML/ASP.NETファイルを編集するような手軽さで変更できれば、言うことがありません。

 それは可能だと言ったら、信じられますか。本稿では、それを現実のものにする手法を紹介します。

基本のルック&フィール

 順序よく進めていきましょう。まず、Webアプリケーションの「基本のルック&フィール」を決めなければなりません。この記事のために次のHTMLとCSSを作ってみました。料理に関するWebサイトを想定しています。ヘッダとフッタのほかに、左側にナビゲーションバーを置くことにしました。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 
    Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

<html>
<head>
    <title>Cooking: it's more fun than you think!</title>

    <style type="text/css">
        body
        {
            font-family: verdana; font-size: 11px; color: #676767;
            margin: 0px; padding: 0px; background-color: #cccccc;
        }

        #Container
        {
            position: relative;
            width: 760px;
            margin: auto;
            margin-top: 10px;
            background-color: #003366;
        }

        #Header
        {
            height: 80px; line-height: 80px;
            background-color: #336699;
            font-size: 18px; font-weight: bold; color: #ffffff;
            text-align: center;
            border-bottom: solid 1px #cccccc;
        }

        #NavBar
        {
            position: absolute; top: 80px; left: 0px;
            width: 130px; height: 300px;
            padding: 5px;
            color: #ffffff;
        }

        #PageContent
        {
            background-color: #ffffff;
            position: relative; top: 0px; left: 140px;
            padding: 10px;
            width: 600px;
            height: 300px;
        }

        #Footer
        {
            padding-left: 10px;
            height: 30px; line-height: 30px;
            background-color: #336699;
            font-weight: bold; color: #ffffff;
            text-align: center;
            border-top: solid 1px #cccccc;
            border-bottom: solid 1px #cccccc;
            clear: both;
        }
    </style>
</head>

<body>
    <div id="Container">
        <div id="Header">
            Cooking: it's more fun than you think!
        </div>

        <div id="NavBar">
            <b>Navigation:</b>
            <ul>
                <li></li>
                <li>Searing Chicken</li>
                <li>Roasting Pork</li>
                <li>Onion Soup</li>
            </ul>
        </div>

        <div id="PageContent">
            <asp:Label id="lblBaseLabel" runat="server">Hi, I'm the Base Label!</asp:Label>

            <br />
            <asp:PlaceHolder id="phMainContent" runat="server" />
        </div>

        <div id="Footer">
            Copyright (C) Cooking: It's more fun than you think, Inc. 2005. All rights reserved.
        </div>
    </div>
</body>
</html>

 もちろん、次のようにしてCSSを外部スタイルシートにすれば、もっとよいものになります。

<link rel="stylesheet" type="text/css" href="/_stylesheets/layout.css" />

 ここでは、CSSを外部ファイルに隠す前にひととおり見てもらいたかったので、あえて上のようにしてみました。

 ページテンプレートを作成するための次のステップは、上記のASP.NET/HTMLをXMLファイルに収めることです。そう、XMLファイルです。こんなふうにすればよいでしょう。

<?xml version="1.0" encoding="utf-8" ?>
<PageTemplate>
   
    <![CDATA[
   
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 
    Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

<html>
<head>
    <title>Cooking: it's more fun than you think!</title>
    <link rel="stylesheet" type="text/css" href="./_stylesheets/layout.css" />
</head>

<body>
    <div id="Container">
        <div id="Header">
            Cooking: it's more fun than you think!
        </div>

        <div id="NavBar">
            <b>Navigation:</b>
            <ul>
                <li></li>
                <li>Searing Chicken</li>
                <li>Roasting Pork</li>
                <li>Onion Soup</li>
            </ul>
        </div>

        <div id="PageContent">
            <asp:Label id="lblBaseLabel" runat="server">Hi, I'm the Base Label!</asp:Label>
           
            <br />
            <asp:PlaceHolder id="phMainContent" runat="server" />
        </div>

        <div id="Footer">
            Copyright (C) Cooking: It's more fun than you think, Inc. 2005. All rights reserved.
        </div>
    </div>
</body>
</html>   

    ]]>

</PageTemplate>

 特別なことは何もしていないことがおわかりでしょう。全体をXMLタグで囲んだだけです。CDATAブロックにもお気づきでしょうか。これは、後でこのファイルをプログラムから読み込むときに、XMLエラーが起こらないようにするための予防線です(もちろん、HTMLがXHTML準拠であることを確認しなければなりませんが、それはまた別の機会に述べることにします)。

 最後に、<asp:PlaceHolder id = "phMainContent" runat = "server" />コントロールに注目してください。これは重要です。このPlaceHolderは、テンプレートで規定されていない内容の正確な表示位置を表しています。つまり、後で継承ページ上に定義したコントロールはすべて、このphMainContentというIDを持つPlaceHolderの位置に置かれることになります。

 これでXMLテンプレートファイルの用意ができました。このファイルは、これから書くベースページクラスに対応するASP.NETページ(またはコードフロント)と見なすことができます。

ベースページクラスの作成

 このページテンプレートの作成プロセスは、次の4つのステップから成ります。

  1. 継承する側のページからすべてのコントロールを取り出し(取り除き)、それを一時コントロール配列に保管します。
  2. XMLテンプレートファイル内のHTMLを文字列変数に読み込みます。
  3. HTMLを格納している文字列変数を解析して、一連のASP.NETリテラルコントロール、HtmlControlsWebControls、サードパーティコントロール(あれば)を識別し、新しく解析されたコントロールをページのControlCollectionに追加します。
  4. 手順1で一時コントロール配列に保管しておいたすべてのコントロールを取り出し、phMainContentというIDを持つPlaceHolderに追加します。

 ちょっと入り組んでいますが、進むにつれて意味がはっきりしてくるはずですから、とにかく始めましょう。まず、BasePageクラスを作成し、ステップ1を実行します。

 BasePageクラスの基本シェルは、次のようなものになるはずです。

using System;
using System.Xml;
using System.Web.UI;
using System.Reflection;
using System.Web.UI.WebControls;

namespace sstchur.web.Pages
{
    public class BasePage : System.Web.UI.Page
    {
        // Protected Variables (UI Components)
        protected Label lblBaseLabel;

        // Private Variables
        private string m_strTemplateFile;

        // Public Properties
        public string TemplateFile
        {
            set { m_strTemplateFile = value; }
        }

        // Constructor
        public BasePage()
        {
            // Initialize the TemplateFile in case one is not specified
            m_strTemplateFile = "~/_templates/standardpage.xml";
                 }

        protected override void OnInit(EventArgs e)
        {
            // Load the XHTML Template
            InvokePageTemplate();

            // Initialize any components/variables specific to this class (or its base)
            InitializeComponent();

            // Let the base class do its thing
            base.OnInit (e);
        }

        private void InitializeComponent()
        {
            // Wireup Page_Load Event
            this.Load += new EventHandler(Page_Load);
        }

        private void Page_Load(object sender, EventArgs e)
        {
            // Put any needed Page_Load functionality here
        }

        // Here is where all the tricky stuff happens
        private void InvokePageTemplate()
        {
            // Here is where the bulk of the BasePage's implementation will eventually go
        }
    }
}

 まず目につくことの1つは、using System.Xmlusing System.Reflectionという、あまり見かけないusingステートメントがあることでしょう。TemplateFileがXMLで書かれていることから、System.Xmlの機能にアクセスする必要があることは想像できたかもしれませんが、System.Reflection名前空間はちょっと意外だったのではないでしょうか。これが必要になる理由は、もう少し先へ行かないとわかりません。ですから、詳しい説明は、その名前空間を実際に使用するコード部分にたどり着くまで後回しにします。

 これ以外には、特に驚くような点はありません。標準のSystem.Web.UI.Pageクラスから諸々を継承し、Page_Loadメソッドを定義します。このとき使用するのは、新しいWebFormを作成するときにVS.NETが実装してくれた既定のテクニックです(OnInit()メソッドをオーバーライドし、InitializeComponent()を呼び出します)。

 InitializeComponent()を呼び出す前に、InvokePageTemplate()というプライベートなカスタムメソッドを呼び出していることに注意してください。このメソッドを、これから実装します。上記の4段階のステップを思い出してください。ステップ1は、継承する側のページからすべてのコントロールを取り出し(取り除き)、それを一時コントロール配列に保管することでした。まず、この部分を次のようにして実装します。

private void InvokePageTemplate()
{
    // Copy off inheriting page's control into a temporary Control[] array
    Control[] controls = new Control[this.Controls.Count];
    this.Controls.CopyTo(controls, 0);
    this.Controls.Clear();
}

 ステップ2は、XMLテンプレートファイルのHTMLを文字列変数に読み込むことでした。この処理を行うコードを追加します。

private void InvokePageTemplate()
{
    // Copy off inheriting page's control into a temporary Control[] array
    Control[] controls = new Control[this.Controls.Count];
    this.Controls.CopyTo(controls, 0);
    this.Controls.Clear();

    // Load the XML tempalte file into an XmlDocument object
    doc = new XmlDocument();
    doc.Load(Server.MapPath(m_strTemplateFile));

    // Place the content's of the <PageTemplate> tag into a string variable
    XmlElement root = doc.DocumentElement;
    XmlNode nodeTemplate = root.SelectSingleNode("//PageTemplate");
    string strTemplate = nodeTemplate.InnerText;
}

 ステップ3は、HTMLを格納している文字列変数を解析して、一連のASP.NETリテラルコントロール、HtmlControlsWebControls、サードパーティコントロール(あれば)を識別し、新しく解析されたコントロールをページのControlCollectionに追加することでした。これは、Page.ParseControl()メソッドを使って驚くほど簡単に実現できます。このメソッドで一番驚いたのは、繰り返し呼び出す必要がないことです。解析する文字列の中でコントロールがネストになっているときは、内側のコントロールが外側のコントロールの子コントロールになります(基本的には、.NETがASP.NETページを解析するときと同じです)。この処理を追加すると次のようになります。

private void InvokePageTemplate()
{
    // Copy off inheriting page's control into a temporary Control[] array
    Control[] controls = new Control[this.Controls.Count];
    this.Controls.CopyTo(controls, 0);
    this.Controls.Clear();

    // Load the XML tempalte file into an XmlDocument object
    doc = new XmlDocument();
    doc.Load(Server.MapPath(m_strTemplateFile));

    // Place the content's of the <PageTemplate> tag into a string variable
    XmlElement root = doc.DocumentElement;
    XmlNode nodeTemplate = root.SelectSingleNode("//PageTemplate");
    string strTemplate = nodeTemplate.InnerText;

    // Parse the HTML/XHTML contained in the strTemplate string;
    // add the parsed controls to the Page's ControlCollection.
    Control ctrlTemplate = Page.ParseControl(strTemplate);
    this.Controls.Add(ctrlTemplate);
}

 ステップ4では、ステップ1で一時コントロール配列に保管しておいたすべてのコントロールを取り出し、phMainContentというPlaceHolderに追加します。ただ、これを行うためには、phMainContentというPlaceHolderへの参照が必要です。特にわかりにくいこともないので、一気にやってしまいましょう。この部分のコードを追加すると次のようになります。

private void InvokePageTemplate()
{
    // Copy off inheriting page's control into a temporary Control[] array
    Control[] controls = new Control[this.Controls.Count];
    this.Controls.CopyTo(controls, 0);
    this.Controls.Clear();

    // Load the XML tempalte file into an XmlDocument object
    doc = new XmlDocument();
    doc.Load(Server.MapPath(m_strTemplateFile));

    // Place the content's of the <PageTemplate> tag into a string variable
    XmlElement root = doc.DocumentElement;
    XmlNode nodeTemplate = root.SelectSingleNode("//PageTemplate");
    string strTemplate = nodeTemplate.InnerText;

    // Parse the HTML/XHTML contained in the strTemplate string;
    // add the parsed controls to the Page's ControlCollection.
    Control ctrlTemplate = Page.ParseControl(strTemplate);
    this.Controls.Add(ctrlTemplate);

    // Get a reference to the phMainContent PlaceHolder
    PlaceHolder phMainContent = (PlaceHolder)ctrlTemplate.FindControl("phMainContent");

    // Add each control from the inheriting page (saved from Step 1) 
    // to the phMainContent's ControlCollection
    foreach (Control c in controls)
        phMainContent.Controls.Add(c);
}

 ここで終わることもできます。中身が何もない(つまり、<%@ Page ... %>ディレクティブだけの)新しいASP.NETページを作成し、sstchur.web.Pages.BasePageからの継承を行わせれば、本稿の先頭で作成したテンプレートファイルにそっくりの、きちんと動作するASP.NETページが得られます。試したい方はどうぞやってみてください(ただ、すべてをコンパイルすることを忘れてはなりません)。

 この時点で唯一の問題は、ベースページのどのコンポーネントにもアクセスできていないことです。テンプレートファイルにラベルを1つ追加しておいたことにお気づきでしょうか。ラベルのIDは「lblBaseLabel」、既定のテキストは「Hi, I'm the Base Label!」です。ベースページ上のコンポーネントにアクセスする例を実際にお見せするために、あらかじめ用意しておきました。現時点のBasePageクラスでは、このlblBaseLabelのようなベースページ上のコンポーネントにアクセスすることはできません(やってみることは可能ですが、残念ながらNullReferenceExceptionになります)。

 私が思いついた解決策は2つです。基本的な考え方はどちらも似ていますが、便利さは大きく違います。1つ目の方法は、FindControl()を使い、所定のIDを持つコントロールへの参照を探すというものです。今回のサンプルのlblBaseLabelで言うと、継承する側のページに置かれるコードは次のようになります。

Label baseLabel = (Label)Page.FindControl("lblBaseLabel");
baseLabel.Text = "Overridden value";

 しかし、ベースページのコントロールにアクセスするたびにFindControl()を呼び出さなければならないのでは、手間がかかりますし、洗練されたやり方とは言えません。

 もう1つの方法の方が、解決策としてはずっとすぐれています。こちらの方法では、ベースページのコンポーネントにbase.{ComponentId}という形でアクセスします。この{ComponentId}は、アクセスしたいコンポーネントのIDを表し、今回の例ではbase.lblBaseLabelとなります。

 具体的にはどうするのでしょうか。もちろん、リフレクションを使います。リフレクションを本格的に説明するとなると、とても本稿ごときには収まりませんから、ここでは、BasePageクラスにあるリフレクションベースのコードを理解するのに必要な、最小限の事柄だけを説明します。まず、ここで何をやろうとしているのかをしっかり理解することから始めましょう。まとめると、次のようになります。

  • すべてのASP.NETページの継承元となるBasePageクラスがあります。
  • BasePageクラスから継承するということは、機能だけでなく、XMLテンプレートファイルに含まれているUIコンポーネントも継承することを意味します。
  • BasePage内でXMLテンプレートファイルのコンポーネントにアクセスするためには、BasePageクラスに、XMLテンプレートファイル内の当該ASP.NETコンポーネントのIDと同じ名前を持つ保護されたメンバ変数を追加する必要があります。
  • さらに、BasePageクラスのすべての保護されたメンバ変数を、継承する側のページからアクセス可能にする必要があります(アクセス時にNullReferenceExceptionが返されてはなりません)。
  • 最後に、前述のとおり、NullReferenceExceptionを避けるための1つの解決策はFindControl()を呼び出すことです。

 ここに記したすべての要件を踏まえると、実装すべきロジックは次のように要約できます。

 BasePageクラス内のすべての保護されたメンバ変数に対して、次のことをする。

  • 変数名を特定する(コンパイラの事情)
  • FindControl(variableName)を呼び出し、variableNameと同じIDを持つコントロールへの参照を取得する
  • 保護されたメンバ変数の値を、FindControl()で返されたコントロールと等しくなるよう設定する

 複雑そうに聞こえますか? 実を言うと、多少複雑です。しかし、その複雑さのかなりの部分は、.NETフレームワークのSystem.Reflection名前空間によってラップされています。そのため、ここでやろうとしていることは、思ったほど難しくありません。

 まず、BasePage内に定義されている保護されたメンバ変数の1つ1つを特定する必要があります。それにはFieldInfoクラスを使用します。ここでは、FieldInfoオブジェクトの配列を作成し、ベースページ内に定義されている各変数に関する情報をそこに格納することにします。その後、sstchur.web.Pages.BasePageという型のTypeオブジェクトを作成し、最後にそのTypeオブジェクトのGetFields()を呼び出して、得られた配列を先ほどのFieldInfo配列に代入します。この処理は次のようになります。

FieldInfo[] fieldInfo;
Type myType = typeof(sstchur.web.Pages.BasePage);
fieldInfo = myType.GetFields(BindingFlags.NonPublic | 
    BindingFlags.Instance | BindingFlags.DeclaredOnly);

 GetFields()メソッドに渡されたBindingFlagsは、どの種類の変数についての情報が欲しいのかを指定するための方法です。ここでは、明示的に宣言されている(DeclaredOnly)、パブリックでない(NonPublic:実際には保護されていますが、それについてはすぐに説明します)、インスタンス変数(Instance)についての情報を要求しています。

 fieldInfo配列に値が格納されたら、その配列をループで処理しながら、残るいくつかの仕事をしていきます。

FieldInfo[] fieldInfo;
Type myType = typeof(sstchur.web.Pages.BasePage);
fieldInfo = myType.GetFields(BindingFlags.NonPublic | 
    BindingFlags.Instance | BindingFlags.DeclaredOnly);

for (int i = 0; i < fieldInfo.Length; i++)
{
    if (!fieldInfo[i].IsPrivate)
    {
        string id = fieldInfo[i].Name;

        Control c = ctrlTemplate.FindControl(id);
        if (c != null)
            fieldInfo[i].SetValue(this, c);
    }
}

 上のforループでは、現在の値がプライベートでないことを確認しています(前述のGetFields()の呼び出しでは、NonPublicフィールドだけを取り出しました。したがって、パブリックでなくプライベートでもないフィールドは、保護された変数ということになります)。次に、変数Nameを取り出し、文字列変数idに代入します。最後に、FindControl(id)を呼び出して、コントロールオブジェクトへの参照を試みます。返されてきたコントロールがnullでなければ、それをforループで現在の変数(フィールド)に代入します。

 これだけのことです。たいして難しくなかったでしょう?

 これではページにすごいオーバーヘッドがかかるのでは? と考える方もいるでしょう。確かに、オーバーヘッドの増加分は大きいと思います。しかし、私がテストしたところでは(とても威張れるようなテスト規模ではありませんが)、ページのパフォーマンスはかなり優秀でした。事実、私の勤め先の会社がそのパフォーマンスを見て、いま取りかかっているWebアプリケーションにこの方法を使うことに同意してくれました。

 オーバーヘッドについて私が言えるのは、それぞれにベンチマークを行ってくださいということです。結果の数字が満足できるものでなければ、なかなか興味深い記事を読んだという満足感だけで我慢していただいて、ページテンプレートの作成には別の方法をお選びください。

完成コード

 以上で、必要なものはほぼ揃いました。サンプルページを構成する完成したファイルの1つ1つを見ていきましょう。

XMLテンプレートファイル
<?xml version="1.0" encoding="utf-8" ?>
<PageTemplate>
   
    <![CDATA[

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 
    Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

<html>
<head>
    <title>Cooking: it's more fun than you think!</title>
    <link rel="stylesheet" type="text/css" href="./_stylesheets/layout.css" />
</head>

<body>
    <div id="Container">
        <div id="Header">
            Cooking: it's more fun than you think!
        </div>

        <div id="NavBar">
            <b>Navigation:</b>
            <ul>
                <li></li>
                <li>Searing Chicken</li>
                <li>Roasting Pork</li>
                <li>Onion Soup</li>
            </ul>
        </div>

        <div id="PageContent">
            <asp:Label id="lblBaseLabel" runat="server">Hi, I'm the Base Label!</asp:Label>

            <br />
            <asp:PlaceHolder id="phMainContent" runat="server" />
        </div>

        <div id="Footer">
            Copyright (C) Cooking: It's more fun than you think, Inc. 2005. All rights reserved.
        </div>
    </div>
</body>
</html>

    ]]>

</PageTemplate>
CSSファイル(すべてのCSSファイルを「./_stylesheets」というサブディレクトリに入れている点に注意)
body
{
    font-family: verdana; font-size: 11px; color: #676767;
    margin: 0px; padding: 0px; background-color: #cccccc;
}

#Container
{
    position: relative;
    width: 760px;
    margin: auto;
    margin-top: 10px;
    background-color: #003366;
}

#Header
{
    height: 80px; line-height: 80px;
    background-color: #336699;
    font-size: 18px; font-weight: bold; color: #ffffff;
    text-align: center;
    border-bottom: solid 1px #cccccc;
}

#NavBar
{
    position: absolute; top: 80px; left: 0px;
    width: 130px; height: 300px;
    padding: 5px;
    color: #ffffff;
         }

#PageContent
{
    background-color: #ffffff;
    position: relative; top: 0px; left: 140px;
    padding: 10px;
    width: 600px;
    height: 300px;
}

#Footer
{
    padding-left: 10px;
    height: 30px; line-height: 30px;
    background-color: #336699;
    font-weight: bold; color: #ffffff;
    text-align: center;
    border-top: solid 1px #cccccc;
    border-bottom: solid 1px #cccccc;
    clear: both;
}
「BasePage.cs」
using System;
using System.Xml;
using System.Web.UI;
using System.Reflection;
using System.Web.UI.WebControls;

namespace sstchur.web.Pages
{
    public class BasePage : System.Web.UI.Page
    {
        // Protected Variables (UI Components)
        protected Label lblBaseLabel;

        // Private Variables
        private string m_strTemplateFile;

        // Public Properties
        public string TemplateFile
        {
            set { m_strTemplateFile = value; }
        }

        // Constructor
        public BasePage()
        {
            // Initialize the TemplateFile in case one is not specified
            m_strTemplateFile = "~/_templates/standardpage.xml";
        }

        protected override void OnInit(EventArgs e)
        {
            // Load the XHTML Template
            InvokePageTemplate();

            // Initialize any components/variables specific to this class (or its base)
            InitializeComponent();

            // Let the base class do its thing
            base.OnInit (e);
        }

        private void InitializeComponent()
        {
            // Wireup Page_Load Event
            this.Load += new EventHandler(Page_Load);
        }

        private void Page_Load(object sender, EventArgs e)
        {
            // Put any needed Page_Load functionality here
        }

        // Here is where all the tricky stuff happens
        private void InvokePageTemplate()
        {
            Control[] controls = new Control[this.Controls.Count];
            this.Controls.CopyTo(controls, 0);
            this.Controls.Clear();

            XmlDocument doc;
            doc = new XmlDocument();
            doc.Load(Server.MapPath(m_strTemplateFile));

            XmlElement root = doc.DocumentElement;
            XmlNode nodeTemplate = root.SelectSingleNode("//PageTemplate");
            string strTemplate = nodeTemplate.InnerText;

            Control ctrlTemplate = Page.ParseControl(strTemplate);
            this.Controls.Add(ctrlTemplate);
            PlaceHolder phMainContent = (PlaceHolder)ctrlTemplate.FindControl("phMainContent");

            foreach (Control c in controls)
                phMainContent.Controls.Add(c);

            FieldInfo[] fieldInfo;
            Type myType = typeof(sstchur.web.Pages.BasePage);
            fieldInfo = myType.GetFields(BindingFlags.NonPublic | 
                BindingFlags.Instance | BindingFlags.DeclaredOnly);
            for (int i = 0; i < fieldInfo.Length; i++)
            {
                if (!fieldInfo[i].IsPrivate)
                {
                    string id = fieldInfo[i].Name;

                    Control c = ctrlTemplate.FindControl(id);
                    if (c != null)
                        fieldInfo[i].SetValue(this, c);
                }
            }
        }
    }
}

 残るファイルは1つ……いえ、ASPXサンプルファイルと、それに対応する分離コードファイルですから、実際には2つです。これらのファイルから「Example.aspx」を作成するのがいかに簡単か、きっと信じられないでしょう。

「Example.aspx」
<%@ Page language="C#" AutoEventWireup="false" 
    Codebehind="example.aspx.cs" Inherits="sstchur.web.ExamplePage" %>
「Example.aspx.cs」
using System;

namespace sstchur.web
{
    public class ExamplePage : sstchur.web.Pages.BasePage
    {
        private void Page_Load(object sender, System.EventArgs e)
        {
            base.lblBaseLabel.Text = "My value has been overridden by the example.aspx page";
        }
        override protected void OnInit(EventArgs e)
        {
            InitializeComponent();
            base.OnInit(e);
        }

        private void InitializeComponent()
        {
            this.TemplateFile = "~/template.xml";
            this.Load += new System.EventHandler(this.Page_Load);
        }
    }
}

 Visual Studioで新しいWebプロジェクトを作成し、上で作成した各ファイルをそこに追加してください。プロジェクトをコンパイルし、「Example.aspx」ファイルを表示します。

 次のようなページが表示されるはずです。

 もちろん、物は試しといいます。いろいろなコンポーネントをいくつか「Example.aspx」ファイルに追加して、テストしてみるのもいいでしょう。通常のASP.NETページの場合と変わりなく動作するかどうか確かめてみてください。次に例を示します。

多少手を加えた「Example.aspx」
<%@ Page language="C#" AutoEventWireup="false" 
    Codebehind="example.aspx.cs" Inherits="sstchur.web.ExamplePage" %>

<form id="theForm" runat="server">
    <p><asp:TextBox id="txt" runat="server" /></p>
    <p><asp:Button id="btn" Text="Click Me!" runat="server" />
</form>

 <form runat="server">タグを必要とするコンポーネントがベースページのXMLテンプレートファイル内にあるときは、サーバーサイドフォームを、継承する側のページのコードフロントでなく、XMLテンプレートファイルに入れることを覚えておいてください。もちろん、その場合、継承する側のページのコードフロントには<form runat="server">が不要となります。

注意

 注意点を2つ記しておきます。

  1. 使用するコントロールには、1つ残らずIDを与えなければなりません。これはタイミングとリフレクションに関係するようですが、いずれにせよ、コントロールには名前を付ける――これは身につけて損のない習慣です。
  2. 継承する側のページでPage_Loadメソッドが実行されるまでは、ベースページ中のコンポーネントへはアクセスできません。たとえば、InitializeComponent()(またはOnInit())でアクセスしようとするのは、リフレクションという魔法が効き目を表す前にコントロールにアクセスするようなもので、NullReferenceExceptionが返されるだけです。

結論

 本稿を読んで、XMLとリフレクションを使った再利用可能なページテンプレートに興味を持ち、その可能性をいろいろと探ってみようとする方が現れれば、筆者としてそれ以上の喜びはありません。本稿で示した例はきわめて初歩的なものばかりでしたが、いくらでも複雑にできるので安心してください。たとえば、私がいま手がけているWebアプリケーションでもこのページテンプレートのアイデアを取り入れていて、サードパーティのナビゲーションコンポーネントやら、ユーザーコンポーネントやらをページテンプレートに含めています。すべてのページに適用できない特殊なナビゲーションコンポーネントがあって、それを実行時に表示したり隠したりできるメソッドも使っています。

 ぜひ、このアイデアを試してみてください。うまくいけば大幅に時間を節約できますし、最悪でも何かを学べます。

 ハッピープログラミング!



  • ブックマーク
  • LINEで送る
  • このエントリーをはてなブックマークに追加

著者プロフィール

バックナンバー

連載:japan.internet.com翻訳記事

もっと読む

All contents copyright © 2005-2020 Shoeisha Co., Ltd. All rights reserved. ver.1.5