ページング処理を行う汎用的なコントロールの作成はじめにエンドユーザーを混乱させないようにデータを効果的に表示することは、Webのデータプレゼンテーションアプリケーションを開発する際の主たる目的の1つです。1ページに20件のレコードを表示するなら我慢できますが、10,000件にもなると混乱を招きます。この問題の解決策としては、データを複数のページに分割するという方法が一般的に用いられています。これを「データのページング処理」と言います。 ASP.NETにおけるページング処理 ASP.NETには、ページングをサポートする 汎用的なページャコントロールに要求される機能汎用的なページャコントロールの中でも、特に優れたものは、単にデータを複数ページに表示するだけではなく、次のような機能を備えているはずです。
同様の機能を持つ商用のページャもいくつかありますが、それなりに値が張ります。資金が潤沢でないWeb企業としては、カスタムのページャコントロールを作成することが必須です。 カスタムのページャコントロールの作成 ASP.NETでは、独自のWebコントロールを作成する方法として、ユーザーコントロール、複合コントロール、カスタムコントロールという3つが用意されています。3つ目に挙げたカスタムコントロールというのは、少し誤解を招く呼び名です。前述のコントロールはいずれも、実際にはカスタムコントロールです。複合コントロールとカスタムコントロールの違いは、 ページャコントロールのしくみ次のシーケンス図は、ページャコントロールのしくみの全体像を示します。 ![]() ここで作成するページャコントロールは、プレゼンテーションコントロールの種類に依存しないものの、何らかの方法でデータにアクセスしなくてはなりません。 プレゼンテーションコントロールが データソースの把握 イベントハンドラメソッドについて説明する前に、1つ指摘しておきたいのは、イベントリスナとして登録するためには、プレゼンテーションコントロールに対する参照を確立する必要があるということです。ページャコントロールでは、 BindToControlプロパティ
public string BindToControl { get { if (_bindcontrol == null) throw new NullReferenceException("You must bind to a control " + "through the BindToControl property before you use the pager"); return _bindcontrol; } set{_bindcontrol=value;} } このメソッドは非常に重要なので、標準の OnInitイベントハンドラ
protected override void OnInit(EventArgs e) { _boundcontrol = Parent.FindControl(BindToControl); BoundControl.DataBinding += new EventHandler(BoundControl_DataBound); base.OnInit(e); } プレゼンテーションコントロールを見つけ出すには、ページャの プレゼンテーションコントロールの DataBoundイベントハンドラ
private void BoundControl_DataBound(object sender,System.EventArgs e) { Type type = sender.GetType(); _datasource = type.GetProperty("DataSource"); if (_datasource == null) throw new NotSupportedException("The Pager control doesn’t support controls " + "that don’t contain a datasource"); object data = _datasource.GetGetMethod().Invoke(sender,null); BindParent(); } リフレクションを使用して、 データソースの操作方法の把握これでデータソースは把握できましたが、ページャはさらに、その操作方法を把握する必要があります。ページャをプレゼンテーションコントロールから独立させるべく多大な努力を費やしているというのに、ここでデータソースに依存するようなしくみにしたら、柔軟性のあるコントロールを構築するという目的が損なわれてしまいます。プラグ可能なアーキテクチャにすれば、.NET標準のものにせよ、独自のものにせよ、あらゆる種類のデータソースをページャコントロールが確実に処理できるようになります。 使用するデザインパターン堅牢性と拡張性に優れたプラグ可能なアーキテクチャを実現するための最善のソリューションは、GoFのBuilderパターンを使用することです。 ![]() IDataSourceAdapterインターフェイス public interface IDataSourceAdapter { int TotalCount{get;} object GetPagedData(int start,int end); } DataViewAdapter internal class DataViewAdapter:IDataSourceAdapter { private DataView _view; internal DataViewAdapter(DataView view) { _view = view; } public int TotalCount { get{return (_view == null) ? 0 : _view.Table.Rows.Count;} } public object GetPagedData(int start, int end) { DataTable table = _view.Table.Clone(); for (int i = start;i<=end && i<= TotalCount;i++) { table.ImportRow(_view[i-1].Row); } return table; } } AdapterBuilder抽象クラスpublic abstract class AdapterBuilder { private object _source; private void CheckForNull() { if (_source == null) throw new NullReferenceException("You must provide a valid source"); } public virtualobject Source { get { CheckForNull(); return _source;} set { _source = value; CheckForNull(); } } public abstract IDataSourceAdapter Adapter{get;} } public class DataTableAdapterBuilder:AdapterBuilder { private DataViewAdapter _adapter; private DataViewAdapter ViewAdapter { get { if (_adapter == null) { DataTable table = (DataTable)Source; _adapter = new DataViewAdapter(table.DefaultView); } return _adapter; } } public override IDataSourceAdapter Adapter { get{return ViewAdapter;} } } public class DataViewAdapterBuilder:AdapterBuilder { private DataViewAdapter _adapter; private DataViewAdapter ViewAdapter { get { //lazy instantiate if (_adapter == null) { _adapter = new DataViewAdapter((DataView)Source); } return _adapter; } } public override IDataSourceAdapter Adapter { get{return ViewAdapter;} } } AdapterCollectionページャコントロールでは、適切なビルダーの検索は、タイプセーフなコレクションによって処理します。 public class AdapterCollection:DictionaryBase { private string GetKey(Type key) { return key.FullName; } public AdapterCollection() {} public void Add(Type key,AdapterBuilder value) { Dictionary.Add(GetKey(key),value); } public bool Contains(Type key) { return Dictionary.Contains(GetKey(key)); } public void Remove(Type key) { Dictionary.Remove(GetKey(key)); } public AdapterBuilder this[Type key] { get{return (AdapterBuilder)Dictionary[GetKey(key)];} set{Dictionary[GetKey(key)]=value;} } } public AdapterCollection Adapters { get{return _adapters;} } private bool HasParentControlCalledDataBinding { get{return _builder != null;} } private void BoundControl_DataBound(object sender,System.EventArgs e) { if (HasParentControlCalledDataBinding) return; Type type = sender.GetType(); _datasource = type.GetProperty("DataSource"); if (_datasource == null) throw new NotSupportedException("The Pager control doesn’t support controls " + "that don’t contain a datasource"); object data = _datasource.GetGetMethod().Invoke(sender,null); _builder = Adapters[data.GetType()]; if (_builder == null) ’’throw new NullReferenceException("There is no adapter installed " + "to handle a datasource of type "+data.GetType());’’ _builder.Source = data; BindParent(); } Pagerメソッド
public Pager() { _adapters = new AdapterCollection(); _adapters.Add(typeof(DataTable),new DataTableAdapterBuilder()); _adapters.Add(typeof(DataView),new DataViewAdapterBuilder()); } 最後に実装するメソッドは BindParentメソッド
private void BindParent() { _datasource.GetSetMethod().Invoke(BoundControl, new object[] { _builder.Adapter.GetPagedData( StartRow,ResultsToShow*CurrentPage)}); } このメソッドはかなり単純です。実際のデータ操作は ページャコントロールのプレゼンテーション 前述のとおり、プレゼンテーションとロジックをきっちりと分離するためには、テンプレートを使用する方法が最適です。もっと具体的に言えば、 4つのナビゲーションボタン さて、ページャコントロールに話を戻しましょう。ページャコントロールには、[先頭]、[前へ]、[次へ]、[最後]の各ボタンと、個々のページャを表すボタンがあります。4つのナビゲーションボタンは、 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ページあたりに表示するレコードやページャの数に依存するからです。ページャは とりあえず、ページャコントロールは、ユーザーがページャのルック&フィールをカスタマイズできるようにするためのテンプレートを提供する必要があります。 [TemplateContainer(typeof(LayoutContainer))] public ITemplate Layout { get{return (_layout;} set{_layout =value;} } public class LayoutContainer:Control,INamingContainer { public LayoutContainer() {this.ID = "Page";} } 次のUML図は、ページャコントロールのプレゼンテーションを定めたものです。 ![]() テンプレート作成の第1歩は、aspxページで簡単なレイアウトを定義することです。 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> この例では、テーブル等のフォーマットはレイアウトに含めていません。しかし、含めることは可能ですし、実際含めるのがよいでしょう。詳しくは後ほど解説します。 InstatiateTemplateメソッド
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); } ページコントロールの 別の方法としては、各ボタンを 4つのボタンを検索できたら、適切なイベントハンドラをバインドします。 OnInitイベント
protected override void OnInit(EventArgs e) { _boundcontrol = Parent.FindControl(BindToControl); BoundControl.DataBinding += new EventHandler(BoundControl_DataBound); InstantiateTemplate(); Controls.Add(_container); base.OnInit(e); } DefaultPagerLayout
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); } } 既定のテンプレート
[TemplateContainer(typeof(LayoutContainer))] public ITemplate Layout { get{return (_layout == null)? new DefaultPagerLayout():_layout;} set{_layout =value;} } ページャの生成個々のページャの生成に戻ります。ページャコントロールではまず、いくつかの有用なプロパティを定める必要があります。生成する個々のページャの数をコントロールに伝えるためのプロパティです。 CurrentPageプロパティ
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;} } PagerSequenceプロパティ
private int PagerSequence { get { return Convert.ToInt32(Math.Ceiling((double)CurrentPage / (double)PagersToShow)); } } NumberOfPagersToGenerateプロパティ
private int NumberOfPagersToGenerate { get{return PagerSequence*PagersToShow;} } TotalPagesToShowメソッド
private int TotalPagesToShow { get{return Convert.ToInt32(Math.Ceiling((double)TotalResults/ (double)ResultsToShow)); } } public int TotalResults { get{return _builder.Adapter.TotalCount;} } ページャの見た目のカスタマイズASP.NETにより定義された既定のスタイルは、ページャコントロールを利用するユーザーにとってあまり役立たないものである場合もあります。ユーザーは、カスタムのスタイルを定義することで、ページャの見た目をカスタマイズできます。 UnSelectedPagerStyleプロパティ
public Style UnSelectedPagerStyle {get {return UnselectedPager;}} public Style SelectedPagerStyle {get {return SelectedPager;}} ページャの生成とイベントハンドラのバインドGeneratePagersメソッド
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); } イベントハンドラの定義イベントハンドラの定義は次のとおりです。 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(); } 各イベントハンドラでは、ページャコントロールの現在のページを設定してから、バインドコントロールを更新しています。 Updateメソッド
private void Update() { if (!HasParentControlCalledDataBinding) return; ApplyDataSensitivityRules(); BindParent(); BoundControl.DataBind(); } ページャコントロールは、まず データ規則の適用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(); } イベントの作成最後の仕上げとして、イベントを用意します。ページャのさまざまなイベントに対して、ユーザーが必要な調整を加えられるようにするためです。 PageChangedEventArgsクラス
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;}} } ページャコントロールはカスタムのイベント引数を返す必要があるため、 イベントの定義
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); } } そして、イベントハンドラでは、 イベントの発生と通知
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); } ここでは、説明を簡略化するために、他のイベントハンドラは示しませんが、 ページャのRepeaterコントロールへのバインド
<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ページは、ページャを ページャコントロールの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コントロールを、汎用的な多目的コントロールへと変えることができ、桁違いの生産性を実現できます。ここで紹介したページャコントロールは、現在および今後のプレゼンテーションのニーズに依存しないコントロールを作成する方法の単なる一例にすぎません。 参考資料
著者紹介Tomasz Kaszuba(Tomasz Kaszuba)
ポーランド最大のオンラインバンクInteligoの上級Web開発者。Javaおよび.NETでの開発経験は5年以上に及び、ポータル、CMS、分散システム、バンキングシステム、CRM、電気通信アプリケーションなど、さまざまなプロジェクトに従事。空き時間には、NetAr(http://sourceforge.net/projects/netarcomponents/)ポータルコンポーネントなど、オープンソースソフトウェアの開発にも携わる。メールアドレスはtomasz.kaszuba@inteligo.pl。
関連記事 最新トップニュース
|
|