DataGridView不支持内置分页,必须将分页逻辑前置到数据库层,仅加载当前页数据;禁用AutoSizeMode避免重算行高;BindingSource“假分页”实为内存切片,会导致性能恶化。

DataGridView 本身不支持分页,硬塞几万行会卡死
别被“分页”俩字骗了——DataGridView 是个展示控件,不是数据库查询器。它没内置分页逻辑,所有数据加载进内存后才渲染。一旦你 DataTable.Load() 或 DataSource = list 塞入几万条,UI 线程直接卡住,滚动条拖不动,双击无响应,甚至触发 Windows 的“未响应”弹窗。
真正可行的路径只有一条:把分页逻辑前置到数据层,让 DataGridView 永远只看到当前页的几百条数据。
- 用
SqlDataReader+OFFSET FETCH(SQL Server 2012+)或ROW_NUMBER()分页查;MySQL 用LIMIT offset, size;PostgreSQL 同理 - 避免
SELECT * FROM huge_table全表加载再用 C# 切片——这等于把压力从 DB 转移到 UI 线程,更慢 - 禁用
AutoSizeMode为AllCells或DisplayedCells,否则每页刷新都会触发行高重算,卡上加卡
用 BindingSource 做“假分页”是典型误区
有人试过把全部数据一次性读进 List<T>,再用 BindingSource 的 Position 和 Count 模拟翻页——这根本不是分页,只是在内存里切数组。用户点“下一页”,你仍得遍历整个列表找下 50 条,且 DataGridView 还要重绘全部行(哪怕只显示其中几十行)。
现象很典型:BindingSource.MoveNext() 越往后越慢,内存占用飙升,GC 频繁,Ctrl+C 复制整列都延迟半秒以上。
-
BindingSource适合小数据( - 它的
Filter和Sort是客户端操作,数据量一大就变拖拉机 - 若真要用,至少配合
VirtualMode = true,但这时你得自己实现CellValueNeeded,反而绕回手动分页逻辑
推荐做法:服务端分页 + 手动刷新 DataSource
核心就是三件事:用户点“第3页”,发请求查第3页数据,清空旧 DataSource,赋新 DataTable 或 BindingList<T>。UI 层不碰原始数据集,只管展示。
关键细节藏在参数和时机里:
- 分页参数必须用
int pageIndex和int pageSize,别传“第几页”字符串,防止解析失败导致OFFSET 0无限循环 - 查完立刻设
dataGridView1.DataSource = null,再赋新值,否则旧引用残留可能引发ObjectDisposedException - 如果用了
DataGridViewTextBoxColumn自定义列,确保DataPropertyName和返回字段名完全一致(区分大小写),否则列空着不显示 - 启用
DoubleBuffered(需反射设置)可缓解翻页闪屏,但治标不治本;真正的流畅来自减少单次渲染量
var data = await LoadPageAsync(pageIndex: 3, pageSize: 50); dataGridView1.DataSource = null; dataGridView1.DataSource = data; // data 是 DataTable 或 List<Order>
滚动加载(Infinite Scroll)比页码更难搞,慎选
用户滚到底部自动加载下一页?听着高级,实操掉坑率极高。因为 DataGridView 没提供“滚动接近底部”的可靠事件,Scroll 事件触发太频繁,FirstDisplayedScrollingRowIndex 在虚拟模式下又不准。
常见崩法:RowsAdded 里再发请求,结果回调回来又触发 RowsAdded,无限递归;或者并发请求没控制,页面乱序插入。
- 真要上滚动加载,必须加防抖:记录上一次请求时间,间隔
- 禁用用户手动翻页控件(页码跳转),否则和滚动逻辑冲突
- 首次加载建议固定 100 行,而不是“尽可能多”,否则初始渲染就卡
- 最稳的方案仍是传统页码——用户明确知道“共多少页”,也方便跳转、打印、导出
分页的本质是限制数据流动的管道口径,不是给 UI 控件贴补丁。最容易被忽略的,是连数据库查询语句都没加 ORDER BY 就直接 OFFSET ——结果每翻一页顺序都变,用户以为数据丢了。










