ページャコントロールのプレゼンテーション
前述のとおり、プレゼンテーションとロジックをきっちりと分離するためには、テンプレートを使用する方法が最適です。もっと具体的に言えば、Itemplateインターフェイスを使用するということです。実際、Microsoftはテンプレートの威力を認識していて、ありとあらゆる所で使用しており、ページパーサー自体でも使用しています。あいにく、テンプレートはすごく簡単なしくみというわけではなく、習得には多少の時間がかかりますが、それを少しでも楽にするためのチュートリアルはたくさんあります。
4つのナビゲーションボタン
さて、ページャコントロールに話を戻しましょう。ページャコントロールには、[先頭]、[前へ]、[次へ]、[最後]の各ボタンと、個々のページャを表すボタンがあります。4つのナビゲーションボタンは、ImageButtonクラスではなくLinkButtonクラスから選択しました。プロフェッショナルなWebデザインの観点から言うと、単なるリンクよりはイメージの方が往々にして扱いやすいものです。
public ImageButton FirstButton{get {return First;}} public ImageButton LastButton{get {return Last;}} public ImageButton PreviousButton{get {return Previous;}} public ImageButton NextButton{get {return Next;}}
ページャのルック&フィールをカスタマイズするためのテンプレート
個々のページャは動的に作成します。データソースや、1ページあたりに表示するレコードやページャの数に依存するからです。ページャはPanelに追加します。これによりWebデザイナは、ページャをどこに表示するかを指定できます。ページャの作成については後で触れます。
とりあえず、ページャコントロールは、ユーザーがページャのルック&フィールをカスタマイズできるようにするためのテンプレートを提供する必要があります。
[TemplateContainer(typeof(LayoutContainer))] public ITemplate Layout { get{return (_layout;} set{_layout =value;} } public class LayoutContainer:Control,INamingContainer { public LayoutContainer() {this.ID = "Page";} }
LayoutContainerクラスはテンプレートのホルダです。テンプレートコンテナには必ずカスタムIDを追加するのがよいでしょう。イベントに伴う問題や、ページによってどのように呼び出されるかの問題を回避できるからです。
次のUML図は、ページャコントロールのプレゼンテーションを定めたものです。

テンプレート作成の第1歩は、aspxページで簡単なレイアウトを定義することです。
<Layout> <asp:ImageButton id="First" Runat="server" AlternateText="first"/> <asp:ImageButton id="Previous" Runat="server" AlternateText="previous"/> <asp:ImageButton id="Next" Runat="server" AlternateText="next"/> <asp:ImageButton id="Last" Runat="server" AlternateText="last"/> <asp:PanelID="Pager" Runat="server"/> </Layout>
この例では、テーブル等のフォーマットはレイアウトに含めていません。しかし、含めることは可能ですし、実際含めるのがよいでしょう。詳しくは後ほど解説します。
Itemplateインターフェイスが持つメソッドはInstantiateInだけです。テンプレートを解析し、ホルダに関連付けます。
private void InstantiateTemplate() { _container = new LayoutContainer(); Layout.InstantiateIn(_container); First = (ImageButton)_container.FindControl("First"); Previous = (ImageButton)_container.FindControl("Previous"); Next = (ImageButton)_container.FindControl("Next"); Last = (ImageButton)_container.FindControl("Last"); Holder = (Panel)_container.FindControl("Pager"); this.First.Click += new System.Web.UI.ImageClickEventHandler(this.First_Click); this.Last.Click += new System.Web.UI.ImageClickEventHandler(this.Last_Click); this.Next.Click += new System.Web.UI.ImageClickEventHandler(this.Next_Click); this.Previous.Click += new System.Web.UI.ImageClickEventHandler(this.Previous_Click); }
ページコントロールのInstatiateTemplateメソッドで最初に実行しているのは、テンプレートのインスタンス化です。Layout.InstantiateIn(_container)という行です。コンテナは、コントロールの1つであることには代わりなく、使い方も他のコントロールと同様です。その点に基づいて、InstantiateTemplateメソッドでは、4つのナビゲーションボタンと、個々のページャを保持するのに必要なパネルを検索します。ボタンの検索にはIDを使用しています。これは、ページャコントロールに課せられているちょっとした制限事項です。ナビゲーションボタンには、"First"、"Previous"、"Next"、"Last"、"Pager"という各IDがあらかじめ定義されていることが必須であり、さもないとボタンを検索できないのです。あいにく、ここで使用しているプレゼンテーション構造では、この方法をとるしかありません。
別の方法としては、各ボタンをImageButtonクラスから継承することで、新しい型を定義するというものもあります。各ボタンがそれぞれ別の型になるので、コンテナに対する再帰的検索で特定の型を見つけるように実装でき、ボタンに適切な名前を付ける必要がなくなります。しかし、ドキュメントに適切に記載されていれば、そうしたちょっとした要件は特に問題とはならないはずです。
4つのボタンを検索できたら、適切なイベントハンドラをバインドします。InstantiateTemplateをいつ呼び出すかについては、きわめて重要な判断を下す必要があります。通常は、こうしたメソッドは、CreateChildControlsメソッドで呼び出します。CreateChildControlsメソッドは、その名のとおり、子コントロールを作成するのが基本だからです。しかし、ページャコントロールの場合は、子コントロールを変更することはないので、CreateChildControlsメソッドに用意されている、一部のイベントに基づいて描画状態を変更するという機能は必要ありません。また、子コントロールの描画はできるだけ速くすることが望まれます。したがって、InstantiateTemplateメソッドはOnInitイベントで呼び出すのが適しています。
protected override void OnInit(EventArgs e) { _boundcontrol = Parent.FindControl(BindToControl); BoundControl.DataBinding += new EventHandler(BoundControl_DataBound); InstantiateTemplate(); Controls.Add(_container); base.OnInit(e); }
OnInitメソッドでは、非常に重要な処理をもう1つ実行しています。ページャコントロールにコンテナを追加する処理です。コンテナをページャのコントロールコレクションに追加しないことには、テンプレートは表示されません。Renderメソッドが呼び出されないからです。テンプレートは、Itemplateインターフェイスを実装することによって、プログラムで定義することも可能です。この機能は、柔軟なコントロールを実現することの一環として、ユーザーがaspxページでテンプレートを指定しなかった場合に既定のテンプレートを提供するという目的で使用できます。
public class DefaultPagerLayout:ITemplate { private ImageButton Next; private ImageButton First; private ImageButton Last; private ImageButton Previous; private Panel Pager; public DefaultPagerLayout() { Next = new ImageButton(); First = new ImageButton(); Last = new ImageButton(); Previous = new ImageButton(); Pager = new Panel(); Next.ID="Next"; Next.AlternateText="Next"; First.ID="First"; First.AlternateText="First"; Last.ID = "Last"; Last.AlternateText ="Last"; Previous.ID="Previous"; Previous.AlternateText="Previous"; Pager.ID="Pager"; } public void InstantiateIn(Control control) { control.Controls.Clear(); Table table = new Table(); table.BorderWidth = Unit.Pixel(0); table.CellSpacing= 1; table.CellPadding =0; TableRow row = new TableRow(); row.VerticalAlign = VerticalAlign.Top; table.Rows.Add(row); TableCell cell = new TableCell(); cell.HorizontalAlign = HorizontalAlign.Right; cell.VerticalAlign = VerticalAlign.Middle; cell.Controls.Add(First); cell.Controls.Add(Previous); row.Cells.Add(cell); cell = new TableCell(); cell.HorizontalAlign= HorizontalAlign.Center; cell.Controls.Add(Pager); row.Cells.Add(cell); cell = new TableCell(); cell.VerticalAlign = VerticalAlign.Middle; cell.Controls.Add(Next); cell.Controls.Add(Last); row.Cells.Add(cell); control.Controls.Add(table); } }
DefaultPagerLayoutでは、aspxページで追加していたすべてのナビゲーション要素をプログラムで実装していますが、今回は通常のHTMLテーブルで要素をフォーマットしています。これで、もしユーザーがプレゼンテーションテンプレートの実装を忘れた場合には、既定のテンプレートが代わりに適用されます。
[TemplateContainer(typeof(LayoutContainer))] public ITemplate Layout { get{return (_layout == null)? new DefaultPagerLayout():_layout;} set{_layout =value;} }
ページャの生成
個々のページャの生成に戻ります。ページャコントロールではまず、いくつかの有用なプロパティを定める必要があります。生成する個々のページャの数をコントロールに伝えるためのプロパティです。
public int CurrentPage { get { string cur = (string)ViewState["CurrentPage"]; return (cur == string.Empty || cur ==null)? 1 : int.Parse(cur); } set { ViewState["CurrentPage"] = value.ToString();} } } public int PagersToShow { get{return _results;} set{_results = value;} } public int ResultsToShow { get{return _resultsperpage;} set{_resultsperpage = value;} }
CurrentPageプロパティが保持するのは、その名のとおり、ページャのViewStateにある現在のページです。一方、PagersToShowメソッドとResultsToShowメソッドは、表示するページャの数と1ページあたりに表示する結果の数をユーザーが定義できるようにするためのプロパティを定義します。既定値は10に設定されています。
private int PagerSequence { get { return Convert.ToInt32(Math.Ceiling((double)CurrentPage / (double)PagersToShow)); } }
private int NumberOfPagersToGenerate { get{return PagerSequence*PagersToShow;} }
NumberofPagersToGenerateは、現在生成すべきページャの数を返します。
private int TotalPagesToShow { get{return Convert.ToInt32(Math.Ceiling((double)TotalResults/ (double)ResultsToShow)); } } public int TotalResults { get{return _builder.Adapter.TotalCount;} }
TotalPagesToShowメソッドは、ユーザーが定義済みのResultsToShowプロパティにより調整された、表示すべきページの総数を返します。
ページャの見た目のカスタマイズ
ASP.NETにより定義された既定のスタイルは、ページャコントロールを利用するユーザーにとってあまり役立たないものである場合もあります。ユーザーは、カスタムのスタイルを定義することで、ページャの見た目をカスタマイズできます。
public Style UnSelectedPagerStyle {get {return UnselectedPager;}} public Style SelectedPagerStyle {get {return SelectedPager;}}
UnSelectedPagerStyleは、選択されていない個々のページャで使用されるスタイルを表します。SelectedPagerStyleは、選択されている個々のページャで使用されるスタイルを表します。
ページャの生成とイベントハンドラのバインド
private void GeneratePagers(WebControl control) { control.Controls.Clear(); int pager = (PagerSequence-1)* PagersToShow +1; for (;pager<=NumberOfPagersToGenerate && pager<=TotalPagesToShow;pager++) { LinkButton link = new LinkButton(); link.Text = pager.ToString(); link.ID = pager.ToString(); link.Click += new EventHandler(this.Pager_Click); if (link.ID.Equals(CurrentPage.ToString())) link.MergeStyle(SelectedPagerStyle); else link.MergeStyle(UnSelectedPagerStyle); control.Controls.Add(link); control.Controls.Add(new LiteralControl(" ")); } } private void GeneratePagers() { GeneratePagers(Holder); }
GeneratePagersメソッドでは、必要な個々のページャすべてを、LinkButton型のボタンとして動的に作成します。個々のページャのテキストとIDプロパティには、ループの制御で使用している現在のページャ番号を割り当てています。また、クリックイベントに適切なイベントハンドラをバインドし、適切なスタイルを設定しています。最後に、保持用の適切なコントロールにページャを追加しています。この例ではPanelオブジェクトです。ボタンIDは、クリックイベントを発生させたボタンを識別するために使用されます。
イベントハンドラの定義
イベントハンドラの定義は次のとおりです。
private void Pager_Click(object sender, System.EventArgs e) { LinkButton button = (LinkButton) sender; CurrentPage = int.Parse(button.ID); Update(); } private void Next_Click(object sender, System.Web.UI.ImageClickEventArgs e) { if (CurrentPage<TotalPagesToShow) CurrentPage++; Update(); } private void Previous_Click(object sender, System.Web.UI.ImageClickEventArgs e) { if (CurrentPage > 1) CurrentPage--; Update(); } private void First_Click(object sender, System.Web.UI.ImageClickEventArgs e) { CurrentPage = 1; Update(); } private void Last_Click(object sender, System.Web.UI.ImageClickEventArgs e) { CurrentPage = TotalPagesToShow; Update(); }
各イベントハンドラでは、ページャコントロールの現在のページを設定してから、バインドコントロールを更新しています。
private void Update() { if (!HasParentControlCalledDataBinding) return; ApplyDataSensitivityRules(); BindParent(); BoundControl.DataBind(); }
ページャコントロールは、まずHasParentControlCalledDataBindingメソッドを呼び出します。これにより、適切なアダプタを初期化するために必要な手順が実行されているかどうかをチェックします。実行されている場合は、仕様の中で「データに応じた処理を行う」という規則として定めた処理を適用します。このデータ規則により、ページャコントロールは、BoundControlのデータに応じて異なる動作を行います。ページャコントロールはこのデータ規則を内部で制御しますが、GoFのStateパターンを使用すれば、外部へ移すことも簡単です。
データ規則の適用
public bool IsDataSensitive { get{return _isdatasensitive;} set{_isdatasensitive = value;} } private bool IsPagerVisible { get{return (TotalPagesToShow != 1) && IsDataSensitive;} } private bool IsPreviousVisible { get { return (!IsDataSensitive)? true: (CurrentPage != 1); } } private bool IsNextVisible { get { return (!IsDataSensitive)? true: (CurrentPage != TotalPagesToShow); } } private void ApplyDataSensitivityRules() { FirstButton.Visible = IsPreviousVisible; PreviousButton.Visible = IsPreviousVisible; LastButton.Visible = IsNextVisible; NextButton.Visible = IsNextVisible; if (IsPagerVisible) GeneratePagers(); }
ApplyDataSensitivityRulesでは、IsPagerVisible、IsPreviousVisible、IsNextVisibleなど、定義済みのデータ規則を適用しています。既定では、データ規則の適用はオンになっています。ユーザーは、IsDataSensitiveプロパティを設定すれば、これをオフにできます。ページャコントロールのプレゼンテーションは以上で完了です。
イベントの作成
最後の仕上げとして、イベントを用意します。ページャのさまざまなイベントに対して、ユーザーが必要な調整を加えられるようにするためです。
public delegate void PageDelegate(object sender, PageChangedEventArgs e); public enum PagedEventInvoker{Next,Previous,First,Last,Pager} public class PageChangedEventArgs:EventArgs { private int newpage; private Enum invoker; public PageChangedEventArgs(int newpage):base() { this.newpage = newpage; } public PageChangedEventArgs(int newpage,PagedEventInvoker invoker) { this.newpage = newpage; this.invoker = invoker; } public int NewPage {get{return newpage;}} public Enum EventInvoker{get{return invoker;}} }
ページャコントロールはカスタムのイベント引数を返す必要があるため、PageChangedEventArgsという専用のクラスを作成しています。PageChangedEventArgsクラスは、PagedEventInvoker型(イベントを発生し得るコントロールの単純な列挙体)と、新しいページ番号を返します。カスタムのイベント引数を処理するために、新しいデリゲートPageDelegateを定義しています。各イベントは次のように定義されています。
public event PageDelegate PageChanged; public event EventHandler DataUpdate;
イベントにリスナが割り当てられていない場合、ASP.NETは、その動作に対処するために、例外を発生させます。ページャコントロールでは、次のようなRaiseEventメソッドを定義しています。
private void RaiseEvent(EventHandler e,object sender) { this.RaiseEvent(e,this,null); } private void RaiseEvent(EventHandler e,object sender, PageChangedEventArgs args) { if(e!=null) { e(sender,args); } } private void RaiseEvent(PageDelegate e,object sender) { this.RaiseEvent(e,this,null); } private void RaiseEvent(PageDelegate e,object sender, PageChangedEventArgs args) { if(e!=null) { e(sender,args); } }
そして、イベントハンドラでは、RaiseEventメソッドを呼び出すことにより、イベントを発生(通知)させることができます。
private void Pager_Click(object sender, System.EventArgs e) { LinkButton button = (LinkButton) sender; CurrentPage = int.Parse(button.ID); RaiseEvent(PageChanged, this, new PageChangedEventArgs(CurrentPage,PagedEventInvoker.Pager)); Update(); } private void Update() { if (!HasParentControlCalledDataBinding) return; ApplyDataSensitivityRules(); BindParent(); _boundcontrol.DataBind(); RaiseEvent(DataUpdate,this); }
ここでは、説明を簡略化するために、他のイベントハンドラは示しませんが、PageChangedイベントを同様に発生させています。これで、ページャコントロールを使えるようになりました。ページャコントロールを使用するためにWebデザイナが行う必要があるのは、プレゼンテーションコントロールにバインドすることだけです。
<asp:Repeater ID="repeater" Runat="server"> <ItemTemplate> Column 1: <%# Convert.ToString(DataBinder.Eval ( Container.DataItem,"Column1"))%><br> Column 2: <%# Convert.ToString(DataBinder.Eval ( Container.DataItem,"Column2"))%><br> Column 3: <%# Convert.ToString(DataBinder.Eval ( Container.DataItem,"Column3"))%><br> <hr> </ItemTemplate> </asp:Repeater> <cc1:Pager id="pager" ResultsToShow="2" runat="server" BindToControl="repeater"> <SelectedPagerStyleBackColor="Yellow"/> </cc1:Pager>
上記のaspxページは、ページャをRepeaterコントロールにバインドし、1ページあたりに表示する結果の数を2に設定し、選択されたページャの色を黄色に設定しています。レイアウトは既定のものを使用しています。次のテストアプリケーションでは、ページャコントロールをDataGridにバインドしています。
<asp:DataGridID="Grid"Runat="server"/> <cc1:Pager id="PagerGrid" ResultsToShow="2" runat="server" BindToControl="Grid"> <SelectedPagerStyleBackColor="Red"/> <Layout> <asp:ImageButton id="First" Runat="server" AlternateText="first"/> <asp:ImageButton id="Previous "Runat="server" AlternateText="previous"/> <asp:ImageButton id="Next" Runat="server" AlternateText="next"/> <asp:ImageButton id="Last" Runat="server" AlternateText="last"/> <asp:PanelID="Pager" Runat="server"/> </Layout> </cc1:Pager>
プラグ可能なアーキテクチャをテストする分離コードです。
protected Pager pager; protected Repeater repeater; protected Pager PagerGrid; protected DataGrid Grid; private void Page_Load(object sender, System.EventArgs e) { pager.Adapters.Remove(typeof(DataTable)); pager.Adapters.Add(typeof(DataTable),new DataTableAdapterBuilder()); DataTable table = GenerateDataTable(); repeater.DataSource = table; repeater.DataBind(); Grid.DataSource = table; Grid.DataBind(); } private DataTable GenerateDataTable() { DataTable table = new DataTable(); table.Columns.Add("Column1"); table.Columns.Add("Column2"); table.Columns.Add("Column3"); for (int i=0;i<20;i++) { DataRow row = table.NewRow(); row[0] = "Row"+(i+1); table.Rows.Add(row); } return table; }
まとめ
このテストアプリケーションからは、このページャコントロールがプレゼンテーションコントロールに依存していないこと、さまざまなデータソースを容易に処理できること、使いやすさが非常に優れていることがわかります。ページャの機能を他のテストで試すには、すべてのテストおよび関連するソースコード一式が含まれたzipファイルをダウンロードしてください。
カスタムのWebコントロールの開発を習得するには、かなりの手間暇がかかる可能性もあるものの、それだけの時間をかけて身に付けることには、計り知れないメリットがあります。再利用可能なコンポーネントを使用すれば、開発者は、わずかな労力で、通常のWebコントロールを、汎用的な多目的コントロールへと変えることができ、桁違いの生産性を実現できます。ここで紹介したページャコントロールは、現在および今後のプレゼンテーションのニーズに依存しないコントロールを作成する方法の単なる一例にすぎません。
参考資料
- Thinking in C# by Bruce Eckel
- Design Patterns By Erich Gamma et al, Addisson Wesley
- Developing Microsoft ASP.NET Server Controls and Components by Nikhil Kothari, Vandana Datye
