ASP.NET 內建 DataPager 僅支援實作 IPageableItemContainer 的控制項(如 ListView)。Repeater、GridView 預設不支援,需自訂控制項繼承並實作介面。
[ToolboxData("<{0}:DataPagerRepeater runat=server PersistentDataSource=true>{0}:DataPagerRepeater>")]
public class DataPagerRepeater : Repeater, IPageableItemContainer, INamingContainer
{
// IPageableItemContainer 介面屬性
public int MaximumRows => ViewState["MaximumRows"] as int? ?? -1;
public int StartRowIndex => ViewState["StartRowIndex"] as int? ?? -1;
public int TotalRows { get => ViewState["TotalRows"] as int? ?? -1; set => ViewState["TotalRows"] = value; }
public bool PersistentDataSource { get => ViewState["PersistentDataSource"] as bool? ?? true; set => ViewState["PersistentDataSource"] = value; }
public bool PagingInDataSource { get => ViewState["PagingInDataSource"] as bool? ?? false; set => ViewState["PagingInDataSource"] = value; }
public bool NeedsDataSource
{
get
{
if (PagingInDataSource) return true;
if (!IsBoundUsingDataSourceID && !Page.IsPostBack) return true;
if (!IsBoundUsingDataSourceID && !PersistentDataSource && Page.IsPostBack) return true;
return false;
}
}
// 事件
public event EventHandler TotalRowCountAvailable;
public event EventHandler FetchingData;
// 載入 ViewState 後重新繫結(可選)
protected override void LoadViewState(object savedState)
{
base.LoadViewState(savedState);
// 若需在 PostBack 還原資料來源,可在此處理
}
protected override void OnLoad(EventArgs e)
{
if (Page.IsPostBack && NeedsDataSource && FetchingData != null)
{
if (PagingInDataSource) SetPageProperties(StartRowIndex, MaximumRows, true);
FetchingData(this, null);
}
if (!IsBoundUsingDataSourceID && PersistentDataSource && ViewState["DataSource"] != null)
{
this.DataSource = ViewState["DataSource"];
this.DataBind();
}
if (IsBoundUsingDataSourceID) this.DataBind();
base.OnLoad(e);
}
// DataPager 呼叫設定分頁屬性
public void SetPageProperties(int startRowIndex, int maximumRows, bool databind)
{
ViewState["StartRowIndex"] = startRowIndex;
ViewState["MaximumRows"] = maximumRows;
if (TotalRows > -1 && TotalRowCountAvailable != null)
TotalRowCountAvailable(this, new PageEventArgs(startRowIndex, maximumRows, TotalRows));
}
protected override void OnDataPropertyChanged()
{
if (MaximumRows != -1 || IsBoundUsingDataSourceID) RequiresDataBinding = true;
base.OnDataPropertyChanged();
}
// 關鍵:Render 時只輸出當前頁資料
protected override void RenderChildren(HtmlTextWriter writer)
{
if (!PagingInDataSource && MaximumRows != -1)
{
foreach (RepeaterItem item in Items)
{
if (item.ItemType == ListItemType.Item || item.ItemType == ListItemType.AlternatingItem)
{
item.Visible = item.ItemIndex >= StartRowIndex && item.ItemIndex < StartRowIndex + MaximumRows;
}
}
}
base.RenderChildren(writer);
}
// 取得資料時計算總筆數
protected override IEnumerable GetData()
{
var data = base.GetData();
if (!PagingInDataSource && MaximumRows != -1 && data != null)
{
int count = 0;
foreach (var _ in data) count++;
ViewState["TotalRows"] = count;
if (!IsBoundUsingDataSourceID && PersistentDataSource) ViewState["DataSource"] = DataSource;
SetPageProperties(StartRowIndex, MaximumRows, true);
}
if (PagingInDataSource && !Page.IsPostBack) SetPageProperties(StartRowIndex, MaximumRows, true);
return data;
}
}
public class DataPagerGridView : GridView, IPageableItemContainer
{
private static readonly object EventTotalRowCountAvailable = new object();
public DataPagerGridView() { PagerSettings.Visible = false; } // 隱藏內建分頁
protected override int CreateChildControls(IEnumerable dataSource, bool dataBinding)
{
int rows = base.CreateChildControls(dataSource, dataBinding);
if (AllowPaging)
{
int totalRowCount = dataBinding ? rows : ((ICollection)dataSource).Count;
OnTotalRowCountAvailable(new PageEventArgs(StartRowIndex, MaximumRows, totalRowCount));
if (TopPagerRow != null) TopPagerRow.Visible = false;
if (BottomPagerRow != null) BottomPagerRow.Visible = false;
}
return rows;
}
void IPageableItemContainer.SetPageProperties(int startRowIndex, int maximumRows, bool databind)
{
int newPageIndex = startRowIndex / maximumRows;
PageSize = maximumRows;
if (PageIndex != newPageIndex)
{
bool cancelled = false;
if (databind)
{
var args = new GridViewPageEventArgs(newPageIndex);
OnPageIndexChanging(args);
cancelled = args.Cancel;
newPageIndex = args.NewPageIndex;
}
if (!cancelled)
{
PageIndex = newPageIndex;
if (databind) OnPageIndexChanged(EventArgs.Empty);
}
if (databind) RequiresDataBinding = true;
}
}
int IPageableItemContainer.StartRowIndex => PageSize * PageIndex;
int IPageableItemContainer.MaximumRows => PageSize;
public event EventHandler TotalRowCountAvailable
{
add => Events.AddHandler(EventTotalRowCountAvailable, value);
remove => Events.RemoveHandler(EventTotalRowCountAvailable, value);
}
protected virtual void OnTotalRowCountAvailable(PageEventArgs e)
{
var handler = (EventHandler)Events[EventTotalRowCountAvailable];
handler?.Invoke(this, e);
}
}
WYJ.Web.Controls.dll。<%@ Register Assembly="WYJ.Web.Controls" Namespace="WYJ.Web.Controls" TagPrefix="WYJ" %>
<WYJ:DataPagerRepeater ID="rptLeaveword" runat="server" PersistentDataSource="true">
<ItemTemplate>
<div class="item">
<h3><%# Eval("Title") %></h3>
<p><%# Eval("Content") %></p>
</div>
</ItemTemplate>
</WYJ:DataPagerRepeater>
<div class="pager">
<asp:DataPager ID="DataPager1" PagedControlID="rptLeaveword" runat="server" PageSize="10">
<Fields>
<asp:NextPreviousPagerField ShowFirstPageButton="True" FirstPageText="首頁" />
<asp:NumericPagerField CurrentPageLabelCssClass="current" />
<asp:NextPreviousPagerField ShowLastPageButton="True" LastPageText="尾頁" />
</Fields>
</asp:DataPager>
</div>
protected void lnkbtn10_Click(object sender, EventArgs e) { DataPager1.PageSize = 10; UpdateCss(lnkbtn10); }
protected void lnkbtn20_Click(object sender, EventArgs e) { DataPager1.PageSize = 20; UpdateCss(lnkbtn20); }
protected void lnkbtn30_Click(object sender, EventArgs e) { DataPager1.PageSize = 30; UpdateCss(lnkbtn30); }
void UpdateCss(LinkButton active)
{
foreach (var btn in new[] { lnkbtn10, lnkbtn20, lnkbtn30 })
btn.CssClass = btn == active ? "currentpagesize" : "";
}
預設模式會一次撈回所有資料再前端分頁,資料量大時效能差。設定 PagingInDataSource="true",並處理 FetchingData 事件:
protected void Page_Load(object sender, EventArgs e)
{
rptLeaveword.PagingInDataSource = true;
rptLeaveword.FetchingData += (s, args) =>
{
int start = rptLeaveword.StartRowIndex;
int max = rptLeaveword.MaximumRows;
// 只查詢當頁資料:SELECT ... OFFSET @start ROWS FETCH NEXT @max ROWS ONLY
var dt = GetPageData(start, max);
rptLeaveword.TotalRows = GetTotalCount(); // 必須設定總筆數
rptLeaveword.DataSource = dt;
rptLeaveword.DataBind();
};
}
| 方案 | 優點 | 缺點 |
|---|---|---|
| 自訂 IPageableItemContainer | 繼續用 Repeater/GridView 模板、DataPager 宣告式分頁 | 需維護自訂控制項、ViewState 巨大、不支援非同步 |
| 改用 ListView | 原生支援 DataPager、版面彈性高 | 需重寫模板、學習曲線 |
| 前端分頁 + Web API | 效能最佳、關注點分離、支援無限捲動 | 需前端框架、SEO 需額外處理 |
| ASP.NET Core TagHelper | 原生支援、強型別、可測試 | 需遷移至 Core |
透過實作 IPageableItemContainer,讓舊有 Repeater/GridView 也能搭配 DataPager 使用,適合不想大幅重構的維護型專案。新專案建議直接採用 ListView 或前後端分離架構。