japan.internet.comThe Internet & IT Network
RSS
  • ニュース
  • コラム
  • リサーチ
  • ヘッドライン
  • 特集
  • ブログ
  • プレスリリース
  • 専門チャンネル
  • イベント
  • ランキング
  • ニュースメール
2008年10月7日
文字サイズ文字サイズ小文字サイズ中文字サイズ大
デベロッパー2005年9月27日 10:00

ページング処理を行う汎用的なコントロールの作成

海外海外internet.com発の記事
  • このエントリーを含むはてなブックマーク
  • この記事をクリップ!
  • Buzzurlにブックマーク
  • Yahoo!ブックマークに登録
  • newsing it!

はじめに

 エンドユーザーを混乱させないようにデータを効果的に表示することは、Webのデータプレゼンテーションアプリケーションを開発する際の主たる目的の1つです。1ページに20件のレコードを表示するなら我慢できますが、10,000件にもなると混乱を招きます。この問題の解決策としては、データを複数のページに分割するという方法が一般的に用いられています。これを「データのページング処理」と言います。

ASP.NETにおけるページング処理

 ASP.NETには、ページングをサポートするDataGridというコントロールが用意されています。これはページングをサポートする唯一のコントロールです。ただ、イントラネット向けのアプリケーションの場合はDataGridページャコントロールで問題ないのですが、一般向けのアプリケーションの場合には、柔軟なWebアプリケーションを開発するために必要な機能が不足しています。具体的に言うと、DataGridコントロールでは、Webデザイナがページャを配置できる位置やその見た目が制限されており、たとえばページャを縦に配置することはできません。

 DataGrid以外のコントロールで、ページングが効果を発揮しそうなものとしては、リピータコントロールがあります。ただ、リピータコントロールでは、Web開発者がデータの表示方法をすばやく設定できるものの、ページング処理は開発者が自ら実装しなくてはなりません。また、データソースやプレゼンテーションに応じて異なるさまざまなコントロールに対して、専用のカスタムのページャをそれぞれ実装するというのは、時間がかかってしまうおそれがあります。特定のプレゼンテーションコントロール専用ではない、汎用的なページャコントロールがあれば、時間を大きく節約できます。

汎用的なページャコントロールに要求される機能

 汎用的なページャコントロールの中でも、特に優れたものは、単にデータを複数ページに表示するだけではなく、次のような機能を備えているはずです。

  1. [先頭]、[前へ]、[次へ]、[最後]の各ボタン、およびページ移動用のボタンが用意されている。
  2. データに応じた処理が行われる。たとえば、10件のレコードを表示するようにページャが設定されていて、表示データが9件しかない場合は、ページャは表示されないのが望ましい。先頭ページでは、[前へ]ボタンと[先頭]ボタンは表示されないのが望ましい。末尾ページでは、[次へ]ボタンと[最後]ボタンは表示されないのが望ましい。
  3. プレゼンテーションを担当するコントロールの種類に依存しない。
  4. 既存および今後のさまざまなデータソースに対応できる。
  5. プレゼンテーションの設定が簡単で、カスタムアプリケーションに統合できる。
  6. ページング処理の実行時には他のコントロールに通知する。
  7. 経験の浅いWebデザイナでも簡単に使える。
  8. 関連するページングデータに対するプロパティを提供する。

 同様の機能を持つ商用のページャもいくつかありますが、それなりに値が張ります。資金が潤沢でないWeb企業としては、カスタムのページャコントロールを作成することが必須です。

カスタムのページャコントロールの作成

 ASP.NETでは、独自のWebコントロールを作成する方法として、ユーザーコントロール、複合コントロール、カスタムコントロールという3つが用意されています。3つ目に挙げたカスタムコントロールというのは、少し誤解を招く呼び名です。前述のコントロールはいずれも、実際にはカスタムコントロールです。複合コントロールとカスタムコントロールの違いは、CreateChildControls()メソッドを使用するかどうかという点です。このメソッドを使用すると、発生したイベントに基づいて、コントロール自身で再描画を行うことができます。この記事では、複合コントロールモデルを使用して汎用的なページャを作成することにします。

