トップページへ PASSJ ブログへ
トップページへ

 
テクニカルライター 大澤 文孝


前回は、テーブル内の全レコードを一覧表示するWebページを作成しました。
今回は、複数のページに分けて表示するページング操作、列の並び替え、検索などの仕組みを実装していきます。
ページング操作
並べ替え
ページング操作と並び替えを連動させる
検索処理
DataViewオブジェクトの多用は禁物
まとめ

■ページング操作
一覧表示するデータが多数ある場合には、1ページにすべてを表示するのではなく、いくつかのページに分割して表示するのが一般的です。
DataGridコントロールは、そのようなページング操作に対応しています。
ページング操作するには、DataGridコントロールのプロパティページの[ページング]の部分で、[ページングを許可]にチェックを付けます。このとき、[ページサイズ]で指定した行数が、1ページに表示するレコードの数になります。


[TIPS]
ページサイズは、DataGridコントロールのPageSizeプロパティに対応します。

たとえば、図4-1のように、「5」を指定すると、1ページに5レコードが表示され、残り部分は、次のページに分離されます。

図4-1 ページングの設定

図4-1 ページングの設定

ページの移動のためのナビゲーション表示は、[ページの移動]の部分で変更できます。
ナビゲーションのモードは、「[次へ][戻る]ボタン」と「ページ番号」の2種類を選ぶことができます。前者を選択した場合には、図4-2のように、「次のページに辿るための『>』」「前のページに戻るための『<』」の2つのリンクが構成され、後者を選択した場合には、図4-3のように、分割されたページ数分だけの数字がリンクとして構成されます。


[TIPS]
図4-2の下の[<]のリンクや[>]のリンクの文字は、それぞれ「[次のページ]ボタンのテキスト」、「[前のページ]ボタンのテキスト」の項目で変更できます。


図4-2 「[次へ][戻る]ボタン」モードの場合のナビゲーションリンクの構成

図4-2 「[次へ][戻る]ボタン」モードの場合のナビゲーションリンクの構成

図4-3 「ページ番号」モードの場合のナビゲーションリンクの構成

図4-3 「ページ番号」モードの場合のナビゲーションリンクの構成

●ページ分割処理におけるデータバインディングのコーディング

図4-1のようにページングの設定をすれば、実行時に、図4-2または図4-3のように、ナビゲーションのリンクが表示されるようになります。
しかしこれらのナビゲーションのリンクをクリックしても、何も起こりません。ナビゲーションのリンクがクリックされたときの処理を記述するのは、開発者の仕事です。
ナビゲーションのリンクがクリックされると、PageIndexChangedのイベントが発生します。PageIndexChangedイベント内の処理では、ユーザーが何ページ目を表示しようとしているのかを判断し、該当ページを表示するよう実装します。
PageIndexChangedイベントを捕獲するには、プロパティページで、図4-4のように、PageIndexChangedイベントを処理するイベントを設定します。


図4-4 PageIndexChangedイベントの設定

図4-4 PageIndexChangedイベントの設定

PageIndexChangedイベントでは、DataGridPageChangedEventArgs型のイベント引数が得られます。このイベント引数のNewPageIndexプロパティには、ユーザーが表示しようとしているページが格納されています。
DataGridコントロールが表示しているページは、CurrentPageIndexプロパティで示されます。
よってPageIndexChangedイベントの処理で、次のように、DataGridコントロールのCurrentPageIndexプロパティに、引数のNewPageIndexプロパティの値を代入すれば、ナビゲーションのリンクがクリックされたときに、該当のページが表示されるようになります。


[TIPS]
ページ番号は、0から始まります。また、DataGridコントロールの最大ページは、PageCountプロパティで示されます。たとえば、「現在のページ/全体ページ」のように表示するユーザーインタフェースをとりたいときには、何らかのラベルコントロールなどを配置し、PageCountプロパティの値を埋め込めば実現できます。


private void ProductGrid_PageIndexChanged(     
    object source, System.Web.UI.WebControls.DataGridPageChangedEventArgs e)
{
    // 表示ページが変化したときの処理
    ProductGrid.CurrentPageIndex = e.NewPageIndex;
}


並べ替え
ページングと同様にして、並べ替えも実現できます。
並べ替えをするには、図4-5のように、DataGridコントロールのプロパティの[全般]の設定にて、[並べ替えを有効にする]にチェックを付けます。


図4-5 並べ替えを有効にする

図4-5 並べ替えを有効にする

そして[列]のページで、並び替えをしたい列に対して、[式の並び替え]の部分で、どの列に対して、データベース上のどの列で並び替えをしたいのかを設定します(図4-6)。
ここでは、次の3つの列を、並び替えの対象とする列として設定するものとします。
  • 商品名―productname
  • 在庫―stock
  • 価格―price
[TIPS]
[式の並び替え]の部分は、文字列にすぎません。そのため、存在しない列名を指定してもかまいません。

図4-6 並び替えの列の設定

図4-6 並び替えの列の設定
図4-6は、商品名に関する列のみを示しています。「在庫」や「価格」も同様に設定します。

並び替えの設定をして実行すると、設定した列のヘッダが、図4-7のようにリンクとして構成されるようになります。
ユーザーがこれらのリンクをクリックすると、DataGridコントロールのSortCommandイベントが発生します。


図4-7 並び替えを設定したときの列ヘッダの表示

図4-7 並び替えを設定したときの列ヘッダの表示

●並び替えの仕組み

SortCommandイベントで、どの列がクリックされたかを調べ、該当列で並べ替えて、DataGridコントロールにバインドし直すという処理を実装すれば、並び替えを実現できます。 該当列で並べ替えるには、次の2つの方法をとることができます。

