教程大纲
- 什么是 DataList? - 快速回顾
- 为什么 DataList 需要分页? - 核心痛点
- 分页实现的核心思想 - 两种主流方案
- 后端分页 (推荐用于生产环境)
- 原理
- 实现步骤
- 代码示例 (ASP.NET Web Forms)
- 前端分页 (适用于数据量小或演示场景)
- 原理
- 实现步骤
- 代码示例 (纯 JavaScript)
- 总结与最佳实践
什么是 DataList?
DataList 是 ASP.NET Web Forms 中一个功能强大的数据绑定控件,它以列表形式显示数据,比 Repeater 功能更多(支持内置的布局和样式),但比 GridView 灵活性稍差,它通过 DataSource 属性绑定数据源(如 DataTable, List<T>, DataSet 等),并使用模板(如 ItemTemplate, HeaderTemplate)来定义显示的样式。

它就像一个可以自定义样式的表格,用来展示数据库查询出来的数据。
为什么 DataList 需要分页?
当你从数据库查询大量数据时(查询成千上万条商品记录),一次性将所有数据加载到 DataList 中会带来严重问题:
- 性能低下: 服务器需要消耗大量内存和 CPU 来处理和传输所有数据。
- 页面加载缓慢: 用户需要等待很长时间才能看到页面。
- 浏览器卡顿: 渲染一个包含成千上万个 DOM 元素的页面会导致浏览器响应迟钝。
- 糟糕的用户体验: 用户需要在一个超长的页面中滚动查找,非常不便。
分页就是为了解决这些问题,它只显示当前页的数据(每页 20 条),并提供导航控件(上一页、下一页、页码)让用户可以轻松浏览其他页的数据。
分页实现的核心思想
分页的核心是 “只取当前页需要的数据”,围绕这个核心思想,主要有两种实现方案:

- 后端分页: 在数据库查询层面就只获取当前页的数据,这是最标准、最高效的方式。
- 前端分页: 一次性从数据库获取所有数据,然后在浏览器端用 JavaScript 进行分页显示,这种方式实现简单,但只适用于数据量非常小的场景。
方案一:后端分页 (推荐用于生产环境)
这是最专业、最常用的方法,我们利用 SQL 查询的 OFFSET-FETCH (SQL Server 2012+) 或 LIMIT-OFFSET (MySQL) 语法,让数据库只返回我们需要的数据。
原理
- 用户请求第一页: 服务器执行
SELECT TOP 20 * FROM Products这样的查询。 - 用户点击“下一页”: 服务器执行
SELECT 20 rows FROM Products WHERE ID > last_seen_id或者使用OFFSET 20 FETCH NEXT 20 ROWS ONLY这样的查询。 - 关键点:
PageSize(每页大小): 固定值,20。PageIndex(当前页码): 从 0 或 1 开始。- 总记录数: 需要单独执行一次
SELECT COUNT(*) FROM Products来获取,用于计算总页数。 PagedDataSource类: ASP.NET 提供了一个非常方便的类PagedDataSource,它可以轻松地将一个数据源(如DataTable)进行分页处理,然后再绑定到DataList。
实现步骤
Step 1: 准备工作
假设你有一个 Products 表,并且已经配置好数据访问层。
Step 2: 创建分页用户控件 (可选但推荐)
为了复用分页逻辑,可以创建一个 UserControl (Pager.ascx),它包含页码按钮和上一页/下一页链接。
Step 3: 在 ASPX 页面中放置控件
<!-- YourPage.aspx -->
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="YourPage.aspx.cs" Inherits="YourApp.YourPage" %>
<%@ Register Src="~/Pager.ascx" TagPrefix="uc" TagName="Pager" %>
<!DOCTYPE html>
<html>
<head>DataList Pagination</title>
</head>
<body>
<form id="form1" runat="server">
<asp:DataList ID="dlProducts" runat="server" RepeatColumns="3" CellPadding="5">
<ItemTemplate>
<div style="border: 1px solid #ccc; padding: 10px; margin: 5px;">
<h3><%# Eval("ProductName") %></h3>
<p>Price: $<%# Eval("UnitPrice") %></p>
</div>
</ItemTemplate>
</asp:DataList>
<!-- 分页控件 -->
<uc:Pager ID="ucPager" runat="server" OnPageChanged="Pager_PageChanged" />
</form>
</body>
</html>
Step 4: 编写后端 C# 代码
这是最关键的一步,在 YourPage.aspx.cs 中,我们将实现数据绑定和分页逻辑。
// YourPage.aspx.cs
using System;
using System.Data;
using System.Data.SqlClient;
using System.Web.UI;
namespace YourApp
{
public partial class YourPage : Page
{
// 分页参数
private const int PageSize = 9; // 每页显示9条数据(3列 x 3行)
private int PageIndex
{
get { return ViewState["PageIndex"] != null ? (int)ViewState["PageIndex"] : 0; }
set { ViewState["PageIndex"] = value; }
}
private int TotalRecordCount
{
get { return ViewState["TotalRecordCount"] != null ? (int)ViewState["TotalRecordCount"] : 0; }
set { ViewState["TotalRecordCount"] = value; }
}
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
// 首次加载,绑定第一页数据
BindData();
}
}
private void BindData()
{
// 1. 获取总记录数
TotalRecordCount = GetTotalProductCount();
// 2. 计算总页数
int totalPages = (int)Math.Ceiling((double)TotalRecordCount / PageSize);
// 3. 配置并绑定 DataList
PagedDataSource pagedData = new PagedDataSource();
pagedData.DataSource = GetProductsPageData(); // 获取当前页的数据
pagedData.AllowPaging = true;
pagedData.PageSize = PageSize;
pagedData.CurrentPageIndex = PageIndex;
// 4. 绑定到 DataList
dlProducts.DataSource = pagedData;
dlProducts.DataBind();
// 5. 配置分页控件
ucPager.TotalRecords = TotalRecordCount;
ucPager.PageSize = PageSize;
ucPager.CurrentPage = PageIndex + 1; // 控件通常从1开始计数
}
// 从数据库获取当前页的数据
private DataTable GetProductsPageData()
{
string connectionString = "Your_Connection_String";
string query = "SELECT ProductID, ProductName, UnitPrice FROM Products ORDER BY ProductID";
// 使用 SqlDataAdapter 填充 DataTable
using (SqlConnection con = new SqlConnection(connectionString))
{
using (SqlCommand cmd = new SqlCommand(query, con))
{
// 使用 OFFSET-FETCH 进行后端分页 (SQL Server 2012+)
// 注意:PageIndex 从 0 开始
cmd.CommandText += $" OFFSET {PageIndex * PageSize} ROWS FETCH NEXT {PageSize} ROWS ONLY";
SqlDataAdapter da = new SqlDataAdapter(cmd);
DataTable dt = new DataTable();
da.Fill(dt);
return dt;
}
}
}
// 从数据库获取总记录数
private int GetTotalProductCount()
{
string connectionString = "Your_Connection_String";
string query = "SELECT COUNT(*) FROM Products";
using (SqlConnection con = new SqlConnection(connectionString))
{
using (SqlCommand cmd = new SqlCommand(query, con))
{
con.Open();
return (int)cmd.ExecuteScalar();
}
}
}
// 分页控件的回调事件
protected void Pager_PageChanged(object sender, EventArgs e)
{
// 更新当前页码
PageIndex = ucPager.CurrentPage - 1; // 转换回从0开始的索引
// 重新绑定数据
BindData();
}
}
}
分页控件代码 (Pager.ascx.cs)
// Pager.ascx.cs
using System.Web.UI;
namespace YourApp
{
public partial class Pager : UserControl
{
public int TotalRecords { get; set; }
public int PageSize { get; set; }
public int CurrentPage { get; set; }
// 定义一个事件,供父页面订阅
public event EventHandler PageChanged;
protected void Page_Load(object sender, EventArgs e)
{
// 首次加载时生成页码
if (!IsPostBack)
{
RenderPager();
}
}
private void RenderPager()
{
int totalPages = (int)Math.Ceiling((double)TotalRecords / PageSize);
string pagerHtml = "";
if (totalPages > 1)
{
pagerHtml += "<div class='pager'>";
// 上一页
if (CurrentPage > 1)
{
pagerHtml += $"<a href='javascript:__doPostBack(\"{this.UniqueID}\", \"{CurrentPage - 1}\")'>上一页</a> ";
}
// 页码
for (int i = 1; i <= totalPages; i++)
{
if (i == CurrentPage)
{
pagerHtml += $"<span class='current'>{i}</span> ";
}
else
{
pagerHtml += $"<a href='javascript:__doPostBack(\"{this.UniqueID}\", \"{i}\")'>{i}</a> ";
}
}
// 下一页
if (CurrentPage < totalPages)
{
pagerHtml += $"<a href='javascript:__doPostBack(\"{this.UniqueID}\", \"{CurrentPage + 1}\")'>下一页</a>";
}
pagerHtml += "</div>";
}
// 将生成的 HTML 写入 Literal 控件
ltrPager.Text = pagerHtml;
}
// 处理分页控件的回发事件
protected override void RaisePostBackEvent(IPostBackEventHandler source, string eventArgument)
{
CurrentPage = int.Parse(eventArgument);
OnPageChanged(EventArgs.Empty);
}
protected virtual void OnPageChanged(EventArgs e)
{
if (PageChanged != null)
{
PageChanged(this, e);
}
}
}
}
方案二:前端分页 (适用于数据量小或演示场景)
这种方法更简单,因为它把分页的逻辑都推给了浏览器。
原理
- 页面首次加载时,通过一次 AJAX 请求或页面加载事件,一次性获取所有数据。
- 数据以 JSON 格式返回到前端。
- 使用 JavaScript(
Lodash库的_.chunk方法,或自己写逻辑)将大数据数组分割成多个小数组(每个小数组代表一页)。 - 根据当前页码,渲染对应的小数组到
DataList中。 - 分页按钮的点击事件只是触发 JavaScript 函数来切换显示的数据页,不会与服务器交互。
实现步骤
Step 1: 修改 ASPX 页面
移除服务器端分页控件,添加一个用于显示分页的 div 和一个 Literal 控件来放分页按钮。
<!-- YourPage_FrontEnd.aspx -->
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="YourPage_FrontEnd.aspx.cs" Inherits="YourApp.YourPage_FrontEnd" %>
<!DOCTYPE html>
<html>
<head>DataList Frontend Pagination</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>
</head>
<body>
<form id="form1" runat="server">
<div id="productList">
<!-- DataList 将在这里被渲染 -->
<asp:DataList ID="dlProducts" runat="server" RepeatColumns="3" CellPadding="5">
<ItemTemplate>
<div class="product-item">
<h3><%# Eval("ProductName") %></h3>
<p>Price: $<%# Eval("UnitPrice") %></p>
</div>
</ItemTemplate>
</asp:DataList>
</div>
<div id="pager"></div>
</form>
<script>
$(document).ready(function () {
// 从 HiddenField 中获取所有数据
var allProducts = JSON.parse($('#<%= hfAllProducts.ClientID %>').val());
var pageSize = 9;
var currentPage = 1;
function renderPage(page) {
// 使用 Lodash 的 chunk 方法将数据分页
var pagedData = _.chunk(allProducts, pageSize);
// 清空 DataList
$('#<%= dlProducts.ClientID %>').empty();
if (pagedData[page - 1]) {
// 创建一个新的 DataTable 并绑定
var dt = new pagedData[page - 1].constructor();
// ... (这里需要手动构建 DataTable 并绑定,比较繁琐)
// 更简单的方式是直接用 JS 生成 HTML
// 更好的做法是服务器端只返回数据,前端用模板引擎渲染
// 为了演示,我们假设 DataList 已经被清空,我们直接操作其父容器
let html = '';
pagedData[page - 1].forEach(p => {
html += `
<div class="product-item">
<h3>${p.ProductName}</h3>
<p>Price: $${p.UnitPrice}</p>
</div>
`;
});
$('#productList').html(html);
}
renderPager(pagedData.length);
}
function renderPager(totalPages) {
let pagerHtml = '';
if (totalPages > 1) {
for (let i = 1; i <= totalPages; i++) {
if (i === currentPage) {
pagerHtml += `<span class="current">${i}</span> `;
} else {
pagerHtml += `<a href="#" onclick="goToPage(${i}); return false;">${i}</a> `;
}
}
}
$('#pager').html(pagerHtml);
}
window.goToPage = function(page) {
currentPage = page;
renderPage(currentPage);
};
// 初始渲染第一页
renderPage(currentPage);
});
</script>
</body>
</html>
Step 2: 编写后端 C# 代码
后端代码变得非常简单,只需要一次性返回所有数据。
// YourPage_FrontEnd.aspx.cs
using System;
using System.Web.Script.Serialization;
using System.Web.UI;
namespace YourApp
{
public partial class YourPage_FrontEnd : Page
{
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
// 获取所有产品数据
DataTable allProductsDt = GetAllProducts();
// 将 DataTable 序列化为 JSON 字符串
JavaScriptSerializer js = new JavaScriptSerializer();
string json = js.Serialize(allProductsDt);
// 将 JSON 存储在 HiddenField 中,供前端 JavaScript 读取
// 注意:对于大型数据集,这会影响页面加载性能
hfAllProducts.Value = json;
}
}
private DataTable GetAllProducts()
{
// 这里执行一个简单的查询,获取所有数据
string connectionString = "Your_Connection_String";
string query = "SELECT ProductID, ProductName, UnitPrice FROM Products ORDER BY ProductID";
// ... 使用 SqlDataAdapter 填充并返回 DataTable ...
// (代码与后端分页中的 GetProductsPageData 类似,只是不加 OFFSET-FETCH)
DataTable dt = new DataTable();
// ... 填充 dt ...
return dt;
}
}
}
注意: 前端分页的代码示例为了简化,直接用 jQuery 操作 DOM 生成 HTML,在实际项目中,更推荐使用模板引擎(如 Handlebars.js)或 Vue/React 等现代前端框架,这样代码会更清晰、更易于维护。
总结与最佳实践
| 特性 | 后端分页 | 前端分页 |
|---|---|---|
| 数据传输 | 每次只传输当前页的数据,数据量小 | 一次性传输所有数据,数据量大时性能差 |
| 服务器负载 | 低,数据库和服务器压力小 | 高,数据库和服务器压力大 |
| 用户体验 | 页面切换快,响应迅速 | 首次加载慢,但页面切换快(无刷新) |
| 实现复杂度 | 较高,需要处理 SQL 和服务器端状态 | 较低,主要处理前端逻辑 |
| 适用场景 | 几乎所有生产环境,特别是数据量大的情况 | 数据量小(几百条以内)、演示项目、或作为前端框架的快速原型 |
最终建议:
- 优先选择后端分页。 这是构建高性能、可扩展 Web 应用的标准做法。
PagedDataSource是一个强大的工具,应该熟练掌握。 - 仅在特定情况下考虑前端分页。 数据完全来自客户端、数据量非常确定且很小,或者你正在使用一个强大的前端框架(如 Vue/React)来管理整个应用状态。
- 无论哪种方案,清晰的代码结构和良好的用户体验(显示当前页/总页数、禁用无效的导航按钮等)都是必不可少的。