ページャコントロールのしくみ

 次のシーケンス図は、ページャコントロールのしくみの全体像を示します。

 ここで作成するページャコントロールは、プレゼンテーションコントロールの種類に依存しないものの、何らかの方法でデータにアクセスしなくてはなりません。Controlクラスから派生した各クラスには、DataBindingイベントが備わっています。ページャは、自らをDataBindingイベントのリスナとして登録することにより、イベントを待機して、データに変更を加えることができます。このイベントは、Controlクラスから派生したすべてのコントロールに備わっているので、これを使用すれば、プレゼンテーションコントロールの種類に依存しないページャコントロールを作成するという目標を達成できます。言い換えると、Controlクラスから派生したコントロール、つまりほぼすべてのWebコントロールにバインドできるということです。

 プレゼンテーションコントロールがDataBindingイベントを発生させると、ページャコントロールはDataSourceプロパティをインターセプトできます。あいにく、すべてのデータバインドクラスが実装しているインターフェイス(たとえばIdataSourceProviderのようなもの)はありません。また、ControlクラスやWebControlクラスから派生したコントロールの中にはDataSourceプロパティを持たないものもあるので、Controlクラスへのアップキャストという方法は使えません。代案としては、リフレクションを使用してDataSourceプロパティを直接操作するというのが唯一の方法です。

データソースの把握

 イベントハンドラメソッドについて説明する前に、1つ指摘しておきたいのは、イベントリスナとして登録するためには、プレゼンテーションコントロールに対する参照を確立する必要があるということです。ページャコントロールでは、BindToControlという簡単な文字列プロパティを公開します。Web開発者は、コードかaspxページからこのプロパティを設定することで、DataSourceをプレゼンテーションコントロールにバインドできます。

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;}
}

 このメソッドは非常に重要なので、標準のNullReferenceExceptionではなく、もっと意味のわかりやすいメッセージをスローするのがよいでしょう。ページャのOnInitイベントハンドラでは、プレゼンテーションコントロールへの参照を解決するための呼び出しを行います。JITコンパイルされたaspxページが必ずBindToControlメソッドを設定するようにするために、コンストラクタではなくOnInitイベントハンドラを使用する必要があります。

OnInitイベントハンドラ
protected override void OnInit(EventArgs e)
{
      _boundcontrol = Parent.FindControl(BindToControl);
      BoundControl.DataBinding += new EventHandler(BoundControl_DataBound);
      base.OnInit(e);
}

 プレゼンテーションコントロールを見つけ出すには、ページャのParentコントロールを検索します。今回の例では、メインのページテンプレートがParentコントロールに当たります。Parentプロパティをこのように使用することには、それなりの危険があります。たとえば、ページャを別のコントロール(たとえばTableコントロール)に埋め込む場合は、Parentプロパティを呼び出すとTableコントロールへの参照が返ります。FindControlメソッドは現在のコントロールコレクションのみを検索するので、プレゼンテーションコントロールは、そのコレクションに含まれていない限りは見つかりません。各コントロールのコントロールコレクションを再帰的に検索し、目的のコントロールが見つかるまで繰り返すという方法の方が安全です。

 BoundControlが見つかったら、ページャをDataBindingイベントのリスナとして登録します。ページャコントロールはデータソースを操作するので、このイベントハンドラは呼び出しチェインの末尾であることが重要です。プレゼンテーションコントロールがDataBindingイベントのイベントハンドラを既定どおりOnInitイベントハンドラで登録している限りは、ページャがデータソースを操作するときに問題は生じません。

 プレゼンテーションコントロールのDataSourceプロパティの取得は、DataBoundイベントハンドラで行います。

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();
}

 リフレクションを使用して、DataSourceプロパティのGet部分を実行するための呼び出しを行い、実際のデータソースへの参照を取得します。

データソースの操作方法の把握

 これでデータソースは把握できましたが、ページャはさらに、その操作方法を把握する必要があります。ページャをプレゼンテーションコントロールから独立させるべく多大な努力を費やしているというのに、ここでデータソースに依存するようなしくみにしたら、柔軟性のあるコントロールを構築するという目的が損なわれてしまいます。プラグ可能なアーキテクチャにすれば、.NET標準のものにせよ、独自のものにせよ、あらゆる種類のデータソースをページャコントロールが確実に処理できるようになります。

