我有一个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。
答案 0 :(得分:17)
我们只是有类似的要求,能够使用库存DataGridView
在我们的应用程序中以“非常好”的性能显示任意的,未编制索引的1M +行表。起初我认为这是不可能的,但是有足够的头部刮擦,我们想出了一些在花费数天时间倾注于Reflector和.NET Profiler之后运行良好的东西。 这很难做到,但结果非常值得。
我们解决此问题的方法是创建一个实现ITypedList
和IBindingList
的类(例如,您可以将其称为LargeTableView
)来管理异步检索和信息缓存来自数据库。我们还创建了一个PropertyDescriptor继承类(例如LargeTableColumnDescriptor
)来从每列中检索数据。
当DataGridView.DataSource
属性设置为实现IBindingList
的类时,它会进入与常规VirtualMode不同的伪虚拟模式,其中每行都是绘制(例如当用户滚动时),DataGridView访问IBindingList
上的索引器[]和每个列的GetValue
上的相应PropertyDescriptor
方法,以根据需要检索值。不会引发CellValueNeeded
事件。在我们的例子中,我们访问索引器时访问数据库,然后缓存该值,以便后续的重新绘制不会到达数据库。
我执行了类似的测试:内存使用情况。 DataGridView
确实分配了一个列表大小的数组(即1M行),但是数组中的每个项最初都引用一个DataGridViewRow,因此内存使用是可以接受的。当VirtualMode为true时,我不确定行为是否相同。 我们能够消除滚动延迟,如果没有缓存行,则立即返回String.Empty
方法中的GetValue
,然后异步执行数据库查询。当异步请求完成时,您可以引发IBindingList.ListChanged
事件以向DataGridView发出信号,表明它应该重新绘制单元格,除非这次从缓存中读取,这是可用的。这样,UI永远不会被阻塞,等待数据库调用。
我们注意到的一件事是,如果在将DataGridView添加到表单之前设置DataSource或虚拟行数,性能会明显提高 - 它会将初始化时间缩短一半。另外,请确保将行和列自动调整设置为None
,否则您将遇到其他性能问题。
旁注:我们在.NET应用程序中完成“加载”这样一个大表的方式是在SQL服务器上创建一个临时表,该表以所需的排序顺序列出主键以及IDENTITY(行号),然后为后续行请求保持连接。这自然需要时间来初始化(在相当快的SQL服务器上大约3-5s),但是在不了解可用索引的情况下,我们没有更好的选择。然后,在我们的ITypedList实现中,我们请求100行页面中的行,其中第50行是正在绘制的行,因此我们限制每次访问索引器时执行的查询数量,并且我们给出了外观在我们的应用程序中提供所有数据。
进一步阅读:
http://msdn.microsoft.com/en-us/library/ms404298.aspx
http://msdn.microsoft.com/en-us/library/system.componentmodel.ibindinglist.aspx
答案 1 :(得分:2)
答案 2 :(得分:1)
答案 3 :(得分:0)
我会说是的...只要你坚持虚拟模式行为触发的事件(例如CellValueNeeded),并且你要好好清理手工构建缓冲区。我已经显示了大量的数据,超过1M而没有大惊小怪。
我对使用基于ITypedList的DataSource或任何IList相关接口实现的Kevin McCormick的实现有点好奇。我想这只是另一个抽象层使用内部透明缓冲区,除了让用户或开发人员使用它来驱动DataGridView,但仍在内部处理本机VirtualMode以显示您已加载的信息缓冲液中。
除了解决虚拟模式的时尚方式之外,对我而言,DataGridView唯一存在的难题是RowCount限制:它仍然是Int32.Max。这可能是由于Winforms绘图系统的遗留问题...就像图像甚至每个尺寸成员,宽度,Winform控件的高度......为什么不坚持使用UInt32类型?
我假设没有人看过控件或具有负尺寸的图片,但这种类型仍然不适合使用环境。
在下面看到我的答案,如果你仍然遇到麻烦,可以帮助你,如果,我想它已经解决了很多年了。 https://stackoverflow.com/a/16373108/1906567