(1)クエリーを実行するときにORDER BY句で指定する
SqlDataAdapterのFillメソッドを呼び出すときに実行されるSelectCommandプロパティに指定するクエリーにORDER BY句を指定しておき、特定の列で並び替えたうえでデータベースから読み込む。

(2)DataViewオブジェクト(DataViewコントロール)を使う
読み込んだDataTableオブジェクトの上にDataViewオブジェクトを被せ、DataViewオブジェクトのSortプロパティを使って並べ替える。

実装は(2)のほうが簡単ですが、次に説明する検索および絞り込みまで考えると、実行効率は(1)のほうが優れています。ここでは、より簡単な(2)の方法を採用することにします。


[TIPS]
(1)の方法の実装については、本稿の最後で少しだけ触れます。

●DataViewオブジェクトとは

DataViewオブジェクトとは、DataTableオブジェクトに被せ、DataTableオブジェクトが保持するレコードの見え方を変更するためのオブジェクトです。
DataViewオブジェクトには、指定した列でレコードを並び替えるSortプロパティや、特定の条件に合致するレコード以外を隠すRowFilterプロパティなどがあります。
DataGridコントロールとDataTableオブジェクトを直接データバインディングせず、図4-8のように、間にDataViewオブジェクトを挟むと、データの見え方をDataViewプロパティで操作することができるようになります。


図4-8 DataViewオブジェクトとDataTableオブジェクトとの関係

図4-8 DataViewオブジェクトとDataTableオブジェクトとの関係

●DataViewコントロールを導入する

プログラムのコードにおいてDataViewオブジェクトを直接実体化しても良いのですが、今回は、簡単に実装するため、GUI環境で簡単に構成できるDataViewコントロールを使うことにします。
まずは、WebフォームにDataViewコントロールを貼り付けます。ここでは、DataViewコントロールの名前を「ProductView」という名前にすることにします(図4-9)。


図4-9 DataViewコントロールをWebフォームに貼り付ける

図4-9 DataViewコントロールをWebフォームに貼り付ける

DataViewコントロールのTableプロパティには、DataSetコントロール内のProductTableテーブルを設定します(図4-10)。この設定により、ProductTableテーブルの上にDataViewコントロールを被せたことになります。

図4-10 DataViewコントロールのTableプロパティを設定する

図4-10 DataViewコントロールのTableプロパティを設定する

そして、DataGridコントロールのDataSourceプロパティを、いま作成したDataViewコントロールに変更します。これにより、DataGridコントロールがDataViewコントロールにデータバインディングされます(図4-11)。

図4-11 DataGridコントロールのDataSourceプロパティを変更する

図4-11 DataGridコントロールのDataSourceプロパティを変更する


●並べ替えを実現するコードの実装

並べ替えを実現するためには、ユーザーがDataGridのヘッダをクリックしたときに発生するSortCommandイベントにて、該当列で並び替えるようにする――すなわち、SortCommandイベントにて、DataViewコントロールのSortプロパティを適切なものに変更する――という処理をします。
SortCommandイベントでは、DataGridSortCommandEventArgs型のイベント引数が得られます。このイベント引数のSortExpressionプロパティには、図4-6の[式の並び替え]の部分に入力した文字列が格納されています。
DataViewコントロール(DataViewオブジェクト)は、Sortプロパティに並べ変えたい列名を指定すると、その列で並び替えることができます。
図4-6において、[式の並び替え]の部分には、並び替えたい列名を設定していますから、結局、SortCommandイベントでは、SortExpressoinプロパティの値を、そのままDataViewコントロールのSortプロパティに代入すればよいことになります。


[TIPS]
Sortプロパティには、列名をカンマで区切って、複数列を指定することもできます。また、列名の後ろに“DESC”と記述すると降順、省略するか“ASC”と記述すると昇順となります。昇順/降順の切り替えについては、すぐあとに説明します。

まずは、図4-12のようにして、SortCommandイベントのイベントハンドラを設定します。

図4-12 SortCommandイベントのイベントハンドラの設定

図4-12 SortCommandイベントのイベントハンドラの設定


そして、イベントハンドラのコードを次のように記述します。


private void ProductGrid_SortCommand(
    object source, System.Web.UI.WebControls.DataGridSortCommandEventArgs e)
{
    // ソートされたときのイベント
    // 並び替える
    ProductView.Sort = e.SortExpression;
    // バインドし直す
    ProductGrid.DataBind();
}


ここで注意したいのは、最後のDataBindメソッドの呼び出しです。
Sortプロパティを変更すると並び替えられるため、DataViewコントロールが保持するデータの内容が変化します。しかしDataGridコントロールは、つねにデータバインディングされているデータソースを監視しているわけではないので、データが変化されたことを知りません。
そのため、明示的にDataBindメソッドを呼び出し、DataGridコントロールに対して、データ変化したことを知らせる必要があります。DataGridメソッドの呼び出しを忘れると、データソースの変更が、DataGridコントロールに反映されません。


■ページング操作と並び替えを連動させる
しかし実際にやってみると、先に実装したページング操作で、次々とページを辿る部分と、並び替え部分との連携がうまくいきません。
DataGridのヘッダをクリックして並べ替えると、そのときだけはうまくいくものの、ページング操作して、他のページに移動したときに、並べ替えの状態が失われてしまいます。
これは、ページング処理において、並び替えをしていないことに起因します。
問題を解決するには、並び替えが発生したとき、並び替えの対象となった列名をどこかに保存しておき、ページング操作があったときにも、その並び替え列で並び替えてからページングする必要があります。


●ViewStateとSession

ASP.NETでは、Webページの状態を保存するために、次の2つの手法をとることができます。

[TIPS]
これ以外に、Cookieを使う方法もあります。また、Applicationコレクションを使うと、ユーザー全体で共有する値を保持することもできます。