使用するデザインパターン

 堅牢性と拡張性に優れたプラグ可能なアーキテクチャを実現するための最善のソリューションは、GoFのBuilderパターンを使用することです。

IDataSourceAdapterインターフェイス

 IDataSourceAdapterインターフェイスは、ページャがデータ操作のために必要とする最も基本的な要素(プラグ)を定義します。

public interface IDataSourceAdapter
{
    int TotalCount{get;}
    object GetPagedData(int start,int end);
}

 TotalCountプロパティは、データソースに含まれる要素の総数を、データの操作前に返します。また、GetPagedDataメソッドは、元のデータのサブセットを返すことにより、データソースを操作します。たとえば、20個の要素を持つ単純な配列がデータソースで、ページャは1ページにつき10個の要素を表示する場合、このデータのサブセットは、1ページ目は要素0〜9、2ページ目は要素10〜19です。

DataViewAdapter

 DataView型用のプラグを提供するのは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;
    }
}

 DataViewAdapterは、IDataSourceAdapterGetPagedDataメソッドを実装しています。その中では、元のDataTableを複製し、元のDataTableから複製テーブルに対して行をインポートしています。クラスの可視性はあえてinternalと設定しています。実装の詳細をWeb開発者から隠ぺいし、Builderクラスを通じて簡単なインターフェイスを提供するためです。

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;}
}

 AdapterBuilder抽象クラスは、IDataSourceAdapter型を扱いやすくするためのインターフェイスを提供します。IDataSourceAdapterを直接使用するのではなく、抽象化のレベルを高めることによって、処理前(データのページング前)の指示を実行するための追加的な層が得られます。また、AdapterBuilderを使用することにより、DataViewAdapterなどの実装本体をページャのユーザーから隠ぺいできます。

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;}
    }
}

 DataView型とDataTable型には密接な関係があるため、汎用的なDataAdapterを用意することも理にかなっているかもしれません。その場合、DataTableを処理する別のコンストラクタを追加すれば十分です。ただあいにく、ユーザーがDataTableに対して異なる機能を必要とする場合に、クラス全体を置き換えるか継承する必要が生じてしまいます。同じIDataSourceAdapterを使用する新しいビルダーを構築することにより、アダプタの実装方法に対するユーザーの自由度が高まります。

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;}
    }
}

 AdapterCollectionDataSourceの種類に依存しており、これはBoundControl_DataBoundメソッドにちょうど適しています。インデックスキーにはType.FullNameメソッドを使用します。これにより、それぞれの型に対してインデックスキーが一意となります。この結果、所定の型に対応するビルダーが1つだけ含まれるようにするという役割は、AdapterCollectionが担うことになります。ビルダーの検索をBoundControl_DataBoundメソッドに追加すると、次のようになります。

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();
}

 BoundControl_DataBoundメソッドでは、HasParentControlCalledDataBindingを使用して、既にビルダーが作成済みかどうかを確認するという処理も行っています。既に作成済みの場合は、適切なビルダーを改めて見つけるという手間は省きます。もちろんこれは、別のDataSourceでユーザーがDataBindingを呼び出していないということが前提です。そういうことは当然ないはずです。Adaptersテーブルはコンストラクタで初期化しています。

Pagerメソッド
public Pager()
{
    _adapters = new AdapterCollection();
    _adapters.Add(typeof(DataTable),new DataTableAdapterBuilder());
    _adapters.Add(typeof(DataView),new DataViewAdapterBuilder());
}

 最後に実装するメソッドはBindParentです。この中では、データを操作し、それを返します。

BindParentメソッド
private void BindParent()
{
    _datasource.GetSetMethod().Invoke(BoundControl,
        new object[] { _builder.Adapter.GetPagedData( StartRow,ResultsToShow*CurrentPage)});
}

 このメソッドはかなり単純です。実際のデータ操作はAdapterが行うからです。完了したら、リフレクションを再度使用しますが、今回はプレゼンテーションコントロールのDataSourceプロパティを設定するための使用です。これでページャコントロールの動作はほぼ完成です。しかし、適切なプレゼンテーションがないことには、あまり役に立ちません。

