CodeZine(コードジン)

特集ページ一覧

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

データソースやプレゼンテーションに依存しないページング処理の実装

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

データのプレゼンテーションを行うWebアプリケーションを開発する際に、データソースやプレゼンテーションの種類に応じて、専用のページング処理を実装すると、大変時間を要することがあります。そこで、本稿では汎用的に利用できるページング処理 コントロールの作成方法を説明します。

目次

はじめに

 エンドユーザーを混乱させないようにデータを効果的に表示することは、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プロパティを設定するための使用です。これでページャコントロールの動作はほぼ完成です。しかし、適切なプレゼンテーションがないことには、あまり役に立ちません。


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

著者プロフィール

  • Tomasz Kaszuba(Tomasz Kaszuba)

    ポーランド最大のオンラインバンクInteligoの上級Web開発者。Javaおよび.NETでの開発経験は5年以上に及び、ポータル、CMS、分散システム、バンキングシステム、CRM、電気通信アプリケーションなど、さまざまなプロジェクトに従事。空き時間には、NetAr(http://sourceforg...

  • japan.internet.com(ジャパンインターネットコム)

    japan.internet.com は、1999年9月にオープンした、日本初のネットビジネス専門ニュースサイト。月間2億以上のページビューを誇る米国 Jupitermedia Corporation (Nasdaq: JUPM) のニュースサイト internet.com や EarthWeb.c...

バックナンバー

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

もっと読む

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