(1)ViewStateを使う
ViewStateとは、Webフォームに隠しフィールドとして埋め込む値です。
Webフォームがクライアントに送信される際には、<FORM>タグと</FORM>タグの間に、次のような<INPUT type="hidden">と記述されたフィールド(以下、隠しフィールドと称す)が含まれています。



<input type="hidden" name="__VIEWSTATE" value="値"/>

値の部分は、Webフォーム上の各コントロールの状態(一部のプロパティの値)がBase64エンコードされた文字列です。

[TIPS]
ViewStateは、無効にすることもできます。ViewStateを無効にするには、EnableViewStateプロパティをfalseに設定します。EnableViewStateプロパティは、Webコントロールごとに設定できます。
ViewStateを無効にすると、__VIEWSTATEの隠しフィールドの値部分に、そのコントロールが保持する値が含まれなくなり、無駄なデータの送受信が減ります。その結果、ネットワークの負荷が抑えられ、高速化に役立ちます。状態を保持する必要がないWebコントロールは、明示的にEnableViewStateプロパティをfalseに設定しておくと、パフォーマンスが向上します。
ただし、EnableViewStateプロパティをfalseにすると、TextChangedなどの、以前の状態と現在の状態とを比較するような一部のイベントが発生しなくなることがあります。
また、Webフォーム全体(Pageオブジェクト)の、EnableViewStateプロパティをfalseに設定した場合には、__VIEWSTATEの隠しフィールド自身がクライアントに送信されなくなり、以下に説明するViewStateコレクションは、利用できなくなります。

ASP.NETでは、Pageオブジェクト(自分自身に相当)のViewStateコレクションを使って、任意の値を保存できます。


this.ViewState["項目名"] = 設定したい値;

[TIPS]
厳密に言えば、ViewStateオブジェクトには、シリアライズ可能な任意のオブジェクトしか保存できません。シリアライズ可能なオブジェクトとは、ISerializableインタフェースを備えるオブジェクトのことです。

ViewStateコレクションに格納した値は、__VIEWSTATE隠しフィールドに含まれ、クライアントに送信されます。
ユーザーがSUBMITボタンを押した場合には、__VIEWSTATE隠しフィールドの内容がIIS側に送信されます。
ASP.NETでは、__VIEWSTATE隠しフィールドの内容を展開し、Webフォーム上の各コントロールのプロパティの値や、ViewStateコレクションを復元します(図4-13)。


[TIPS]
WebフォームのEnableViewStateMacプロパティの値をtrueにすると、__VIEWSTATE隠しフィールドの値の末尾にハッシュコードが付き、ユーザーが__VIEWSTATE隠しフィールドを偽造したときには、例外が発生するようになります。EnableViewStateMacプロパティのデフォルトはfalseであり、偽装の防止機能は無効です。しかし安全性を高めるならば、EnableViewStateMacプロパティの値をtrueにしておいたほうがよいでしょう。

図4-13 ViewStateの構成

図4-13 ViewStateの構成

図4-13に示したように、ViewStateを使うと、任意の値を保存でき、あとで取り出すことができるようになります。

(2)Sessionを使う
ASP.NETは、接続してきたクライアントを識別するために、接続ごとにセッションを構成します。
セッションは、クライアントに対して、他のクライアントと重複しない固有のセッションIDを送信することで構成されます。
セッションの構成は、アプリケーション構成ファイル(Web.configファイル)のsessionState要素で決まります。
Visual Studio .NET 2003がデフォルトで構成するWeb.configファイルのsessionState要素は、次のようになっています。



<sessionState
    mode="InProc"
    stateConnectionString="tcpip=127.0.0.1:42424"
    sqlConnectionString="data source=127.0.0.1;Trusted_Connection=yes"
    cookieless="false"
    timeout="20"
/>


・mode属性
セッションが構成されるモードを指定します。次のいずれかの値を指定します。


mode属性の値 意味
Off セッションを使わない
InProc ローカル上に(IIS上)に構成する。もっとも高速
StateServer StateServer上に構成する。StateServerとは、「ASP.NET State Service」サービスを動作させたサーバのこと。StateServerの場所は、stateConnectionString属性で指定する
SQLServer SQL Server上に保存する。SQL Serverへのデータベース接続文字列は、sqlConnectionString属性に指定する。sqlConnectionString属性に指定したSQL Server上では、InstallPersistSqlState.sqlを実行し、セッションの保存に必要なデータベースならびにテーブルを構成しておく


[TIPS]
InstallPersistSqlState.sqlファイルは、“Windowsフォルダ\ Microsoft.NET\Framework\バージョン番号”フォルダにあります。

・cookieless属性
クライアントにセッションIDをどのようにして送信するかを設定します。trueのときはCookieを使って、flaseのときにはURLの一部に含める形で送信します。


[TIPS]
一般には、セッションIDは、Cookieとして送信するようcookieless属性をtrueとして構成します。しかし、CookieをサポートしないWebブラウザでもセッションの機能を使えるようにしたいならば、cookieless属性をfalseとして構成します。
cookieless属性がtrueであるとき、Cookieに対応しないWebブラウザ、もしくは、ユーザーがCookieを無効にしている場合には、後述するSessionコレクションに保存した値は、失われます(Sessionコレクションを利用しても例外は発生しませんが、次回クライアントが接続してきたとき、前回Sessionコレクションに保存した値は失われます)

・timeout属性
セッションが無効になるまでの時間を分単位で指定します。

セッションは、クライアントごとおよびWebアプリケーションごと(IISの仮想ディレクトリごと)に、図4-14のように用意されます。


図4-14 セッションの構成

図4-14 セッションの構成

セッションには、Sessionコレクションを使って任意のオブジェクトを保存できます。


this.Session["項目名"] = 設定したい値;

