news 2026/10/20 2:58:02

WinForm DataGridView分页控件封装:从原理到实战的完整指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
WinForm DataGridView分页控件封装:从原理到实战的完整指南

简介:面向WinForm开发者的DataGridView分页控件源码,由作者自行封装实现,主要解决C#桌面项目中DataGridView数据多、翻页不便的问题。控件使用比较简单,不需要额外依赖复杂库,适合有一定.NET基础、希望快速集成或学习分页实现逻辑的读者。RAR压缩包整体只有96KB,共包含45个文件;其中10个cs源文件负责核心逻辑,sln/csproj为Visual Studio工程组织文件,resx/resources用于界面资源配置,dll/exe为编译输出,pdb为调试符号,另有一些临时生成文件和项目配置文件。源码与可执行程序一并打包,既能直接运行体验,也可对照源码阅读。目前已有223人学习下载。包内目录结构清晰,分为分页控件主体、测试工程和公共属性配置;测试工程中带有窗体界面与程序入口,可以直观看到分页控件如何被创建、绑定数据以及触发翻页。整体封装层次比较完整,便于将控件提取到其他WinForm项目,也方便按需修改按钮样式、页码显示或数据源类型。

1. 为什么说 WinForm 的 DataGridView 分页控件值得自己做一套

DataGridView 是 WinForm 开发里用得最多的表格控件,但用过的人都知道:它天生不带分页。数据量一上去,直接绑定几千行,界面就开始卡顿,滚动条拖起来像在拉牛车。于是做企业管理系统的人基本都会遇到同一个需求:给自己项目里的 DataGridView 套一个分页控件。市面上的第三方控件要么收费,要么年头太久跟新环境不兼容,与其找轮子还不如自己封装一个,几十分钟就能做出来,而且完全可控。这个标题讲的就是这套东西,适合正在做进销存、后台管理、报表系统的 .NET 开发者,不管你是刚入门还是写了好几年,把分页控件的实现逻辑吃透,日常开发能省不少事。

分页控件听起来像个新功能,其实本质就一句话:把 DataGridView 绑定的数据源按页切块,一次只显示当前页大小的行数。真正需要设计的部分是页码导航、数据切块策略和与数据库的配合方式。接下来我会从控件骨架、分页原理、数据访问层写法、常见坑到进阶技巧全部过一遍,照着抄就能用。

2. DataGridView 分页控件的原理与选型:先搞懂分页切在哪里,再动手写控件

2.1 分页的本质:显示层切块还是数据库切块

先明确一个概念:分页到底是在哪里切的。很多人会误以为分页就是在 DataGridView 前面挡一道,把传进来的数据揉碎了展示,这种思路只适合数据量小的场景。真正要区分的是两个方向:内存分页和数据库分页。

内存分页指把全量数据一次性查出来放在 DataTable 或 List 里,然后用 Skip 和 Take 的 LINQ 语法按页取数。这种做法逻辑简单,写起来很快,但数据量超过几万行之后,取数本身和 UI 渲染都会变慢。数据库分页则是在 SQL 语句里直接写页码和每页大小,数据库只把需要的那一页返回给客户端。这才是几十万行数据的正解。

WinForm 分页控件你能不能做好,关键看你选择了哪一条路。一般我的建议是:控件本身做成一个用户控件,内部支持传入页码和页大小,至于数据是内存里切还是数据库里切,由调用方决定,控件只管把当前要显示的数据 Bind 到 DataGridView,并把分页状态(总页数、当前页、总记录数)显示在导航条上。

判定标准就一条:总行数在 5000 行以内,用内存分页完全可以接受,响应速度几乎无感;超过这个量级,请直接走数据库分页。不要在一开始就纠结哪个高级,在数据量没上来之前,内存分页的维护成本最低。

2.2 分页导航条的组成与用户习惯

写分页控件之前先想清楚导航条上要放哪些东西。我见过很多项目把导航条做得很花哨,有翻页动画、有页码下拉框,结果现场用户根本不会用。分页导航的核心就那么几样:首页、上一页、下一页、末页,中间至少要有数字页码,最后放一个当前页/总页数的文本显示和页码跳转输入框。这个组合是所有老管理系统里最常见的习惯,做好它,业务人员上手零成本。