ページャコントロールのプレゼンテーション

 前述のとおり、プレゼンテーションとロジックをきっちりと分離するためには、テンプレートを使用する方法が最適です。もっと具体的に言えば、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ページで簡単なレイアウトを定義することです。

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だけです。テンプレートを解析し、ホルダに関連付けます。

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);
}

 ページコントロールのInstatiateTemplateメソッドで最初に実行しているのは、テンプレートのインスタンス化です。Layout.InstantiateIn(_container)という行です。コンテナは、コントロールの1つであることには代わりなく、使い方も他のコントロールと同様です。その点に基づいて、InstantiateTemplateメソッドでは、4つのナビゲーションボタンと、個々のページャを保持するのに必要なパネルを検索します。ボタンの検索にはIDを使用しています。これは、ページャコントロールに課せられているちょっとした制限事項です。ナビゲーションボタンには、"First"、"Previous"、"Next"、"Last"、"Pager"という各IDがあらかじめ定義されていることが必須であり、さもないとボタンを検索できないのです。あいにく、ここで使用しているプレゼンテーション構造では、この方法をとるしかありません。

 別の方法としては、各ボタンをImageButtonクラスから継承することで、新しい型を定義するというものもあります。各ボタンがそれぞれ別の型になるので、コンテナに対する再帰的検索で特定の型を見つけるように実装でき、ボタンに適切な名前を付ける必要がなくなります。しかし、ドキュメントに適切に記載されていれば、そうしたちょっとした要件は特に問題とはならないはずです。

 4つのボタンを検索できたら、適切なイベントハンドラをバインドします。InstantiateTemplateをいつ呼び出すかについては、きわめて重要な判断を下す必要があります。通常は、こうしたメソッドは、CreateChildControlsメソッドで呼び出します。CreateChildControlsメソッドは、その名のとおり、子コントロールを作成するのが基本だからです。しかし、ページャコントロールの場合は、子コントロールを変更することはないので、CreateChildControlsメソッドに用意されている、一部のイベントに基づいて描画状態を変更するという機能は必要ありません。また、子コントロールの描画はできるだけ速くすることが望まれます。したがって、InstantiateTemplateメソッドはOnInitイベントで呼び出すのが適しています。

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ページでテンプレートを指定しなかった場合に既定のテンプレートを提供するという目的で使用できます。

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);
    }
}

 DefaultPagerLayoutでは、aspxページで追加していたすべてのナビゲーション要素をプログラムで実装していますが、今回は通常のHTMLテーブルで要素をフォーマットしています。これで、もしユーザーがプレゼンテーションテンプレートの実装を忘れた場合には、既定のテンプレートが代わりに適用されます。

既定のテンプレート
[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;}
}

 CurrentPageプロパティが保持するのは、その名のとおり、ページャのViewStateにある現在のページです。一方、PagersToShowメソッドとResultsToShowメソッドは、表示するページャの数と1ページあたりに表示する結果の数をユーザーが定義できるようにするためのプロパティを定義します。既定値は10に設定されています。

PagerSequenceプロパティ
private int PagerSequence
{
    get
    {
        return Convert.ToInt32(Math.Ceiling((double)CurrentPage / (double)PagersToShow));
    }
}
NumberOfPagersToGenerateプロパティ
private int NumberOfPagersToGenerate
{
    get{return PagerSequence*PagersToShow;}
}

 NumberofPagersToGenerateは、現在生成すべきページャの数を返します。

TotalPagesToShowメソッド
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により定義された既定のスタイルは、ページャコントロールを利用するユーザーにとってあまり役立たないものである場合もあります。ユーザーは、カスタムのスタイルを定義することで、ページャの見た目をカスタマイズできます。

UnSelectedPagerStyleプロパティ
public Style UnSelectedPagerStyle {get {return UnselectedPager;}}
public Style SelectedPagerStyle {get {return SelectedPager;}}

 UnSelectedPagerStyleは、選択されていない個々のページャで使用されるスタイルを表します。SelectedPagerStyleは、選択されている個々のページャで使用されるスタイルを表します。