[TIPS]
Sessionコレクションには、mode属性がInProcであるときに限り、シリアル化できないオブジェクトも保存できます。mode属性がStateServerやSQLServerである場合には、シリアル化可能なオブジェクトしか保存できません。

セッションに保存した値は、クライアントが最後に接続してからtimeout属性で指定した分数が経過するまで有効です。

[TIPS]
SessionコレクションのAbandonメソッドを呼び出すと、timeout属性で指定した分数が経過していなくても、即座にセッションを無効にすることができます。

●ViewStateとSessionとの違い
ViewStateとSessionは似ていますが、次の2つの明白な違いがあります。

(1)データを保存する場所
ViewStateの場合には、__VIEWSTATE隠しフィールドに保存され、クライアントに送信されます。つまりクライアント側に保存されることになります。
それに対してSessionの場合には、サーバ内に保存されます。
すなわち、ViewStateに保存したデータは、クライアントが参照することができるという意味です。よって、秘密にしたいデータをViewStateに保存すべきではありません。


[TIPS]
_VIEWSTATE隠しフィールドの値は、一見、暗号化された文字列のようにも見えますが、実際には、Base64エンコードされただけのデータであり、解読は容易です。

また、巨大なデータをViewStateに保存すべきでもありません。ViewStateに保存した値は、クライアントに送信され、ユーザーがSUBMITボタンを押したときに、サーバに向けて送信し直してきます。
そのため、ViewStateに巨大なデータを保存すると、ネットワークを流れるデータが膨大なものとなります。


(2)有効範囲
ViewStateに保存したデータは、同一のWebフォーム内でのみ有効です。言い換えれば、ポストバック中のみ有効です。
それに対して、Sessionに保存したデータは、Webアプリケーションの異なるWebフォームから共通のものを利用可能です。

これらの(1)(2)の点を踏まえ、ViewStateとSessionは、次のように使い分けるとよいでしょう。


●ViewStateを使うのが望ましい場面
  • ポストバック中だけ値を保存
  • 小さなデータである
  • クライアントに見られてもよい
●Sessionを使うのが望ましい場合
  • 複数のWebフォーム間での値の保存
  • 比較的大きなデータである
  • クライアントに見せたくない
今回の並べ替え処理では、ユーザーがどの項目で並べ替えたのか、その列名を保持すればよく、クライアントに対して秘密にする必要はありません。また小さなデータですし、他のWebフォーム内でその情報を参照する必要もありません。
よってここでは、ViewStateを使って実装することにします。


ViewStateに状態を保存するように構成する
では実際に、ViewStateを使って情報を保存するように変更します。まずは、WebフォームのLoadイベントの処理をリスト4-1のように実装します。


リスト4-1 Loadイベントの処理

private string orderfield; // 並び替える列
private string asc_or_desc; // 降順または昇順

private void DataGridBindStart()
{
    // データバインディングする

    // データベースからデータを取得する
    sqlDataAdapter1.Fill(ProductDataSet);

    // 並び替えをする
    ProductView.Sort = orderfield + " " + asc_or_desc;

    // データバインディングする
    ProductGrid.DataBind();

    // ViewStateに値を設定する
    this.ViewState["orderfield"] = orderfield;
    this.ViewState["asc_or_desc"] = asc_or_desc;
}

private void Page_Load(object sender, System.EventArgs e)
{
    // ページを初期化するユーザー コードをここに挿入します。
    if (!this.IsPostBack)
    {
        // ポストバックではない場合
        // 並び替えは、とりあえずid列順とする
        orderfield = "id";
        asc_or_desc = "";

        // データバインディングする
        DataGridBindStart();
    }
    else
    {
        // ポストバックの場合
        // ViewStateから値を読み取る
        orderfield = this.ViewState["orderfield"].ToString();
        asc_or_desc = this.ViewState["asc_or_desc"].ToString();
        // ここではデータバインディングせず、後続のイベントに任せる
    }
}


リスト4-1では、並び替え列を保持するorderfield変数と、昇順/降順のどちらかを保持するasc_or_desc変数を用意しています。


private string orderfield; // 並び替える列
private string asc_or_desc; // 降順または昇順


asc_or_desc変数には、昇順であるときには空文字、降順であるときには“DESC”を格納するものとします。
Loadイベント内では、まず、IsPostBackプロパティを調べ、ポストバック状態であるかどうかを調べています。
ポストバック状態ではない場合には、ViewStateは有効ではないので、次のようにして、orderfield変数にはとりあえずのところid列を、asc_or_desc変数には空文字を設定し、DataGridBindStartメソッドを呼び出しています。



if (!this.IsPostBack)
{
    // ポストバックではない場合
    // 並び替えは、とりあえずid列順とする
    orderfield = "id";
    asc_or_desc = "";

    // データバインディングする
    DataGridBindStart();
}


DataGridBindStartメソッドは、リスト4-1に示したように、Fillメソッドを呼び出してデータベースからレコードを取得したのち、DataViewコントロールのSortプロパティを設定し、データバインディングする処理をしています。


// データベースからデータを取得する
sqlDataAdapter1.Fill(ProductDataSet);

// 並び替えをする
ProductView.Sort = orderfield + " " + asc_or_desc;

// データバインディングする
ProductGrid.DataBind();


Sortプロパティには、並び替えたい列名を指定しますが、その後ろに“DESC”と記述すると、降順で並び替えるという仕様になっています。つまり上記の処理では、、変数asc_or_descが“DESC”に設定されているならば、このとき降順で並べ替えられることになります。
DataGridBindStartメソッドでは、最後にViewStateに変数orderfieldと変数asc_or_descの値を保存するようにしています。



// ViewStateに値を設定する
this.ViewState["orderfield"] = orderfield;
this.ViewState["asc_or_desc"] = asc_or_desc;


