ASP.NET 2.0 で利用できるデータソースコントロール(SqlDataSource など)とデータバインドコントロール(GridView など)を用いると、ほとんどコードを自力で書くことなくページング機能を実装できます。
ところが、GridView は意外と融通性がなく、例えば 1 レコードを複数行に表示する場合は実質的に使えません(使って使えないことはないですが、問題が多いです)。
ASP.NET 3.5 が使えなかった時代は、Repeater を使うという選択になったのですが、ページングが必要な場合は自分でコードを書いて実装する必要があります。その例を書いてみました。
実行結果は以下のようになります(縮小してあります)。
ASP.NET 3.5 の ListView を使えばこのような苦労はないので、今となってはこの例が役に立つことはないかもしれませんけど。
実装しなければならないのは以下のとおりです。
-
ページャー(ページングのためのユーザーインターフェイス用カスタムコントロール)
-
ページング制御しながらデータを抽出するストアドプロシージャ
-
ユーザーによるページャーの操作に応じて、ストアドプロシージャを利用して DataTable を作成し Reapter にバインドするプログラムを含む aspx ファイル。
それぞれについてコードを記載します。説明はコードの中のコメントを参照してください。
ページャー
using System;
using System.Web.UI;
using System.Web.UI.WebControls;
// 以下のようなページャーが表示される。
//
// <<最初 <前へ 1, 2, 3, 4, 5 次へ> 最後>>
//
// クリックするとポストバックがかかり、そのクリックした
// ページが表示されるようコントロールする。
namespace CustomWebFormsControls
{
public class PageNumberer : WebControl, IPostBackEventHandler
{
private int _selectedPage, _count, _displayedPages;
// 現在選択されているページ番号
public int SelectedPage
{
get
{
if (_selectedPage == 0)
{
object obj = ViewState["SelectedPage"];
_selectedPage = (obj != null) ? (int)obj : 1;
}
return _selectedPage;
}
set
{
ViewState["SelectedPage"] = value;
_selectedPage = value;
}
}
// ページ総数
public int Count
{
get
{
if (_count == 0)
{
object obj = ViewState["Count"];
_count = (obj != null) ? (int)obj : 1;
}
return _count;
}
set
{
ViewState["Count"] = value;
_count = value;
}
}
// ページャーに表示するページの最大数
// 例えば、DisplayedPages="7" と設定すると、Count が
// 8 ページ以上ある場合、以下のように表示される。
// 1, 2, 3, 4, 5, 6, 7 次へ> 最後 >>
public int DisplayedPages
{
get
{
if (_displayedPages == 0)
{
object obj = ViewState["DisplayedPages"];
_displayedPages = (obj != null) ? (int)obj : 1;
}
return _displayedPages;
}
set
{
ViewState["DisplayedPages"] = value;
_displayedPages = value;
}
}
// html の一番外側は div 要素。
protected override HtmlTextWriterTag TagKey
{
get
{
return HtmlTextWriterTag.Div;
}
}
protected override void RenderContents(HtmlTextWriter writer)
{
int startPage; // 左端に表示するページ番号
int endPage; // 右端に表示するページ番号
if (Count > DisplayedPages)
{
// SelectedPage の前後のページの数を求める。
// 例えば、
// 1, 2, 3, 4, 5, 6, 7 次へ> 最後 >>
// で 2 が SelectedPage の場合、prevCount = 1,
// nextCount = 5 となる。
int prevCount = Math.Abs((DisplayedPages - 1) / 2);
if (SelectedPage <= prevCount)
{
prevCount = SelectedPage - 1;
}
int nextCount = DisplayedPages - prevCount - 1;
if (SelectedPage + nextCount > Count)
{
nextCount = Count - SelectedPage;
prevCount = DisplayedPages - nextCount - 1;
}
// 上で求めた prevCount, nextCount を基に
// startPage, endPage を計算する。
startPage = SelectedPage - prevCount;
endPage = SelectedPage + nextCount;
}
else
{
startPage = 1;
endPage = Count;
}
if (startPage > 1)
{
RenderItem(writer, "<<最初", 1);
}
if (SelectedPage > 1)
{
RenderItem(writer, "<前へ", SelectedPage - 1);
}
for (int i = startPage; i <= endPage; i++)
{
string label;
if (i != endPage)
{
label = i.ToString() + ",";
}
else
{
label = i.ToString();
}
if (i == SelectedPage)
{
RenderItem(writer, label, 0);
}
else
{
RenderItem(writer, label, i);
}
}
if (SelectedPage < Count)
{
RenderItem(writer, "次へ>", SelectedPage + 1);
}
if (endPage < Count)
{
RenderItem(writer, "最後>>", Count);
}
}
void RenderItem(HtmlTextWriter writer, string text, int pageNum)
{
// <span>
writer.RenderBeginTag(HtmlTextWriterTag.Span);
// <a href="javascript:__doPostBack('ユニークID','ページ番号')">
if (pageNum != 0)
{
writer.AddAttribute(
HtmlTextWriterAttribute.Href,
Page.ClientScript.GetPostBackClientHyperlink(
this, pageNum.ToString())
);
writer.RenderBeginTag(HtmlTextWriterTag.A);
}
// ページ番号
writer.Write(text);
// </a>
if (pageNum != 0)
{
writer.RenderEndTag();
}
// </span>
writer.RenderEndTag();
}
private static readonly object EventSelectedPageChanged = null;
// これは += と -= の定義。
public event EventHandler SelectedPageChanged
{
add
{
Events.AddHandler(EventSelectedPageChanged, value);
}
remove
{
Events.RemoveHandler(EventSelectedPageChanged, value);
}
}
// サーバーにポストバックされたとき、ページがこの
// メソッドを呼び出して、__doPostBack() の第 2 引
// 数(クリックされたページ番号)を eventArgument
// に渡す。
void IPostBackEventHandler.RaisePostBackEvent(string eventArgument)
{
int newPage;
if (int.TryParse(eventArgument, out newPage))
{
this.SelectedPage = newPage;
OnSelectedPageChanged(EventArgs.Empty);
}
}
protected virtual void OnSelectedPageChanged(EventArgs e)
{
EventHandler changehandler =
(EventHandler)Events[EventSelectedPageChanged];
if (changehandler != null)
{
changehandler(this, e);
}
}
}
}
ストアドプロシージャ
Microsoft が提供しているサンプルデータベース Northwind の Products テーブルを利用しています。
ALTER PROCEDURE dbo.PagedProductList4
(
@pageNum int = 1,
@pageSize int = 10,
@pageCount int OUTPUT
)
/* pageNum: 現在選択されているページ番号
pageSize: ページ当りの行数
@rows: スキップする行数 */
AS
declare @rows int
declare @rowCount float
SET @rows = (@pageNum - 1) * @pageSize
SELECT @rowCount = COUNT(*) FROM [Products]
SELECT *
FROM (
SELECT *, ROW_NUMBER()
OVER (ORDER BY [ProductID] ASC) AS rownum
FROM [Products]
) AS DerivedTable
WHERE rownum BETWEEN (@rows + 1) AND (@rows + @pageSize)
ORDER BY [ProductID] ASC
SET @pageCount = CEILING(@rowCount / @pageSize)
aspx ファイル
<%@ Page Language="C#" %>
<%@ Register TagPrefix="MyControl" Namespace="CustomWebFormsControls" %>
<%@ Import Namespace="System.Data" %>
<%@ Import Namespace="System.Data.SqlClient" %>
<%@ Import Namespace="System.Configuration" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<script runat="server">
// ついでに、SqlDataSorce, ObjectDataSource は使わな
// いことで考えました。
DataTable CreateDataSource(int pageNum, int pageSize)
{
string connString =
ConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;
using (SqlConnection sqlConn = new SqlConnection(connString))
{
SqlCommand sqlCom =
new SqlCommand("dbo.PagedProductList4", sqlConn);
sqlCom.CommandType = CommandType.StoredProcedure;
sqlCom.Parameters.AddWithValue("@pageNum", pageNum);
sqlCom.Parameters.AddWithValue("@pageSize", pageSize);
SqlParameter param =
new SqlParameter("@pageCount", SqlDbType.Int);
param.Direction = ParameterDirection.Output;
sqlCom.Parameters.Add(param);
SqlDataAdapter adapter = new SqlDataAdapter();
adapter.SelectCommand = sqlCom;
DataSet ds = new DataSet();
adapter.Fill(ds);
pn1.Count = (int)param.Value;
pn2.Count = (int)param.Value;
return ds.Tables[0];
}
}
// ページ当りの行数
protected int pageSize = 7;
void Page_Load(Object sender, EventArgs e)
{
if (!IsPostBack)
{
Repeater1.DataSource =
CreateDataSource(pn1.SelectedPage, pageSize);
Repeater1.DataBind();
}
}
protected void pn1_SelectedPageChanged(object sender, EventArgs e)
{
Repeater1.DataSource =
CreateDataSource(pn1.SelectedPage, pageSize);
Repeater1.DataBind();
pn2.SelectedPage = pn1.SelectedPage;
}
protected void pn2_SelectedPageChanged(object sender, EventArgs e)
{
Repeater1.DataSource =
CreateDataSource(pn2.SelectedPage, pageSize);
Repeater1.DataBind();
pn1.SelectedPage = pn2.SelectedPage;
}
</script>
<html>
<head>
<title>無題のページ</title>
<style type="text/css">
table.style1
{
border-style: solid;
border-width: 2px;
border-color: Black;
text-align: center;
border-collapse: collapse;
}
table.style1 th
{
border-style: solid;
border-width: 2px 1px 2px 1px;
border-color: Black;
background-color: #6699FF;
color: #FFFFFF;
}
table.style1 td
{
border-style: solid;
border-width: 1px;
border-color: Black;
}
.alternate
{
background-color: #CCFFFF;
}
.PageNumbers
{
display:inline;
}
.PageNumbers span
{
padding-left: 10px;
}
</style>
</head>
<body>
<form id="form1" runat="server">
<div>
<MyControl:PageNumberer ID="pn1"
runat="server"
DisplayedPages="5"
onselectedpagechanged="pn1_SelectedPageChanged"
CssClass="PageNumbers"/>
<asp:Repeater ID="Repeater1" runat="server">
<HeaderTemplate>
<table class="style1">
<tr>
<th rowspan="2">ProductID</th>
<th colspan="7">ProductName</td>
<th rowspan="2">Discontinued</th>
</tr>
<tr>
<th>SupplierID</th>
<th>CategoryID</th>
<th>QuantityPerUnit</th>
<th>UnitPrice</th>
<th>UnitsInStock</th>
<th>UnitsOnOrder</th>
<th>ReorderLevel</th>
</tr>
</HeaderTemplate>
<ItemTemplate>
<tr>
<td rowspan="2"><%# Eval("ProductID")%></td>
<td colspan="7"><%# Eval("ProductName")%></td>
<td rowspan="2">
<asp:CheckBox ID="CheckBox1"
runat="server"
Enabled="False"
Checked='<%# Eval("Discontinued") %>' />
</td>
</tr>
<tr>
<td><%# Eval("SupplierID")%></td>
<td><%# Eval("CategoryID")%></td>
<td><%# Eval("QuantityPerUnit")%></td>
<td><%# Eval("UnitPrice")%></td>
<td><%# Eval("UnitsInStock")%></td>
<td><%# Eval("UnitsOnOrder")%></td>
<td><%# Eval("ReorderLevel")%></td>
</tr>
</ItemTemplate>
<AlternatingItemTemplate>
<tr class="alternate">
<td rowspan="2"><%# Eval("ProductID")%></td>
<td colspan="7"><%# Eval("ProductName")%></td>
<td rowspan="2">
<asp:CheckBox ID="CheckBox1"
runat="server"
Enabled="False"
Checked='<%# Eval("Discontinued") %>' />
</td>
</tr>
<tr class="alternate">
<td><%# Eval("SupplierID")%></td>
<td><%# Eval("CategoryID")%></td>
<td><%# Eval("QuantityPerUnit")%></td>
<td><%# Eval("UnitPrice")%></td>
<td><%# Eval("UnitsInStock")%></td>
<td><%# Eval("UnitsOnOrder")%></td>
<td><%# Eval("ReorderLevel")%></td>
</tr>
</AlternatingItemTemplate>
<FooterTemplate>
</table>
</FooterTemplate>
</asp:Repeater>
<MyControl:PageNumberer ID="pn2"
runat="server"
DisplayedPages="5"
onselectedpagechanged="pn2_SelectedPageChanged"
CssClass="PageNumbers"/>
</div>
</form>
</body>
</html>