Windows Forms 的 DataGridView 是否实现了真正的虚拟模式?

13

我有一个包含100万行数据的SQL表,未来还会增长。

用户要求以可排序的方式显示所有行,不分页。用户期望使用滚动条可以快速跳转到任意行和顶部底部。

我熟悉“虚拟模式”网格,它只显示整个数据的一个可见子集。 它们提供出色的UI性能和最小的内存需求(我甚至在多年前实现了一个应用程序使用这种技术)。

Windows Forms 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行可见。然后我可以快速滚动到列表底部而没有延迟。内存使用量始终保持在10MB。


如果你还没有看过这些文章,可以去看看:如何在Windows Forms DataGridView控件中实现虚拟模式 使用VirtualMode在DataGridView中分页数据 - GalacticJello
3个回答

17

我们刚刚有一个类似的要求,需要使用默认的DataGridView在我们的应用程序中显示任意的未索引的100万行以上的表格,并且需要达到“非常好”的性能。起初我认为这是不可能的,但经过足够的头脑风暴后,我们在 Reflector 和 .NET Profiler 上花费了几天时间,终于想出了一些非常有效的方法。虽然这很难做到,但结果是值得的。

我们解决这个问题的方式是创建一个实现ITypedListIBindingList接口的类(例如 LargeTableView),以管理从数据库异步检索和缓存信息。我们还创建了一个单独的PropertyDescriptor继承类(例如LargeTableColumnDescriptor),以从每个列检索数据。

DataGridView.DataSource属性设置为实现IBindingList接口的类时,它进入一种伪虚拟模式,与常规VirtualMode不同,当绘制每一行时(例如当用户滚动时),DataGridView访问IBindingList的索引器[]和每个列的PropertyDescriptor的相应的GetValue方法以根据需要检索值。这里不会触发CellValueNeeded事件。在我们的情况下,当访问索引器时,我们访问数据库,然后缓存该值,以便后续的重绘不会再次访问数据库。

我进行了类似的内存使用测试。 DataGridView确实分配了一个与列表大小相同(即1M行)的数组,但是数组中的每个项目最初都引用单个DataGridViewRow,因此内存使用量是可以接受的。 我不确定当VirtualMode为true时行为是否相同。

我们通过在GetValue方法中立即返回String.Empty来消除滚动卡顿。如果行未缓存,则异步执行数据库查询。完成异步请求后,您可以引发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

的翻译如下:该网页介绍了IBindingList接口,它是一个.NET Framework类库中的接口,提供了数据绑定功能。

我尝试复现您描述的实现,但遇到了几个问题。1)当我将Grid.DataSource设置为我的绑定列表时,它会立即请求计数和所有实体。2)我还无法在数据仍在获取时进行索引器调用。3)在数据仍在获取时更改排序顺序并使缓存失效会返回陈旧的结果。 - Firo
如果您尚未完成后台获取,请尝试返回计数为0。如果您想发送一些代码给我,我很乐意查看。 - Kevin McCormick

1

我认为该链接指向最新版本,而评论在那里不可见。它是以“这个示例似乎不能在 .NET 4.0 中工作”开头的评论吗? - Mathias F
我不确定了,但我猜是“.NET Framework 3.0”,不过“Visual Studio 2010”也很有趣。 - Firo

0
我认为可以...只要你坚持使用由虚拟模式行为触发的事件(例如CellValueNeeded),并且注意清除手动构建的缓冲区。我已经展示了大量数据,超过了100万条,没有任何问题。
我对Kevin McCormick的实现有点好奇,他使用基于ITypedList或任何IList相关接口实现的数据源。我猜这只是另一个抽象层,除了让用户或开发人员使用内部和透明的缓冲区来为DataGridView提供动力外,还在内部处理本机VirtualMode以显示您加载到缓冲区中的信息。
除了绕过虚拟模式的时尚方式之外,对我来说,DataGridView仅剩下的难题是RowCount限制:它仍然是Int32.Max。这可能是由于Winforms绘图系统的遗留问题...就像Winform控件的图像或每个大小成员(宽度、高度)一样...为什么不坚持使用UInt32类型呢?
我假设没有人看到过带有负尺寸的控件或图片,但是类型仍然不适合使用环境的上下文。

请看下面我的回答,如果你仍然被困扰,它可能会对你有所帮助,即使我猜想这个问题早已解决了。 https://stackoverflow.com/a/16373108/1906567


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接