この処理により、次回、ポストバック状態となった場合には、ViewStateの値を参照することにより、以前にユーザーが設定した並べ替えの列と昇順/降順の値がわかることになります。
実際、PageクラスのLoadイベントの処理では、ポストバックのときには、次のように、orderfield変数とasc_or_desc変数の値をViewStateから取り出しています。



if (!this.IsPostBack)
{
    // ポストバックではない場合
    …省略…
}
else
{
    // ポストバックの場合
    // ViewStateから値を読み取る
    orderfield = this.ViewState["orderfield"].ToString();
    asc_or_desc = this.ViewState["asc_or_desc"].ToString();
    // ここではデータバインディングせず、後続のイベントに任せる
}


ポストバックの場合には、先に示したDataGridBindStart()メソッドの呼び出しをしていません。つまり、ポストバックの場合には、この時点ではデータバインディングしないようにしてあります。

ここまでのコーディングでは、ポストバック状態となるのは、

(1)DataGridのページングのナビゲーションがクリックされ、ページ操作が発生したとき
(2)DataGridのヘッダがクリックされ、並び替えが発生したとき

のいずれかです。
すでに連載第1回目の図1-13に示したように、上記の(1)(2)のイベントは、Loadイベントよりもあとで発生します。上記の(1)(2)では、当然、表示するページを変更したり、並べ替えの条件を変更したりする必要があるため、Loadイベントの時点でデータバインディングしても効果がありません。
そこでポストバック時には、以降発生するPageIndexChangedイベントやSortCommandイベントにて適切なページ設定や並び替え条件を設定したあとに、データバインディングします。
具体的には、PageIndexChangedイベントは、リスト4-2のように実装します。

リスト4-2 PageIndexChangedイベントの処理

private void ProductGrid_PageIndexChanged(
    object source, System.Web.UI.WebControls.DataGridPageChangedEventArgs e)
{
    // 表示ページが変化したときの処理
    ProductGrid.CurrentPageIndex = e.NewPageIndex;

    // データバインディングする
    DataGridBindStart();
}


リスト4-2では、CurrentPageIndexプロパティを変更したのち、データバインディングするという処理をしています。


ProductGrid.CurrentPageIndex = e.NewPageIndex;
DataGridBindStart();


一方、SortCommandイベントの処理は、リスト4-3のようになります。

リスト4-3 SortCommandイベントの処理

private void ProductGrid_SortCommand(
    object source, System.Web.UI.WebControls.DataGridSortCommandEventArgs e)
{
    // ソートされたときのイベント

    // 並べ替えの列を設定する
    orderfield = e.SortExpression;

    // 降順、昇順を切り替える
    if (asc_or_desc == "")
    {
        asc_or_desc = "DESC";
    }
    else
    {
        asc_or_desc = "";
    }

    // 並び替えられたときは、PageIndexを先頭にする
    ProductGrid.CurrentPageIndex = 0;

    // データバインディングする
    DataGridBindStart();
}


リスト4-3では、並び替え列をイベント引数のSortExpressionプロパティから取得した値に変更するという処理をしています。


// 並べ替えの列を設定する
orderfield = e.SortExpression;


すでにリスト4-1に示したように、データバインディングする処理を実装したDataGridBindStartメソッドでは、orderfield変数の値を並び替え列としていますから、このあと、DataGridBindStartメソッドを呼び出せば、この列で並び替えが実現できるようになります。
さらにリスト4-3では、次のようにして、asc_or_desc変数の値を変更しています。


// 降順、昇順を切り替える
if (asc_or_desc == "")
{
    asc_or_desc = "DESC";
}
else
{
    asc_or_desc = "";
}


この処理により、列がクリックされるたびに、asc_or_desc変数が、空文字と“DESC”とで切り替わります。すなわち、「クリックするたびに昇順/降順を切り替える」という処理が実現できます。
また、並び替えの列が変化した場合には、次のように、DataGridコントロールの表示ページを先頭に変更するという処理もしています。


// 並び替えられたときは、PageIndexを先頭にする
ProductGrid.CurrentPageIndex = 0;


この処理は不可欠というものではありませんが、この処理をしないと、並び替え状態が切り替わったあとも、切り替える前に見ていたページ番号のページを見てしまうので、ユーザーにとってわかりにくくなります。
最後に、次のようにして、データバインディングのために、DataGridBindStartメソッドを呼び出します。



// データバインディングする
DataGridBindStart();


この処理によりデータバインディングが実現されます。
また、DataGridBindStartメソッドでは、リスト4-1に示したように、変数orderfieldと変数asc_or_descの値をViewStateに保存する処理をしていますから、次にユーザーが、たとえば、ページのナビゲーションのリンクをクリックしてポストバックしてきたときにも、ここで設定した並び替えの列や昇順/降順の状態は、保持されます。
つまり並び替えたとき、ページ移動しても、その並び替え順序で、正しくページ移動できるようになります。



■検索処理
ところで、DataViewオブジェクト(DataViewコントロール)には、RowFilterプロパティがあり、このプロパティを設定すると、条件に合致するレコードのみを有効とし、残りを隠してしまうことができます。
この機能を使うと、データの簡単な検索機能が容易に実現できます。そこで、この機能を使って、製品名を検索する機能を実装してみます。


●検索用のテキストボックスとボタンの配置
まずは、Webフォーム上に、検索文字列を入力するためのテキストボックスと、ボタンを用意します。
ここでは、図4-15に示すように、SearchTextテキストボックスと、SearchBtnボタンを用意します。
SearchTextテキストボックスは<asp:TextBox>で、SearchBtnは<asp:Button>で構成するものとします。