有不少开发者会在这里犯一个错:把页码做成下拉框。看起来方便,实际用起来很别扭,因为用户往往记得自己要翻到第几页,却记不住页码是哪一个选项,输入框跳转反而更直接。

导航条的选型还可以细化到一个点:页面中间的页码按钮怎么排。比如一共 50 页,你不能把 1 到 50 全部排出来,对齐都会很痛苦。通用做法是显示有限个页码,比如当前页前后各扩展 2 个,首尾页固定显示,中间用省略号代替。这个逻辑看着简单,写的时候要注意边界:当前页在 1、2 页时,前面不能出现负数页码;当前页在倒数第二页时,后面扩展的页码不能超过总页数。这个细节是后面避坑章节的重点之一,别忽略。

2.3 为什么用 UserControl 而不是直接改写 DataGridView

有人在设计时可能会想:能不能直接继承 DataGridView,给它加一个分页属性?技术上能实现,但我强烈不建议。DataGridView 继承链上的职责已经很重,把分页导航揉进去会破坏原本的显示职责,后期维护非常痛苦。更好的做法是做一个独立的 UserControl,里面放一个 DataGridView 和导航条区域,对外提供属性、方法和事件,让调用方像使用普通控件一样使用它。

这个控件的对外接口我一般设计成这样:PageSize 属性控制每页行数;PageIndex 属性表示当前页码;TotalCount 字段用来记录总记录数;BindData 方法接收数据源;PageIndexChanged 事件在外面挂载翻页后的数据刷新逻辑。调用方不需要关心导航条内部是怎么生成页码的,只要告诉控件“我要从数据库查第 3 页的数据”,控件就会更新表格显示。

用 UserControl 还有个好处是按钮布局可以随项目风格调整,比如有的系统喜欢把翻页按钮放在表格上方,有的放在下方,你只需要改一个 UserControl 内部的布局,所有用了这个控件的地方都会跟着变,不用一个窗体一个窗体去改。

3. 写一个可复用的 WinForm DataGridView 分页控件:从属性设计到核心代码

3.1 控件的属性、事件与字段设计

动手写之前先把控件的类骨架搭好。我习惯把分页控件命名为 PagerControl,放在项目公共类库里,后续任何窗体都能引用。控件里核心的字段和属性如下。

