早期 ASP 開發者習慣在頁面直接撰寫分頁邏輯。ASP.NET WebForms 雖有 `DataGrid`/`GridView` 內建分頁,但理解底層分頁算法對自訂查詢、大數據優化、API 分頁仍至關重要。本文補全原始碼片段,展示完整手寫分頁實作,並對比現代 ORM/LINQ 分頁寫法。
private int _pageSize = 20; // 每頁筆數
private int _pageCount; // 總頁數
private int _recordCount; // 總筆數
private int _currentPage = 1; // 當前頁碼 (1-based)
public void BindData()
{
// 1. 取得總筆數
string countSql = "SELECT COUNT(*) FROM Articles WHERE IsPublished=1";
_recordCount = Convert.ToInt32(SqlHelper.ExecuteScalar(countSql));
// 2. 計算總頁數
_pageCount = (_recordCount + _pageSize - 1) / _pageSize;
if (_currentPage > _pageCount) _currentPage = _pageCount;
if (_currentPage < 1) _currentPage = 1;
// 3. 建構分頁 SQL (SQL Server 2012+ OFFSET/FETCH)
string sql = @"
SELECT Id, Title, CreateTime, Summary
FROM Articles
WHERE IsPublished=1
ORDER BY CreateTime DESC
OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY";
var parameters = new[]
{
new SqlParameter("@Offset", (_currentPage - 1) * _pageSize),
new SqlParameter("@PageSize", _pageSize)
};
DataTable dt = SqlHelper.ExecuteDataTable(sql, parameters);
gridView.DataSource = dt;
gridView.DataBind();
// 4. 產生分頁導覽 HTML
litPager.Text = BuildPagerHtml();
}
private string BuildPagerHtml()
{
var sb = new StringBuilder();
sb.Append("");
// 首頁/上一頁
if (_currentPage > 1)
{
sb.Append($"首頁 ");
sb.Append($"上一頁 ");
}
// 頁碼區塊 (顯示前後各 2 頁)
int start = Math.Max(1, _currentPage - 2);
int end = Math.Min(_pageCount, _currentPage + 2);
for (int i = start; i <= end; i++)
{
if (i == _currentPage)
sb.Append($"{i} ");
else
sb.Append($"{i} ");
}
// 下一頁/末頁
if (_currentPage < _pageCount)
{
sb.Append($"下一頁 ");
sb.Append($"末頁 ");
}
sb.Append($"共 {_recordCount} 筆 / 第 {_currentPage}/{_pageCount} 頁");
sb.Append("");
return sb.ToString();
}
// 頁面載入時還原頁碼
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
_currentPage = Request.QueryString["page"] != null ?
int.Parse(Request.QueryString["page"]) : 1;
BindData();
}
}
| 資料庫 | 語法 | 適用版本 |
|---|---|---|
| SQL Server | `OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY` | 2012+ |
| SQL Server (舊) | `ROW_NUMBER() OVER (ORDER BY ...) AS RN` + 外層 `WHERE RN BETWEEN ...` | 2005-2008 R2 |
| MySQL / PostgreSQL / SQLite | `LIMIT @PageSize OFFSET @Offset` | 所有版本 |
| Oracle | `OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY` | 12c+ |
| Oracle (舊) | `ROWNUM` 子查詢雙層包裹 | 11g及之前 |
var query = _context.Articles.Where(a => a.IsPublished)
.OrderByDescending(a => a.CreateTime);
int total = await query.CountAsync();
var items = await query.Skip((page - 1) * pageSize).Take(pageSize).ToListAsync();
// 回傳 DTO
return new PagedResult
{
Items = items.Select(MapToDto).ToList(),
TotalCount = total,
Page = page,
PageSize = pageSize
};
string sql = @"SELECT * FROM Articles WHERE IsPublished=1 ORDER BY CreateTime DESC
OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY";
var items = await conn.QueryAsync(sql, new { Offset = (page-1)*pageSize, PageSize = pageSize });
int total = await conn.ExecuteScalarAsync("SELECT COUNT(*) FROM Articles WHERE IsPublished=1");
public class PagedResult
{
public List Items { get; set; }
public int TotalCount { get; set; }
public int Page { get; set; }
public int PageSize { get; set; }
public int TotalPages => (TotalCount + PageSize - 1) / PageSize;
public bool HasPrev => Page > 1;
public bool HasNext => Page < TotalPages;
}
手寫分頁核心在於 **`COUNT(*)` 取總數 + `OFFSET/FETCH` 取分頁資料 + 頁碼邊界檢查 + 導覽 HTML 生成**。現代開發請直接使用 **EF Core `Skip/Take` 或 Dapper 原生 SQL**,搭配統一 `PagedResult