[TIPS]
SearchBtnは、Submitボタン(<INPUT type="SUBMIT">)ではなく、<asp:Button>のASPのサーバーコントロールとして用意することにします。Submitボタンの場合には、イベントが発生せず、<asp:Button>の場合には、イベントが発生するという違いがあります。

図4-15 検索用のテキストボックスとボタンの配置

図4-15 検索用のテキストボックスとボタンの配置


●検索処理の実装
[検索]ボタン(SearchBtnボタン)が押されたときには、Clickイベントが発生します。
Clickイベントは、次のように実装します。



private string filter; // 検索文字列

private void SearchBtn_Click(object sender, System.EventArgs e)
{
    // [検索]ボタンが押されたときの処理
    filter = SearchText.Text;

    // PageIndexを先頭にする
    ProductGrid.CurrentPageIndex = 0;

    // データバインディングする
    DataGridBindStart();
}


ここでは、SearchTextテキストボックスに入力されたテキストを、そのまま変数filterに代入しているだけです。

[TIPS]
実際には、不正な文字列が入力されたときのエラー処理を実装する必要があります。たとえば、連載第1回目で説明した検証コントロールを使って、正しい文字列が入力されているかどうかをチェックするようにします。しかし冗長になるので、ここでは、エラーチェックは省略することにします。

次に、リスト4-1に示したLoadイベントの処理やデータバインディング時の処理を、リスト4-4のように変更します。

リスト4-4 検索機能に対応したLoadイベントおよびデータバインディングの処理

using System.Text.RegularExpressions;

private void DataGridBindStart()
{
    // データバインディングする

    // データベースからデータを取得する
    sqlDataAdapter1.Fill(ProductDataSet);

    // 並び替えをする
    ProductView.Sort = orderfield + " " + asc_or_desc;

    // 検索条件があれば加える
    if (filter != "")
    {
        string escape =
            Regex.Replace(filter, "([%\\*\\]\\[])", "[$1]" ) .Replace("'", "''");
        ProductView.RowFilter = "productname like '%" + escape + "%'";
    }


    // データバインディングする
    ProductGrid.DataBind();

    // ViewStateに値を設定する
    this.ViewState["orderfield"] = orderfield;
    this.ViewState["asc_or_desc"] = asc_or_desc;
    this.ViewState["filter"] = filter;
}

private void Page_Load(object sender, System.EventArgs e)
{
    // ページを初期化するユーザー コードをここに挿入します。
    if (!this.IsPostBack)
    {
       // ポストバックではない場合
       // 並び替えは、とりあえずid列順とする
       orderfield = "id";
       asc_or_desc = "";
       filter = "";

        // データバインディングする
        DataGridBindStart();
        }
        else
        {
            // ポストバックの場合
            // ViewStateから値を読み取る
            orderfield = this.ViewState["orderfield"].ToString();
            asc_or_desc = this.ViewState["asc_or_desc"].ToString();
            filter = this.ViewState["filter"].ToString();
            // ここではデータバインディングせず、後続のイベントに任せる
    }
}


リスト4-1から変更のあった部分は、太字で示してあります。

リスト4-4において、検索条件を指定しているのは、次の部分です。



// 検索条件があれば加える
if (filter != "")
{
    string escape =
        Regex.Replace(filter, "([%\\*\\]\\[])", "[$1]" ).Replace("'", "''");
    ProductView.RowFilter = "productname like '%" + escape + "%'";
}


検索条件は、RowFilterに指定します。RowFilterには、いくつかの条件式を指定できます。ここではLike演算子を使い、「productname like '%ユーザーが入力した値%'」という条件を指定しています。
Like演算子は、「その文字列を含むかどうか」を調べる演算子です。「%」は任意の文字列を示します。そのため、「文字列%」は、その文字列があり任意の文字列が続くことを意味しますから、結局、「その文字列が始まること」、「%文字列」は、何か任意の文字がありその文字列が続くということを意味しますから、結局、「その文字列で終わること」を示します。つまり、「%文字列%」を指定すると、「その文字列を含む」という条件を指定できます。


[TIPS]
「%」の代わりに「*」を使っても、同じ意味です。

基本的には、それでよいのですが、RowFilterプロパティで指定するシングルクォートで囲まれた文字列は、いくつか特殊文字として扱われます。
具体的には、次の4つの文字は、特殊な文字として扱われ、これらの文字を扱う場合には、“[”と“]”で括り、エスケープしなければならない決まりになっています。


特殊文字 エスケープ後の文字
% [%]
* [*]
[ [[]
] []]

またシングルクォートは、シングルクォート2つに置換しなければなりません。
そこで、ここでは正規表現による置換を行ない、上記の4つの文字を、“[”と“]”で括り、シングルクォートをシングルクォート2つに変換したあと、RowFilterの条件として指定するようにしています。
これらの処理によって、ユーザーが何らかの検索文字を入力した場合には、RowFilterプロパティによって、与えられた条件のものだけが表示されるようになります。


■DataViewオブジェクトの多用は禁物
このように、DataViewオブジェクトを利用すると、並べ替えや特定レコードの抽出が容易に行えます。
しかしDataViewオブジェクトを使った並べ替えやレコードの抽出は、簡易機能にすぎないということを念頭におかなければなりません。
DataViewオブジェクトは、DataTableオブジェクトの一部を抽出するものにすぎません。DataTableオブジェクトは、メモリ上にあります。
たとえば、10万件のレコードが存在するテーブルを考えると、たとえ、DataViewオブジェクトのRowFilterプロパティを使って、そのうちの数百件しか取り出していない場合や、ページングしてその一部しかDataGridコントロールに表示していない場合でも、メモリ上には、10万件のレコードが存在しています。
つまり、扱うレコードが多い場合には、DataGridコントロールのページング機能や、DataViewコントロールのRowFilterプロパティを使った検索機能は、サーバのリソースを消費しすぎるため、現実的ではありません。
扱うレコードが多い場合には、DataTableオブジェクトにデータを格納する時点で、不要なデータを排除する工夫が不可欠です。
そのためには、SqlDataAdapterオブジェクトのFillメソッドを呼び出すときに、SelectCommandプロパティに指定するSELECT文にWHERE句を付けて必要なレコードしか取り出さないようにするとか、SqlDataReaderオブジェクトを使って実装してしまうといった方法をとるしかありません。
ここでは、参考までに、SqlDataReaderオブジェクトを使った実装方法を簡単に説明します。



