ASP.NET Web Forms の GridView で PagerTemplate に DropDownList を置いたのに、ページを選んでも常に1ページ目のまま。SqlDataSource から手動 DataBind に切り替えた途端に起きる典型的な原因と、確実に直す実装パターンを整理します。
問題の概要:PagerTemplate の DropDownList でページ選択したい
GridView のページング UI は、標準の「次へ」「前へ」だけでなく、PagerTemplate を使うと自由に作り込めます。よくある要望が「DropDownList でページ番号を選び、任意ページへジャンプしたい」です。
ところが、次のような変更をした直後に「どのページを選んでも 1 ページ目から動かない」症状にぶつかることがあります。
- 以前は
SqlDataSourceを使い、GridView にはDataSourceIDを設定していた - 変更後は
Page_Loadなどで DB から取得し、GridView.DataSource→DataBind()する「手動バインド」にした - ついでに
AutoGenerateColumns="False"にして、BoundFieldなどで列を自前定義した
この状態で DropDownList_SelectedIndexChanged は発火しているのに、見た目が変わらない(ページが切り替わらない)――これが今回のテーマです。
症状:イベントは動くのにページが切り替わらない
代表的な症状は次のとおりです。
- PagerTemplate 内の DropDownList の選択を変えると、
SelectedIndexChangedは確実に呼ばれる - しかし GridView の表示は変わらず、常に 1 ページ目のデータが表示され続ける
- GridView の
DataBoundが「もう一度呼ばれるはず」と思っていたが、呼ばれない - ページ数(
PageCount)や現在ページ(PageIndex)の値をデバッグすると、どこかで想定とズレている
結論から言うと、「ページ番号(PageIndex)を変更したあとに、データを再バインドしていない」ことが原因であるケースがほとんどです。SqlDataSource を使っていたときは暗黙に成立していた前提が、手動バインドでは成立しません。
原因の本質:SqlDataSource は“勝手に再バインド”してくれていた
SqlDataSource(DataSourceID)構成が「楽」なのは、ページ移動などの操作に合わせて データの再取得と再バインド を GridView/DataSourceControl 側が面倒を見てくれる点です。つまり開発者は UI をいじっても、裏側の再描画が自然に追従していました。
一方で、SqlDataAdapter などで取得して GridView.DataSource に入れる方式では、ページ移動=表示データの作り直しを自分でやる必要があります。ここが意外と盲点になります。
| 項目 | SqlDataSource(DataSourceID) | 手動バインド(DataSource → DataBind) |
|---|---|---|
| ページ移動時の再取得 | GridView が DataSourceControl を通じて選択処理を呼び出しやすい | 自分で DB から取得するか、キャッシュしたデータを取り出す必要がある |
| ページ移動時の再バインド | 内部で再バインドされることが多く、開発者の意識が薄くなりがち | 必ず自分で DataBind を呼ぶ(呼ばないと表示が更新されない) |
| PagerTemplate 内コントロール | 再バインドにより毎回作り直される(結果として整合が取りやすい) | 同様に作り直されるが、作り直すタイミングを自分で設計する必要がある |
| 「1ページ目から動かない」原因 | 起きにくい | ページ変更後に再バインドしない/再バインドの順序が逆 |
GridView 標準ページングの前提:ページ移動のたびに“作り直す”
GridView のページングは、根本的には次の思想でできています。
- 現在ページは
PageIndexが保持する - 表示内容は
DataBind()によって生成される - つまり PageIndex を変えたら、同じデータ(または該当ページのデータ)で再度 DataBind して初めて表示が変わる
PagerTemplate の DropDownList も同じです。DataBind のたびに Pager 行ごと作り直されるため、次の 2 点が必ず発生します。
- ページ一覧(DropDownList の Items)の再作成
- 現在ページ(SelectedValue / SelectedIndex)の復元
まず確認したい落とし穴:Page_Load で毎回 DataBind していないか
よくある失敗が「ページングや選択変更のイベントより先に、Page_Load で毎回 DataBind してしまう」パターンです。Web Forms のイベント順序では、Page_Load のあとに各種イベント(SelectedIndexChanged など)が動きます。つまり、Page_Load で無条件に DataBind すると、イベントで PageIndex を変える前の状態で GridView が作られてしまい、結果として “いつも同じ表示” になりやすいです。
| やりがち | 起きること | おすすめ |
|---|---|---|
Page_Load で毎回 LoadGrid(); | イベント前に描画が固まり、イベント後に再バインドがないと表示が更新されない | 初回のみ if (!IsPostBack) で LoadGrid() |
イベント内で PageIndex を変えるだけ | PageIndex の値は変わっても、表示データが更新されない | イベント内で LoadGrid() を呼んで再バインド |
一言でまとめると、「初回は Page_Load(IsPostBack で判定)」「ページ変更はイベント内で再バインド」が基本です。
解決の基本:PageIndex を変えたら必ず再バインドする
DropDownList でページを選んだ瞬間、あなたがやるべき処理は大きく 3 つです。
- 選択されたページ番号を取り出す
GridView.PageIndexに反映する- 同じ条件でデータを再取得(または取り出し)して
DataBind()する
さらに PagerTemplate の DropDownList は DataBind のたびに作り直されるので、DataBound などで Items を再構築し、現在ページの選択状態を戻します。
実装の考え方:処理のタイミングを“イベント順”で押さえる
Web Forms のポストバックは「画面が一度消えて、サーバー側でコントロールツリーを復元し、イベントを処理し、最後に HTML を再生成する」という流れです。ページングが絡む場合は、次の表のどこで何をするかを意識すると迷いません。
| タイミング | ここで起きること | ここでやるべきこと |
|---|---|---|
| Page_Load | ページ表示の入り口。ポストバックか初回かが分かる | 初回のみ LoadGrid() を呼ぶ(if (!IsPostBack)) |
| 各種イベント(SelectedIndexChanged / PageIndexChanging など) | ユーザー操作に応じて発火 | PageIndex を更新し、必ず LoadGrid() を呼ぶ |
| GridView.DataBound | データバインドが完了し、PageCount が確定した状態 | PagerTemplate 内の DropDownList を Items から作り直し、選択状態を反映 |
特に PageCount は DataBind しないと確定しないため、DropDownList の Items を作るなら DataBound が安全です。
ASPX の例:PagerTemplate に DropDownList を置く
まずは PagerTemplate 側の最小構成です。ポイントは AutoPostBack="True" と、イベントハンドラの指定です。
<asp:GridView ID="myGridView" runat="server"
AllowPaging="True"
PageSize="20"
AutoGenerateColumns="False"
OnDataBound="myGridView_DataBound"
OnPageIndexChanging="myGridView_PageIndexChanging">
<Columns>
<asp:BoundField DataField="Id" HeaderText="ID" />
<asp:BoundField DataField="Name" HeaderText="名前" />
<asp:BoundField DataField="UpdatedAt" HeaderText="更新日" DataFormatString="{0:yyyy/MM/dd}" />
</Columns>
<PagerTemplate>
<span>ページ:</span>
<asp:DropDownList ID="PageDropDownList" runat="server"
AutoPostBack="True"
OnSelectedIndexChanged="PageDropDownList_SelectedIndexChanged" />
<span> (<asp:Label ID="CurrentPageLabel" runat="server" />)</span>
</PagerTemplate>
</asp:GridView>
AutoGenerateColumns を True/False にしても、ページングが動かない直接原因にはなりません。ここで重要なのは「ページを変えた後に、GridView を再バインドしているか」です。
C# の例:LoadGrid と Pager 再構築を分離して確実に動かす
次のコードは、手動バインドでページングを安定させる“王道パターン”です。
LoadGrid()に「取得→バインド」だけを集約する- ページ変更イベント(DropDownList / PageIndexChanging)では
PageIndexを更新してLoadGrid() DataBoundで PagerTemplate 内の DropDownList を再構築し、選択状態を復元する
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
myGridView.PageIndex = 0; // 初期ページ
LoadGrid();
}
}
private void LoadGrid()
{
// 1) DB から取得(例:DataTable)
var dt = GetData();
// 2) GridView にバインド
myGridView.DataSource = dt;
myGridView.DataBind();
}
// 例:SqlDataAdapter などで DataTable を作る(実装はプロジェクトに合わせてください)
private DataTable GetData()
{
// ここでは雛形だけ示します
var dt = new DataTable();
// TODO: SqlConnection/SqlCommand/SqlDataAdapter で dt を埋める
// 重要:ページングで表示順が変わらないように、SQL には ORDER BY を付ける
return dt;
}
// 標準のページャ(次へ/前へ)や、コードからの PageIndex 変更に対応
protected void myGridView_PageIndexChanging(object sender, GridViewPageEventArgs e)
{
myGridView.PageIndex = e.NewPageIndex;
LoadGrid();
}
// PagerTemplate 内の DropDownList を作り直す(DataBind のたびに呼ばれる)
protected void myGridView_DataBound(object sender, EventArgs e)
{
BindPagerDropDown();
}
// DropDownList でページを選んだとき
protected void PageDropDownList_SelectedIndexChanged(object sender, EventArgs e)
{
var ddl = (DropDownList)sender;
// Value に 0 始まりの PageIndex を入れておく前提
int newPageIndex;
if (!int.TryParse(ddl.SelectedValue, out newPageIndex))
return;
// 念のため範囲チェック(データ件数が変動する場合に安全)
if (newPageIndex < 0) newPageIndex = 0;
if (newPageIndex > myGridView.PageCount - 1) newPageIndex = myGridView.PageCount - 1;
myGridView.PageIndex = newPageIndex;
// ★最重要:PageIndex を変えたら必ず再バインド
LoadGrid();
}
private void BindPagerDropDown()
{
var pagerRow = myGridView.BottomPagerRow ?? myGridView.TopPagerRow;
if (pagerRow == null) return;
var ddl = pagerRow.FindControl("PageDropDownList") as DropDownList;
var lbl = pagerRow.FindControl("CurrentPageLabel") as Label;
if (ddl == null) return;
ddl.Items.Clear();
// PageCount は DataBind 後に確定する
for (int i = 0; i < myGridView.PageCount; i++)
{
int pageNumber = i + 1;
// 表示は 1 始まり、Value は 0 始まりの PageIndex にすると混乱しにくい
ddl.Items.Add(new ListItem(pageNumber.ToString(), i.ToString()));
}
// 現在ページを反映
ddl.SelectedValue = myGridView.PageIndex.ToString();
if (lbl != null)
{
lbl.Text = string.Format("{0} / {1}", myGridView.PageIndex + 1, myGridView.PageCount);
}
}
この形にしておくと、次のメリットがあります。
- 「どのイベントで再バインドすべきか」が明確になる
- PagerTemplate のコントロール再生成に引きずられず、常に現在ページが正しく表示される
- DropDownList だけでなく、標準のページャ(次へ/前へ)も同じロジックで動く
「DataBound が呼ばれない」本当の意味
質問でよく出てくるのが「SelectedIndexChanged は動くのに DataBound がもう一度呼ばれない」という状況です。これは、ほぼ次のどちらかです。
- そもそも DataBind() を呼んでいない(PageIndex だけ変えて満足している)
- Page_Load の無条件 DataBind() がイベント前に走り、イベント後の再バインドがない
DataBound は “DataBind の結果として” 発火します。逆に言えば、DataBind を呼ばない限り DataBound は増えません。イベントの発火と DataBound の発火は別物なので、混同しないのが近道です。
よくある落とし穴とチェックリスト
同じ「1ページ目から動かない」でも、原因が複数重なっていることがあります。切り分けに使えるチェックリストをまとめます。
| 症状 | ありがちな原因 | 確認ポイント/対処 |
|---|---|---|
| DropDownList のイベントが発火しない | AutoPostBack が False、またはイベント未設定 | ASPX で AutoPostBack="True" と OnSelectedIndexChanged を確認。UpdatePanel 内ならトリガーも確認 |
| イベントは発火するが表示が変わらない | イベント後に DataBind していない | イベント内に LoadGrid()(再取得+DataBind)があるか確認 |
| FindControl で DropDownList が取れない | PagerRow が未生成/TopPagerRow だけ表示/ID違い | BottomPagerRow ?? TopPagerRow の両方を見る。ID は PageDropDownList と一致させる |
| ページ数が 1 のときだけおかしい | PageCount が 1 で Items が 1 つ、SelectedValue の不整合 | DataBound で必ず Items を作り直し、SelectedValue に PageIndex を設定 |
| 途中でデータ件数が変わると例外 | 現在の PageIndex が範囲外になる | PageIndex を 0~PageCount-1 に丸める(範囲チェック) |
| 並び順がページごとに変わる | SQL に ORDER BY がない(取得順が不定) | ページングの前提として、必ず安定した ORDER BY を付ける |
デバッグのときは、イベント内で以下の値をウォッチすると状況が一気に見えます。
myGridView.PageIndex(イベント前後でどう変わるか)myGridView.PageCount(DataBind 後に確定しているか)myGridView.Rows.Count(バインド結果が出ているか)- PagerRow が null かどうか(
BottomPagerRow/TopPagerRow)
運用上の注意:標準ページングは“全件取得→メモリで分割”になりやすい
GridView の標準ページングは、データソース側が「ページング対応の Select」を提供していない限り、いったん全件を取ってから サーバー側でページ分割して表示する構造になりがちです。件数が増えると、次のような問題が出やすくなります。
- ページをめくるたびに全件取得してしまい、DB と Web サーバーの負荷が跳ねる
- DataTable が大きくなり、メモリと GC の負荷が増える
- 同時アクセスが増えるとレスポンスが急に悪化する
| データ件数の目安 | おすすめ方針 | 理由 |
|---|---|---|
| 数百件程度 | 全件取得+GridView 標準ページングでも許容されることが多い | 実装が簡単で、体感速度も維持しやすい |
| 数千件以上 | SQL 側でページング(OFFSET/FETCH など)を検討 | ページサイズ分だけ取得でき、負荷が安定する |
| 検索条件が複雑/アクセスが多い | サーバー側ページング+インデックス設計(ORDER BY に効くキー) | 「速いページング」は SQL の設計が鍵になる |
SQL Server のページング例:OFFSET/FETCH を使って“必要な分だけ”取る
データ件数が大きい場合は、ページ番号とページサイズをパラメータとして SQL に渡し、該当ページだけを取得する方が安定します。SQL Server の一例を示します(実際のテーブル名・列名に置き換えてください)。
SELECT
Id,
Name,
UpdatedAt
FROM dbo.YourTable
WHERE IsDeleted = 0
ORDER BY UpdatedAt DESC, Id DESC
OFFSET (@PageIndex * @PageSize) ROWS
FETCH NEXT @PageSize ROWS ONLY;
この方式なら、GridView のページング操作ごとに取得する行数は PageSize 程度に抑えられます。総件数(ページ数)を出すには、別クエリで COUNT を取るか、要件に応じて COUNT(*) OVER() を使うなど工夫します。
キャッシュで DB 負荷を抑える場合の選択肢
「ページングするたびに同じ検索結果を DB から取り直すのがもったいない」という場合、取得結果を一時的に保持する手もあります。ただし保持場所によって特徴が違うので、安易に ViewState に詰め込むのはおすすめしません。
| 保持場所 | メリット | デメリット | 向いているケース |
|---|---|---|---|
| ViewState | 実装が手軽 | ページサイズが巨大化しやすい(通信量増) | 少量データで、どうしても再取得したくない |
| Session | クライアントへの転送が増えない | セッション数が増えるとメモリを圧迫 | 社内ツールなど、同時ユーザーが少ない |
| サーバー側ページング(SQL) | 最もスケールしやすい | 実装・設計が少し増える | 件数が多い/アクセスが多い/長期運用 |
まとめ:手動バインドにしたら“ページ変更後の再バインド”が必須
PagerTemplate の DropDownList が反応するのに 1 ページ目から動かない問題は、ほとんどの場合 「PageIndex を変えた後に DataBind していない」、または 「Page_Load の無条件 DataBind で順序が崩れている」ことが原因です。
- 初回は
if (!IsPostBack) LoadGrid(); - ページ変更(DropDownList / PageIndexChanging)では
PageIndexを更新してLoadGrid(); DataBoundで DropDownList の Items を作り直し、現在ページを反映
この 3 点を押さえておけば、SqlDataSource から手動バインドへ移行しても、PagerTemplate のページジャンプは安定して動作します。

コメント