我有一个 SQL 表,当前包含 100 万行,这些行会随着时间的推移而增长。
有一个特定的用户要求提供一个可排序的网格,显示所有行而不分页。用户希望能够通过使用滚动条非常快速地从行跳到行,从上跳到下。
我熟悉只呈现整体数据可见子集的“虚拟模式”网格。它们可以提供出色的 UI 性能和最小的内存要求,(我什至在多年前使用这种技术实现了一个应用程序)。
Windows 窗体 DataGridView 提供了一种看起来应该是答案的虚拟模式。然而,与我遇到的其他虚拟模式不同,它仍然为每一行分配内存(在 ProcessExplorer 中确认)。显然,这会导致整体内存使用量不必要地大幅增加,并且在分配这些行时,会有明显的延迟。滚动性能也受到超过 100 万行的影响。
真正的虚拟模式不需要为未显示的行分配任何内存。您只需给它总行数(例如 1,000,000),网格所做的就是相应地缩放滚动条。当它第一次显示时,网格只询问前 n(比如 30)行可见行的数据,即时显示。
当用户滚动网格时,会提供一个简单的行偏移量和可见行数,可用于从数据存储中检索数据。
下面是我当前使用的 DataGridView 代码示例:
public void AddVirtualRows(int rowCount)
{
dataGridList.ColumnCount = 4;
dataGridList.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.None;
dataGridList.AutoSizeRowsMode = DataGridViewAutoSizeRowsMode.None;
dataGridList.VirtualMode = true;
dataGridList.RowCount = rowCount;
dataGridList.CellValueNeeded += new DataGridViewCellValueEventHandler(dataGridList_CellValueNeeded);
}
void dataGridList_CellValueNeeded(object sender, DataGridViewCellValueEventArgs e)
{
e.Value = e.RowIndex;
}
我在这里遗漏了什么,还是 DataGridView 的“虚拟”模式根本不是虚拟的?
[更新]
看起来不错的旧 ListView 实现了我正在寻找的那种虚拟模式。但遗憾的是ListView 没有DataGridView 的单元格格式化功能,所以我无法使用它。
对于其他可能能够做到的,我使用四列 ListView(在详细模式下)、VirtualMode=True 和 VirtualListSize=100,000,000 行对其进行了测试。
该列表立即显示,前 30 行可见。然后我可以毫不延迟地快速滚动到列表的底部。内存使用量始终为 10 MB。
最佳答案
我们只是有一个类似的要求,即使用股票 DataGridView
能够在我们的应用程序中以“非常好”的性能显示任意的、未索引的 1M+ 行表。起初我认为这是不可能的,但是经过足够的挠头,我们在花了几天时间倾注 Reflector 和 .NET Profiler 之后想出了一些非常有效的东西。 这很难做到,但结果非常值得。
我们解决这个问题的方法是创建一个实现 ITypedList
和 IBindingList
的类(例如,你可以称之为 LargeTableView
)来管理从数据库中异步检索和缓存信息。我们还创建了一个继承 PropertyDescriptor 的类(例如 LargeTableColumnDescriptor
)来从每一列中检索数据。
当 DataGridView.DataSource
属性被设置为一个实现 IBindingList
的类时,它进入一个 伪虚拟模式 时,当用户绘制的索引不同于普通模式时,作为用户滚动的索引(如访问) ] 在每列的 IBindingList
上的 GetValue
和相应的 PropertyDescriptor
方法上,以根据需要检索值。未引发 CellValueNeeded
事件。在我们的例子中,我们在访问索引器时访问数据库,然后缓存该值,以便后续的重绘不会命中数据库。
我执行了类似的测试:内存使用。 DataGridView
确实分配了一个与列表大小相同的数组(即 1M 行),但是数组中的每个项目最初都引用单个 DataGridViewRow,因此内存使用是可以接受的。当 VirtualMode 为 true 时,我不确定行为是否相同。 如果行没有被缓存,我们能够通过立即在 String.Empty
方法中返回 GetValue
来消除滚动延迟 ,然后异步执行数据库查询。当异步请求完成时,您可以引发 IBindingList.ListChanged
事件以向 DataGridView 发出信号,表明它应该重新绘制单元格,但这次从缓存中读取,这是随时可用的。这样,UI 永远不会被阻塞等待数据库调用。
我们注意到的一件事是,如果您在 将 DataGridView 添加到表单之前设置 DataSource 或虚拟行数 ,则性能会显着提高 - 它会将初始化时间缩短一半。此外,请确保您将行和列自动调整大小设置为 None
,否则您将遇到其他性能问题。
旁注:我们在 .NET 应用程序中“加载”如此大的表的方式是在 SQL 服务器上创建一个临时表,该表以所需的排序顺序列出主键以及 IDENTITY(行号),然后为后续行请求保持连接。这自然需要时间来初始化(在相当快的 SQL 服务器上大约需要 3-5 秒),但是在不了解可用索引的情况下,我们没有更好的选择。然后,在我们的 ITypedList 实现中,我们以 100 行的页面请求行,其中第 50 行是正在绘制的行,以便限制每次访问索引器时执行的查询次数,并给出外观让我们的应用程序中的所有数据都可用。
进一步阅读:
http://msdn.microsoft.com/en-us/library/ms404298.aspx
http://msdn.microsoft.com/en-us/library/system.componentmodel.ibindinglist.aspx
关于c# - Windows 窗体 DataGridView 是否实现了真正的虚拟模式?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/2020819/