●DataGridコントロールに対して動的にデータソースを割り当てる
まずは、DataGridコントロールに対して、固定されたデータソースを割り当てるのではなく、実行時にデータソースを割り当てるようにします。
いままでの行程では、DataGridコントロールに対して、DataViewコントロールをデータソースとして割り当てていたので、これをリセットして空白にします(図4-16)。


図4-16 DataGridコントロールのDataSourceプロパティを空にする

図4-16 DataGridコントロールのDataSourceプロパティを空にする

以降のコードでは、SqlDataReaderオブジェクトを使ってデータバインディングするので、連載第3回目でWebフォームに配置したSqlDataAdapterコントロールやSqlConnectionコントロール、DataSetコントロール、そして今回配置したDataViewコントロールは、すべて削除してしまってかまいません。
次に、DataGridコントロールでページングするのではなく、自らページング処理を行なうため、DataGridコントロールのプロパティで、[カスタムページング]にチェックを付けます(図4-17)。


図4-17 [カスタムページング]を有効にする

図4-17 [カスタムページング]を有効にする
カスタムページングを有効にすると、DataGridコントロールが、レコードの総数から、どの部分を表示すべきかを自動計算しなくなります。

●SqlDataReaderオブジェクトを使ったコーディング
では、先にリスト4-4に示したデータバインディングするためのメソッドであるDataGridBindStartメソッドを、リスト4-5のように変更します。

リスト4-5 SqlDataReaderオブジェクトを使うようにしたDataGridBindStartメソッド


using System.Data.SqlClient;

private void DataGridBindStart()
{
    // データバインディングする

    // データベース接続
    SqlConnection sqlconn =
        new SqlConnection(ConfigurationSettings.AppSettings["DSNSTRING"]);

    // 条件抽出のためのWHERE句
    string where;
    if (filter != "")
    {
        // フィルタあり
        string escape = Regex.Replace(filter, "([%\\*\\]\\[])", "[$1]" ).Replace("'", "''");
        where = " WHERE productname like '%" + escape + "%'";
    }
    else
    {
        // フィルタなし
        where = "";
    }

    SqlDataReader sqlreader = null;
    try
    {
        sqlconn.Open();

        // レコード数を取得するためのSELECT文
        SqlCommand sqlcmd1 = new SqlCommand(
            "SELECT COUNT(*) FROM Products" + where, sqlconn);

        // レコード数を取得
        int recCnt = (int)sqlcmd1.ExecuteScalar();

        // レコード本体を取得
        string desc1, desc2;
        if (asc_or_desc == "")
        {
            // 昇順のとき
            desc1 = " DESC";
            desc2 = "";
        }
        else
        {
            // 降順のとき
            desc1 = "";
            desc2 = " DESC";
        }

        // 実行するSQL文
        string mainquery =
            "SELECT TOP " + ProductGrid.PageSize + " * FROM " +
            "(SELECT TOP " + (recCnt - ProductGrid.CurrentPageIndex * ProductGrid.PageSize) +
            " id, productname, price, stock, comment, detail, imgdata, createdate, lastupdate FROM Products " + where +
            " ORDER BY " + orderfield + desc1 + ") As Products ORDER BY " + orderfield + desc2;

        SqlCommand sqlcmd2 = new SqlCommand(mainquery, sqlconn);

        // 全レコード数を設定
        ProductGrid.VirtualItemCount = recCnt;

        // 実際のデータを読み込んでバインディング
        sqlreader = sqlcmd2.ExecuteReader();
        ProductGrid.DataSource = sqlreader;
        ProductGrid.DataBind();
    }
    catch (SqlException e)
    {
        // 何らかのエラーが発生
        // ここではエラー処理は省略
        throw e;
    }
    finally
    {
        if (sqlreader != null)
        {
            sqlreader.Close();
        }
        sqlconn.Close();
    }

    // ViewStateに値を設定する
    this.ViewState["orderfield"] = orderfield;
    this.ViewState["asc_or_desc"] = asc_or_desc;
    this.ViewState["filter"] = filter;
}


リスト4-5の処理は、少し複雑ですが、基本的には、実行するSELECT文で、検索条件をWHERE句で設定し、現在表示しているページだけを取り出すという処理をしているにすぎません。

[TIPS]
リスト4-5は、実行すべきクエリーを、文字列連結で作っており、あまり好ましいものではありません。実際には、パラメータクエリーを使ったほうが良いですし、また、欲を言えば、ストアドプロシージャ化したほうがよいでしょう。
また誤差のない表示をするならば、トランザクション処理やロック処理も検討すべきです。リスト4-5では、SELECT文を2回実行し、1回目の実行では、対象となるレコードの総数を、2回目の実行では、実際のレコードの値を取り出しています。もし、1回目の実行と、2回目の実行の間で、レコードの数が増減すると、ページング処理における表示位置がずれることになります。
そこまで完璧に実装すると、複雑になるので、リスト4-5では、そこまでの考慮はしていません。

リスト4-5の処理でポイントとなるのは、次の2点です。