ページャの生成とイベントハンドラのバインド

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);
}

 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();
}

 各イベントハンドラでは、ページャコントロールの現在のページを設定してから、バインドコントロールを更新しています。

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では、IsPagerVisibleIsPreviousVisibleIsNextVisibleなど、定義済みのデータ規則を適用しています。既定では、データ規則の適用はオンになっています。ユーザーは、IsDataSensitiveプロパティを設定すれば、これをオフにできます。ページャコントロールのプレゼンテーションは以上で完了です。

イベントの作成

 最後の仕上げとして、イベントを用意します。ページャのさまざまなイベントに対して、ユーザーが必要な調整を加えられるようにするためです。

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;}}
}

 ページャコントロールはカスタムのイベント引数を返す必要があるため、PageChangedEventArgsという専用のクラスを作成しています。PageChangedEventArgsクラスは、PagedEventInvoker型(イベントを発生し得るコントロールの単純な列挙体)と、新しいページ番号を返します。カスタムのイベント引数を処理するために、新しいデリゲートPageDelegateを定義しています。各イベントは次のように定義されています。

イベントの定義
public event PageDelegate PageChanged;
public event EventHandler DataUpdate;

 イベントにリスナが割り当てられていない場合、ASP.NETは、その動作に対処するために、例外を発生させます。ページャコントロールでは、次のようなRaiseEventメソッドを定義しています。

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デザイナが行う必要があるのは、プレゼンテーションコントロールにバインドすることだけです。

ページャの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ページは、ページャをRepeaterコントロールにバインドし、1ページあたりに表示する結果の数を2に設定し、選択されたページャの色を黄色に設定しています。レイアウトは既定のものを使用しています。次のテストアプリケーションでは、ページャコントロールをDataGridにバインドしています。

ページャコントロールの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
最新トップニュース
Graphic Design Forum
【Graphic Design Forum】
あなたならどうする - 状況その2 (10月6日)
データメーション
【データメーション】
雇用凍結でも楽観的状況か(10月6日)
ベンチャー専門家の目利きブログ「なぜこの企業は伸びるのか?」
【ベンチャー専門家の目利きブログ「なぜこの企業は伸びるのか?」】
「プロの営業マンを社会に輩出していく!!」/株式会社A・R・M(10月6日)
エンジニアの独り言
【エンジニアの独り言】
得体の知れない情報(?)との向き合い方(9月17日)
最新テクノロジーの意外な処方箋
【最新テクノロジーの意外な処方箋】
昆虫と退屈なことについて(9月16日)
DevX
DevX
アジャイルソフトウェアプロジェクトを管理する(10月7日)
エンジニア転職ノウハウ開発室
エンジニア転職ノウハウ開発室
SEって、デジタル製品は判官びいきで選ぶよね?(10月7日)
アイレップの SEM フロンティア
アイレップの SEM フロンティア
フル CSS でサイト構築をする SEO のメリット(10月7日)
百式のネットビジネス研究
百式のネットビジネス研究
YouTube の動画に吹き出しで台詞を入れられる「TubePopper」(10月7日)
「IT の耳」
「IT の耳」
【書評】『世界を変えるビジネス』――企業の社会貢献活動とは…(10月6日)
モバイルSEO@フラクタリスト
モバイルSEO@フラクタリスト
応用的な SEO 施策(3)(10月6日)
サーチからはじまるインタラクティブエージェンシー
サーチからはじまるインタラクティブエージェンシー
DB マーケティングと Web マーケティング 〜ビールとオムツの伝説から〜(10月6日)
最新ハイテク講座
最新ハイテク講座
視聴者が参加する時代へ!ネットにつながる「テレビ」(10月3日)
developer.com
developer.com
デザインパターンの使い方: Command(10月3日)
最新アフィリエイト事例にみる成功の法則
最新アフィリエイト事例にみる成功の法則
アフィリエイトメディアとの付き合い方(10月3日)
海外のインターネットコムアメリカ韓国ドイツトルコ
Copyright 2008 Jupitermedia Corporation All Rights Reserved.http://www.internet.com/