public partial class PagerControl : UserControl { // 数据源与表格引用 private DataTable _dataSource; // 当前页要绑定的数据 private DataGridView _grid; // 目标表格控件引用 private int _pageIndex = 1; // 当前页码,默认从第 1 页开始 private int _pageSize = 20; // 每页显示行数,默认 20 行 private int _totalCount = 0; // 总记录数 // 对外事件:翻页时触发,用于让外部重新查询数据 public event EventHandler PageIndexChanged; // 属性:当前页码 public int PageIndex { get { return _pageIndex; } set { _pageIndex = value; } } // 属性:每页行数 public int PageSize { get { return _pageSize; } set { _pageSize = value; } } // 属性:总记录数,设置后自动刷新总页数显示 public int TotalCount { get { return _totalCount; } set { _totalCount = value; UpdatePageInfo(); } } // 属性:总页数(只读) public int PageCount { get { if (_pageSize <= 0) return 1; return (_totalCount + _pageSize - 1) / _pageSize; } } public PagerControl() { InitializeComponent(); } }

这段代码定义了分页控件的基本数据契约。TotalCount 属性赋值后会自动刷新页面信息,PageCount 的计算用的是向上取整公式,(totalCount + pageSize - 1) / pageSize 是经典写法,避免了 Math.Ceiling 的浮点数转换。PageIndexChanged 事件是控件与外部窗体沟通的桥梁,翻页按钮点击后只触发事件,真正的数据查询由外部窗体完成。

3.2 翻页按钮逻辑:首页、上一页、下一页、末页

导航条上放四个按钮,点击后的逻辑核心是边界判断。写这个逻辑时最容易翻车的地方是:当当前页已经在首页时,上一页按钮还能不能点。答案是不能,点了会导致 PageIndex 变成 0 甚至负数,查询直接出错。

private void btnFirst_Click(object sender, EventArgs e) { if (_pageIndex != 1) { _pageIndex = 1; UpdatePagerStatus(); OnPageIndexChanged(); } } private void btnPrev_Click(object sender, EventArgs e) { if (_pageIndex > 1) { _pageIndex--; UpdatePagerStatus(); OnPageIndexChanged(); } } private void btnNext_Click(object sender, EventArgs e) { if (_pageIndex < PageCount) { _pageIndex++; UpdatePagerStatus(); OnPageIndexChanged(); } } private void btnLast_Click(object sender, EventArgs e) { if (_pageIndex != PageCount) { _pageIndex = PageCount; UpdatePagerStatus(); OnPageIndexChanged(); } } private void OnPageIndexChanged() { PageIndexChanged?.Invoke(this, EventArgs.Empty); }

这段代码的关键点在于每个按钮触发翻页前都要先判断边界条件,判断不通过就什么都不做。btnFirst 和 btnLast 不是每次点击都有效,只有当前页和目标页相差时才更新状态。注意每次修改 _pageIndex 后都调用了 UpdatePagerStatus 方法,这个方法负责刷新按钮的可用状态和页码显示,稍后会给实现。OnPageIndexChanged 使用空传播运算符调用事件,外部窗体在 PageIndexChanged 事件里执行新的数据查询。

3.3 数字页码动态生成与省略号逻辑

中间的数字页码看着不难,写起来要注意边界情况。我的实现思路是:维护一个 List,把需要显示的页码全部算出来,排入列表,然后循环生成按钮。省略号不需要特殊对待,直接生成一个不可点击的 Label。

private List<int> GetPagerItems() { List<int> items = new List<int>(); int pageCount = PageCount; int current = _pageIndex; items.Add(1); // 首页永远显示 if (pageCount <= 7) // 总页数少时全显示 { for (int i = 2; i < pageCount; i++) items.Add(i); } else { // 当前页前后各扩展 2 页 int start = Math.Max(2, current - 2); int end = Math.Min(pageCount - 1, current + 2); if (start > 2) items.Add(-1); // -1 表示省略号 for (int i = start; i <= end; i++) items.Add(i); if (end < pageCount - 1) items.Add(-2); // 尾部省略号 } if (pageCount > 1) items.Add(pageCount); // 末页永远显示 return items; }

这个逻辑的精髓在于把页码和省略号统一抽象为一个整数列表,-1 和 -2 分别代表前部省略号和后部省略号。pageCount 大于 7 时才启用省略号逻辑,等于或小于 7 时全部展开。getPagerItems 不用去考虑按钮的实际创建和间距,只负责告诉外面“这一页应该显示哪几个页码”,职责单一,后续如果要改成下拉框模式也只需要替换这个方法的调用方。

创建按钮这一步就简单了,循环 items 列表,遇到负数就创建 Label 显示“…”,遇到正数就创建 Button 并绑定点击事件。按钮的点击事件里把 _pageIndex 改成该按钮对应的页码,然后刷新表格数据。

3.4 数据绑定方法与表格联动的完整链路

控件写到这里还差最后一步:把数据真正显示到 DataGridView 上。数据绑定方法一般由外部窗体调用,传入当前页的 DataTable。

public void BindData(DataTable dt, string gridName) { if (_grid == null) { _grid = this.Controls.Find(gridName, true)[0] as DataGridView; } _dataSource = dt; _grid.DataSource = null; _grid.DataSource = dt; UpdatePagerStatus(); } private void UpdatePagerStatus() { lblPageInfo.Text = string.Format("第 {0} 页 / 共 {1} 页", _pageIndex, PageCount); btnPrev.Enabled = _pageIndex > 1; btnNext.Enabled = _pageIndex < PageCount; btnLast.Enabled = _pageIndex < PageCount; btnFirst.Enabled = _pageIndex > 1; }

这段代码解决了两个问题:一是通过控件名字查找到外部窗体的 DataGridView,避免在设计期写死引用;二是先清空 DataSource 再重新赋值,否则 DataGridView 在绑定新数据源时会因为旧数据源中已有默认视图而出现刷新异常。BindData 方法接收的 dt 就是已经按页码切好的当前页数据,控件本身不做切分。

这里要提醒一句:在 UpdatePagerStatus 里,btnNext 和 btnLast 的判断条件要一致,btnFirst 和 btnPrev 的判断条件要一致。我在实际项目中见过有人把 btnLast 写成 _pageIndex < PageCount + 1,结果最后一页时按钮还亮着,点击后页码直接越界。边界判断的逻辑统一写在一个方法里,能避免这类问题。

4. 数据访问层的分页写法:SQL 端配合控件的三种方案与选择

4.1 先说清楚为什么客户端控件管不了大数据量

控件写得再好,数据访问层不配合也是白搭。如果你用内存分页,那当用户点下一页时,你重新把 10 万行数据从数据库查一遍,再在内存里 Skip 掉 20 行,耗时和带宽都受不了。所以当数据量明显超过 1 万行时,一定要让数据库只返回当前页数据。客户端分页控件此时扮演的角色就是“传参员”:把页码 PageIndex 和每页大小 PageSize 传给数据访问层,数据访问层把当前页的数据封装成 DataTable 返回,再交给 BindData 方法显示。

数据访问层的分页写法主要有三种:ROW_NUMBER 分页、OFFSET FETCH 分页、临时表分页。日常开发中前两种足够,第三种一般只在复杂统计报表中才用得到。下面逐个说明。

4.2 ROW_NUMBER 分页:适用范围最广的经典写法

ROW_NUMBER 是 SQL Server 2005 以后就有的功能,也是我见到最多老项目在用的分页方式。它的核心思路是先按排序字段给记录编号,再取出编号落在当前页范围内的记录。

-- 查询第 2 页,每页 20 行 DECLARE @PageIndex INT = 2; DECLARE @PageSize INT = 20; SELECT * FROM ( SELECT ROW_NUMBER() OVER (ORDER BY CreateTime DESC) AS RowNum, * FROM Product ) AS T WHERE RowNum BETWEEN (@PageIndex - 1) * @PageSize + 1 AND @PageIndex * @PageSize;

这里 ROW_NUMBER() OVER 的 ORDER BY 决定了分页的排序规则,业务上必须有一个确定的排序字段,否则会出现同一份数据在多次查询时顺序不一致的情况。BETWEEN 后面的两个表达式分别对应本页起始行号和结束行号,第 2 页 20 行时就是 21 到 40 行。这个写法的缺点是子查询会把全表数据都编号,数据量大时效率一般,但好在不需要额外建临时表,适合中大规模数据。

4.3 OFFSET FETCH 分页:简洁且性能稳定

OFFSET FETCH 是 SQL Server 2012 引入的语法,比 ROW_NUMBER 更简洁。它的逻辑更贴近人类的自然思维:跳过多少行,取多少行。

-- 查询第 3 页,每页 20 行 DECLARE @PageIndex INT = 3; DECLARE @PageSize INT = 20; DECLARE @OffsetRows INT = (@PageIndex - 1) * @PageSize; SELECT * FROM Product ORDER BY CreateTime DESC OFFSET @OffsetRows ROWS FETCH NEXT @PageSize ROWS ONLY;

OFFSET 后面是跳过的行数,(页号 - 1) 乘以每页行数正好是前面页数占用的总行数。FETCH NEXT 后面的值就是当前页要取的行数。这段 SQL 比 ROW_NUMBER 少了子查询层,在执行计划上往往更优。要注意的是 OFFSET FETCH 必须和 ORDER BY 一起使用,数据库不允许没有排序的分页操作,因为那样它自己也不知道该从哪一页取起。

我在项目里一般会用这个方案作为推荐项,前提是数据库版本不低于 SQL Server 2012。老版本数据库只能退回 ROW_NUMBER。

4.4 一次查出数据与总数:避免总页数更新不及时

分页控件显示总页数时需要知道总记录数,任何分页查询都绕不开这个问题。理想的做法是用一个批次执行两条 SQL,第一条查总记录数,第二条查当前页数据,不要分两次往返连接数据库。

public DataTable GetProductPage(int pageIndex, int pageSize, out int totalCount) { string sql = @" SELECT COUNT(*) FROM Product; SELECT * FROM Product ORDER BY CreateTime DESC OFFSET @OffsetRows ROWS FETCH NEXT @PageSize ROWS ONLY;"; using (SqlConnection conn = new SqlConnection(connectionString)) using (SqlCommand cmd = new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue("@OffsetRows", (pageIndex - 1) * pageSize); cmd.Parameters.AddWithValue("@PageSize", pageSize); conn.Open(); using (SqlDataReader reader = cmd.ExecuteReader()) { reader.Read(); totalCount = reader.GetInt32(0); // 第一条结果集是总数 reader.NextResult(); DataTable dt = new DataTable(); dt.Load(reader); // 第二条结果集是当前页数据 return dt; } } }

这段代码用了 SqlDataReader 的 NextResult 方法顺序读取两个结果集,避免开两次连接。totalCount 通过 out 参数传出,调用方拿到后直接赋给分页控件的 TotalCount 属性,总页数就自动刷新了。这里最关键的是必须先把 Reader 移到第一个结果集才读总数,然后 NextResult 跳过第一个结果集去读第二个,顺序一旦颠倒,第二个结果集会读不到。AddWithValue 在传参时的数据库类型推断可能不准确,如果遇到参数类型报错,建议改成 SqlParameter 显式指定 DbType。

5. 分页控件集成时的避坑与排查:五个高频事故的处理记录

5.1 页码越界导致数据库查询异常

现象:用户停留在第 10 页,此时有人在别的终端删除了大量数据,总记录数从 200 条降到 150 条,总页数从 10 页变成 8 页。用户再点下一页,控件仍然把 PageIndex 传到数据层,SQL 里的 OFFSET 计算出来的偏移量超过总行数,数据库不报错但返回空 DataTable,界面上表格什么都看不到。更严重的情况是,有些封装的数据访问层在 offset 超过总行数时会抛出异常。

原因:分页控件内部的 PageCount 是根据外部传进来的 TotalCount 计算的,但用户在翻页过程中,数据源的真实行数可能因为多端操作而发生变化,控件不知道这个变化。

解决:在每次翻页操作触发数据查询前,先重新查询一次总记录数,让 TotalCount 刷新,再判断当前 PageIndex 是否已经大于新的 PageCount。如果是,就把 PageIndex 重置为新的 PageCount,不执行下一页查询。

int freshTotal = GetTotalCount(); // 重新查询总数 pagerControl.TotalCount = freshTotal; if (pagerControl.PageIndex > pagerControl.PageCount) { pagerControl.PageIndex = pagerControl.PageCount; } pagerControl.OnPageIndexChanged(); // 实际项目中会封装成公开方法

这段逻辑放在外部窗体的翻页事件处理开头,而不是放在控件内部。因为只有外部调用方才清楚自己数据总数的来源。

5.2 筛选条件和分页状态不同步

现象:用户在查询条件里输入了“类型 = A”,查询结果有 300 条,共 15 页。用户翻到第 7 页,突然清空了筛选条件重新查询,结果有 500 条,共 25 页。此时分页控件还停留在第 7 页,但筛选条件和页码之间已经错位。更隐蔽的场景是:在筛选条件下翻到某一页,再点击查询按钮时,SQL 的 WHERE 条件变了但 OFFSET 还是按旧条件的页码计算的,返回的数据在逻辑上缺失了一段。

原因:分页状态是全局的,但筛选条件变化后,业务上应该重置分页状态,很多项目漏掉了这个重置动作。

解决:在所有筛选条件变化触发的查询方法里,统一加上两句:把 PageIndex 重置为 1,把 TotalCount 清空为 0,再执行查询。把这个重置动作写进一个公共查询方法,禁止在查询逻辑里直接绕过分页控件去取数。

5.3 跨线程更新 DataGridView 导致界面卡死或闪烁

现象:点击下一页按钮,UI 线程直接去数据库执行查询,查询耗时 3 秒以上。这期间界面无法拖动,鼠标在按钮上一直显示转圈图标,体验很差。有些项目为了避免卡顿,把查询放到后台线程,但直接在后台线程里赋值给 _grid.DataSource,抛出了跨线程操作异常,或者不报错但界面黑掉。

原因:WinForm 的控件必须在创建它的线程上更新,后台线程直接操作控件违反了这个规则。另外,即使是 UI 线程查询,长时间阻塞也会有界面冻结的问题。

解决:用 async/await 配合 Task.Run 把数据库查询放到线程池,查询完成后再通过 await 回到 UI 线程继续绑定数据。

private async void btnNext_Click(object sender, EventArgs e) { btnNext.Enabled = false; // 防止重复点击 int pageIndex = pagerControl.PageIndex; int pageSize = pagerControl.PageSize; DataTable dt = await Task.Run(() => GetProductPage(pageIndex, pageSize, out int totalCount)); pagerControl.TotalCount = totalCount; pagerControl.BindData(dt, "dataGridView1"); btnNext.Enabled = true; }

异步方法里查数据耗时再长也不会阻塞 UI 线程,用户至少可以拖动窗体。要注意 await 后面的代码已经回到 UI 上下文,所以 BindData 是安全的。还有一点:如果用户快速连续点击下一页,按钮要禁用,等查询完成后再恢复,否则会出现页码跳变,发出两个请求后第二次返回的数据覆盖了第一次,页面显示错乱。

5.4 DataTable.AsEnumerable 在 .NET Framework 下缺引用

现象:按照网上的内存分页教程写了 var rows = dt.AsEnumerable().Skip(...).Take(...).CopyToDataTable(),编译时却报错说 DataTable 没有 AsEnumerable 方法。

原因:AsEnumerable 扩展方法定义在 System.Data.DataSetExtensions 程序集里,.NET Framework 项目默认没有引用这个程序集。

解决:在项目引用里添加 System.Data.DataSetExtensions,或者干脆避开这个方法,直接用 DataTable.Select 加循环,或者用 CopyToDataTable 之前确保 using System.Data。如果是 .NET Core / .NET 5+ 项目,这个问题几乎不存在,因为框架已经默认包含。还有一种规避方案是不要用 AsEnumerable,改用 DataView 的方式过滤,但 DataView 的排序逻辑又得自己写,代码量上去了。所以我一般会优先加引用,保留 LINQ 写法的简洁性。

5.5 最后一页只剩少量数据时显示异常

现象:总共 101 条数据,每页 20 条,总页数是 6。点第 6 页时只应显示 1 条数据,但界面偶尔会显示 21 条,或者出现一条空白行。排查半天发现是内存分页的 Take 方法参数写成了固定页大小,没有用总数减去前面页数的差值。

原因:内存分页时取当前页数据量的公式是:当前页行数 = min(PageSize, TotalCount - (PageIndex - 1) * PageSize)。如果直接用 PageSize 固定 Take,最后一条记录之后越界,LINQ 会返回空集合而不是自动纠正。

解决:在切分数据时写一个公共方法,内部同时计算这一页的实际行数,与数据库分页的 OFFSET FETCH 行为保持一致。数据库分页天然不会取多,因为 FETCH NEXT 在剩余行不足时按实际剩余行返回。内存分页必须手动模拟这个逻辑,否则最后一页显示空表格或错乱行。

6. 把分页控件做成通用组件:数据量大时的虚拟模式与查询联动技巧

分页控件写到这里已经能用了,但想让它在真实项目里扛得住更大的数据量,还可以再做两件事:启用 DataGridView 的 VirtualMode,以及把查询条件和分页导航做成一条链路。虚拟模式是什么?简单说,就是让 DataGridView 只有在显示某一行时才去数据源里取那一行的数据,而不是一次性把所有行都加载到内存。这对动辄几万行的表格有非常明显的效果,滚动条顺滑度提升一个档次。

启用虚拟模式需要在 DataGridView 上设置 VirtualMode = true,并实现 CellValueNeeded 事件。我的习惯是让分页控件对外提供一个 TotalRows 属性,数据访问层给控件传一个“取单行数据”的回调委托。当用户滚动时 DataGridView 请求第 100 行,回调就执行一次轻量查询或从缓存字典里取。注意 VirtualMode 和分页并不冲突:分页把总数据切成页,VirtualMode 保证每一页内的行加载不卡顿,两者叠加效果最好。

public void BindVirtualData(Func<int, DataRow> rowLoader, int totalRows) { _grid.VirtualMode = true; _grid.RowCount = totalRows; _grid.CellValueNeeded += (sender, e) => { DataRow row = rowLoader(e.RowIndex); if (row != null) e.Value = row[e.ColumnIndex]; }; }

这个回调式设计的好处是数据源本身不依赖 DataTable 全量加载,只要按行号取数。实际项目里 rowLoader 内部可以从缓存字典查找,也可以走一次索引查询。要注意的是 VirtualMode 下 DataGridView 不会自己维护 RowCount 之外的任何数据属性,所有取值逻辑全部落在 CellValueNeeded 里,所以必须保证这个事件始终被订阅。

查询联动是另一个实用技巧:把搜索框、查询按钮和分页控件放进同一个用户控件里。外部只管设置查询条件源,控件内部统一处理“点击查询时重置页码并刷新数据”的逻辑。这样业务窗体写代码时只需一行初始化,不会出现上一节说的筛选条件和分页状态不同步问题。

我在做一个库存查询模块时就踩过这个坑,当时一个查询条件改了但页码没重置,数据从第 11 页开始查,查了十分钟才发现问题。后来把所有查询入口统一收敛到一个方法,开头固定把 PageIndex 重置为 1,这个毛病再没出现过。希望帮到你,把自己的分页控件做扎实,后面维护老系统时真的能省下大量时间。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 2:57:17

3、内存 - 计算机品牌排行榜

基于2026年全球DRAM原厂产能、消费级内存模组市场占有率及用户综合口碑&#xff0c;当前电脑内存品牌排行榜如下&#xff0c;‌金士顿以全球领先的市占率稳居第一‌&#xff0c;一线电竞品牌在超频性能上表现突出&#xff0c;国产高性价比品牌近年市场份额增长迅猛。 1. 金士顿…

作者头像 李华
网站建设 2026/10/20 2:56:54

AI学术写作助手全流程解析:从选题到答辩的高效方案

刚拿到项目组内部的“虎贲等考 AI”试用权限时&#xff0c;我其实没抱太大期望。市面上打着“AI写作助手”旗号的工具太多了&#xff0c;绝大多数无非是套壳的文本生成器&#xff0c;生成的文字像模像样&#xff0c;但用过的人都知道&#xff0c;真正落到一篇完整的学位论文或期…

作者头像 李华
网站建设 2026/10/20 2:56:22

YOLOv11家具识别系统全栈开发实战:从模型训练到UI部署

1. 项目概述与整体设计思路1.1 项目核心需求拆解家具识别检测这个话题&#xff0c;你说它新&#xff0c;其实已经不算新了&#xff0c;但YOLOv11出来之后&#xff0c;很多人还拿它做老任务&#xff0c;这就有点浪费了。我接触到手头的这个项目时&#xff0c;第一个反应是&#…

作者头像 李华
网站建设 2026/10/20 2:55:49

基于深度学习的PCB缺陷检测系统实战:从YOLOv5训练到部署

1. 项目概述与需求拆解1.1 这个标题背后到底是什么我第一眼看到“基于深度学习的PCB板缺陷检测系统(源码lw部署文档讲解等)”这个标题时&#xff0c;其实是有点感慨的。因为它是一个非常典型的“课程设计/毕业设计/实训项目”式命名&#xff0c;几乎把买家最关心的东西全部塞进…

作者头像 李华
网站建设 2026/10/20 2:54:50

SIP.js 与 FreeSWITCH 实现 WebRTC 网页电话:呼入呼出、保持转接与避坑指南

简介&#xff1a;这份资源是面向WebRTC语音通信开发者的实战示例包&#xff0c;聚焦sipjs与FreeSWITCH的整合应用&#xff0c;解决网页端实现电话呼入、呼出、转移与保持等核心功能的落地问题。适合具备一定前端与SIP协议基础、希望快速验证软电话能力的开发者参考。压缩包共4个…

作者头像 李华
网站建设 2026/10/20 2:54:34

前端URL参数获取与Cookie操作实战:核心原理与常见坑

1. 一次真实的前端开发经历&#xff1a;从URL取参到Cookie操作事情是这样的。上周我在做一个内部运营系统的前端重构&#xff0c;有个页面要从列表页跳转到详情页&#xff0c;跳转时需要带上用户ID、来源渠道和回跳地址三个参数。后端同学直接在服务端渲染的模板里拼好了URL&am…

作者头像 李华