はじめに
コードの再利用は、オブジェクト指向プログラマの目指すべき大目標の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つのステップから成ります。
- 継承する側のページからすべてのコントロールを取り出し(取り除き)、それを一時コントロール配列に保管します。
- XMLテンプレートファイル内のHTMLを文字列変数に読み込みます。
- HTMLを格納している文字列変数を解析して、一連のASP.NETリテラルコントロール、
HtmlControls
、WebControls
、サードパーティコントロール(あれば)を識別し、新しく解析されたコントロールをページのControlCollection
に追加します。 - 手順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.Xml
とusing 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リテラルコントロール、HtmlControls
、WebControls
、サードパーティコントロール(あれば)を識別し、新しく解析されたコントロールをページの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 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>
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; }
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」を作成するのがいかに簡単か、きっと信じられないでしょう。
<%@ Page language="C#" AutoEventWireup="false"
Codebehind="example.aspx.cs" Inherits="sstchur.web.ExamplePage" %>
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ページの場合と変わりなく動作するかどうか確かめてみてください。次に例を示します。
<%@ 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つ残らずIDを与えなければなりません。これはタイミングとリフレクションに関係するようですが、いずれにせよ、コントロールには名前を付ける――これは身につけて損のない習慣です。
- 継承する側のページで
Page_Load
メソッドが実行されるまでは、ベースページ中のコンポーネントへはアクセスできません。たとえば、InitializeComponent()
(またはOnInit()
)でアクセスしようとするのは、リフレクションという魔法が効き目を表す前にコントロールにアクセスするようなもので、NullReferenceException
が返されるだけです。
結論
本稿を読んで、XMLとリフレクションを使った再利用可能なページテンプレートに興味を持ち、その可能性をいろいろと探ってみようとする方が現れれば、筆者としてそれ以上の喜びはありません。本稿で示した例はきわめて初歩的なものばかりでしたが、いくらでも複雑にできるので安心してください。たとえば、私がいま手がけているWebアプリケーションでもこのページテンプレートのアイデアを取り入れていて、サードパーティのナビゲーションコンポーネントやら、ユーザーコンポーネントやらをページテンプレートに含めています。すべてのページに適用できない特殊なナビゲーションコンポーネントがあって、それを実行時に表示したり隠したりできるメソッドも使っています。
ぜひ、このアイデアを試してみてください。うまくいけば大幅に時間を節約できますし、最悪でも何かを学べます。
ハッピープログラミング!