教程大纲

  1. 什么是 DataList? - 快速回顾
  2. 为什么 DataList 需要分页? - 核心痛点
  3. 分页实现的核心思想 - 两种主流方案
  4. 后端分页 (推荐用于生产环境)
    • 原理
    • 实现步骤
    • 代码示例 (ASP.NET Web Forms)
  5. 前端分页 (适用于数据量小或演示场景)
    • 原理
    • 实现步骤
    • 代码示例 (纯 JavaScript)
  6. 总结与最佳实践

什么是 DataList?

DataList 是 ASP.NET Web Forms 中一个功能强大的数据绑定控件,它以列表形式显示数据,比 Repeater 功能更多(支持内置的布局和样式),但比 GridView 灵活性稍差,它通过 DataSource 属性绑定数据源(如 DataTable, List<T>, DataSet 等),并使用模板(如 ItemTemplate, HeaderTemplate)来定义显示的样式。

datalist 分页教程
(图片来源网络,侵删)

它就像一个可以自定义样式的表格,用来展示数据库查询出来的数据。

为什么 DataList 需要分页?

当你从数据库查询大量数据时(查询成千上万条商品记录),一次性将所有数据加载到 DataList 中会带来严重问题:

  • 性能低下: 服务器需要消耗大量内存和 CPU 来处理和传输所有数据。
  • 页面加载缓慢: 用户需要等待很长时间才能看到页面。
  • 浏览器卡顿: 渲染一个包含成千上万个 DOM 元素的页面会导致浏览器响应迟钝。
  • 糟糕的用户体验: 用户需要在一个超长的页面中滚动查找,非常不便。

分页就是为了解决这些问题,它只显示当前页的数据(每页 20 条),并提供导航控件(上一页、下一页、页码)让用户可以轻松浏览其他页的数据。

分页实现的核心思想

分页的核心是 “只取当前页需要的数据”,围绕这个核心思想,主要有两种实现方案:

datalist 分页教程
(图片来源网络,侵删)
  • 后端分页: 在数据库查询层面就只获取当前页的数据,这是最标准、最高效的方式。
  • 前端分页: 一次性从数据库获取所有数据,然后在浏览器端用 JavaScript 进行分页显示,这种方式实现简单,但只适用于数据量非常小的场景。

方案一:后端分页 (推荐用于生产环境)

这是最专业、最常用的方法,我们利用 SQL 查询的 OFFSET-FETCH (SQL Server 2012+) 或 LIMIT-OFFSET (MySQL) 语法,让数据库只返回我们需要的数据。

原理

  1. 用户请求第一页: 服务器执行 SELECT TOP 20 * FROM Products 这样的查询。
  2. 用户点击“下一页”: 服务器执行 SELECT 20 rows FROM Products WHERE ID > last_seen_id 或者使用 OFFSET 20 FETCH NEXT 20 ROWS ONLY 这样的查询。
  3. 关键点:
    • 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);
            }
        }
    }
}

方案二:前端分页 (适用于数据量小或演示场景)

这种方法更简单,因为它把分页的逻辑都推给了浏览器。

原理

  1. 页面首次加载时,通过一次 AJAX 请求或页面加载事件,一次性获取所有数据
  2. 数据以 JSON 格式返回到前端。
  3. 使用 JavaScript(Lodash 库的 _.chunk 方法,或自己写逻辑)将大数据数组分割成多个小数组(每个小数组代表一页)。
  4. 根据当前页码,渲染对应的小数组到 DataList 中。
  5. 分页按钮的点击事件只是触发 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)来管理整个应用状态。
  • 无论哪种方案,清晰的代码结构和良好的用户体验(显示当前页/总页数、禁用无效的导航按钮等)都是必不可少的。