(1)レコードの総数をVirtualItemCountプロパティに設定する
カスタムページングを有効にした場合には、DataGridコントロールで表示される予定の全レコード総数をVirtualItemCountプロパティに設定する必要があります。
リスト4-5では、次のようにCOUNT関数を使ったSELECT文を送信し、レコードの総数を得て、それをVirtualItemCountプロパティに設定しています。


// レコード数を取得するためのSELECT文
SqlCommand sqlcmd1 = new SqlCommand(
    "SELECT COUNT(*) FROM Products" + where, sqlconn);

// レコード数を取得
int recCnt = (int)sqlcmd1.ExecuteScalar();

// 全レコード数を設定
ProductGrid.VirtualItemCount = recCnt;


VirtualItemCountプロパティは、DataGridコントロールが、ページナビゲーションのリンクを表示するとき、移動可能な最大ページ数を把握するのに使われます。
なおここでは、SELECT文を実行するのに、ExecuteScalarメソッドを使っています。ExecuteScalarメソッドは、該当するクエリーを実行し、結果レコードの第1行、第1列目を単一値として取得するためのメソッドです。COUNT関数でレコードの総数を調べる場合などには、ExecuteReaderメソッドを呼び出して、1レコード分だけ取得するよりも、ExecuteScalarメソッドを使ったほうが簡単かつ高速です。


(2)表示対象となるページだけを取り出してデータバインディングする
カスタムページングする場合には、ユーザーがどのページを表示しようとしているのかを判定し、表示に必要なレコードだけをデータバインディングします。
現在表示しているページ番号は、CurrentPageIndexプロパティで示されます。また、DataGridコントロールの1ページに表示するレコード数(図4-1で設定した[ページサイズ])は、PageSizeプロパティで示されます。
すなわち、

「 CurrentPageIndex * PageSize + 1番目のレコード」から、PageSize件分

までのレコードを取り出してデータバインディングすればよいことになります。
SELECT文を実行したとき、任意の範囲のレコードを取り出すことができれば話は簡単なのですが、SQL Serverでは、TOP句を使って、先頭から指定したレコード数だけを読み取ることはできるものの、いくつかのレコードを読み飛ばす機能は備えていません。
そこで、リスト4-5では、次のように、TOP句を使ったサブクエリを組み合わせて、特定範囲のレコードを取得する方法を使っています。



// 実行するSQL文
string mainquery =
        "SELECT TOP " + ProductGrid.PageSize + " * FROM " +
        "(SELECT TOP " + (recCnt - ProductGrid.CurrentPageIndex * ProductGrid.PageSize) +
        " id, productname, price, stock, comment, detail, imgdata, createdate, lastupdate FROM Products " + where +
        " ORDER BY " + orderfield + desc1 + ") As Products ORDER BY " + orderfield + desc2;


文字列が長くてわかりにくいので、例を挙げて説明します。たとえば、次の場合を考えます。

・DataGridコントロールのページサイズが5レコードに設定されている
        →1ページあたりのレコード数は5
・ユーザーが2番目のページ(CurrentPageIndexプロパティの値が1)を表示しようとしている
        →11件目〜15件目までを表示
・id列で昇順に並べ替えられようとしている

・表示すべきレコードの総数(先の(1)の処理で得たレコード数)は、30レコード


このとき、変数mainqueryに設定されるSQL文は、おおむね、次のようになります(列名は冗長なので「*」で示すことにします)。


SELECT TOP 5 * FROM
(SELECT TOP 20 * FROM Products ORDER BY id DESC) As Products ORDER BY id


このサブクエリは、id列で降順にし、先頭から20レコード分を取り出したものとなります。これにより、30〜10件分が取り出されます。そしてその結果を昇順にして先頭5レコードを取り出しているので、最終的に10件〜15件までが取り出されることになります(図4-18)。

図4-18 TOP句とサブクエリを組み合わせて、レコードの一部を取り出す



リスト4-5では、このSELECT文を実行し、その結果をDataGridコントロールにデータバインディングしています。


SqlCommand sqlcmd2 = new SqlCommand(mainquery, sqlconn);

// 実際のデータを読み込んでバインディング
sqlreader = sqlcmd2.ExecuteReader();
ProductGrid.DataSource = sqlreader;
ProductGrid.DataBind();


この結果、ユーザーには、10件〜15件目が表示されます。
ここで注目したいのは、データベースからは、本当に10〜15件目の、たかだか5レコード分しか取得していないという点です。この方法ならば、元のレコードが何万レコードあっても、取り出すレコードは、DataGridコントロールが表示しているレコード数だけなので、サーバのリソースは問題となりません。


■まとめ
一番最後に説明した、SqlDataReaderオブジェクトを使う方法は、やや複雑なので、パフォーマンスを向上させたい場合などに限って使えばよく、常に、SqlDataReaderオブジェクトを使わなければならないという意味ではありません。
しかしSqlDataReaderオブジェクトを使ったデータバインディング方法は、ページングや並び替えのみならず、実行するクエリー次第で、自由な条件でのレコード取得ができますから、DataGridコントロールを使ううえで、このような方法もあることは知っておいたほうがよいでしょう。
最終回となる次回では、既存レコードの更新や削除、そして、パフォーマンスの向上などについて説明する予定です。

今回のリストをすべてダウンロード(LZH形式)


「ステップアップ!ADO.NET」 目次
第1回 ASP.NETの基礎とデータベースへの書き込み
第2回 データベースからの読み込み 〜その1:単一レコードの表示〜
第3回 データベースからの読み込み 〜その2:複数レコードの表示〜
第5回 データベースの更新 〜レコードの更新と削除、そして競合問題を考える〜

★この連載に関する意見交換は、下記の掲示板・MLをご利用ください★
・PASSJ掲示板 『開発の現場を語ろう』
・Webテクノロジー分科会メーリングリスト pml-web@sqlpassj.org