DataGrid101:使用 Windows.Forms DataGrid
关于 Windows.Forms.DataGrid 用法的教程

引言
Visual Studio .NET 自带了一个名为 DataGrid
的现成网格控件。它支持数据绑定和许多便捷的功能,看起来是一个非常方便的控件。然而,一旦您开始使用它,您可能会发现它的用法有些笨拙,并且在许多实际情况下,甚至是令人费解的。本文旨在引导初学者掌握 DataGrid
的使用方法,包括:
- 复杂数据集和 OO 类层次结构的数据绑定
- 调整列和各种“样式”问题
- 构建可用的上下文菜单,根据位置进行响应
- 刷新网格外部更新的数据
测试用例
DataGrid
在处理单个表时非常易于使用。它的导航功能提供了非常炫酷(尽管我倾向于怀疑其有用性)的表关系视图。为了避免显而易见的问题,我设计了一个简单但实际的域,其结构和需求不适合 DataGrid
的简单绑定能力。我称这个域为 Cars
,并将在本文中用它来举例。
- 一辆
Car
有 3 个属性:licensePlate
(车牌号)、carType
(车型)和price
(价格)。 - 一种
CarType
有 2 个属性:name
(名称)和manufacturer
(制造商)。 - 一个
Manufacturer
有一个name
(名称)。
此域可在附带的 ZIP 文件中找到,有 3 种形式:
- 一个 Jet 数据库(Cars.mdb)
- 一个 XML 架构(ObjectCars\CarDataSet.xsd)
- 一组类文件(ObjectCars\Car.vb,CarType.vb, CarManufacturer.vb)
需求很简单:编写一个网格状的屏幕来管理汽车价格。上面提供了一个快照。
数据绑定
为什么 DataGrid
绑定会不足?DataGrid
将其 DataSource
定义为一个单独的 IList
,无论是 DataTable
还是对象 Array
(多个表会创建“导航”界面)。虽然通过遍历关系/引用可以轻松推断出一辆汽车的所有相关数据,但 DataGrid
列绑定不支持“点”表示法。换句话说,当绑定到 Car
对象列表时,我可以显示 Car.licensePlate
,但不能显示 Car.carType.name
。让我们来看 3 种可能的解决方案,每种都有其优缺点。
基于 JOIN 的数据绑定
在数据查询方面,没有什么比使用 SQL 更简单或更易于维护了。一个简单的 SQL join
语句可以将相关数据填充到一个我们使用的 DataTable 中。优点:速度快、简单、易于维护。请不要在此停止阅读,因为存在一些缺点……
- 虽然您轻松获得了要显示的数据,但更新现在变得很麻烦:您需要“打破”回到原始表结构,然后才能更新数据库。
- 在三层架构场景中,要求服务器为屏幕专门定制的数据结构存在一些根本性的问题。在您不拥有服务器的情况下,这不仅是错误的,而且是不可能的。
- JOIN 所固有的数据冗余(相同数据存在于多行中)在行创建时容易出错。
这种方法的示例可以在附带的 ZIP 文件中找到:SingleQueryCars\SQLJoinBasedForm.vb。
多表数据绑定
Microsoft 的教程强调 DataSet
存储复杂表结构的能力,这允许更高的客户端独立性并减少往返次数。一旦我们接受了这个理念,我们就需要客户端进行表连接:没有 SQL。不幸的是,.NET 不包含 Joiner
实用类,所以我们需要在代码中进行表连接。这个过程很简单:
- 定义一个新的
DataSet
,并在其中定义一个具有所需结构的表。 - 通过循环处理子表(在本例中为
Cars
)来填充它。
这是代码
Dim carRec As DataRow
Dim viewRec As DataRow
For Each carRec In MyCarsDataSet.Tables("Cars").Rows
viewRec = MyGridViewDataSet.Tables("CarView").NewRow
viewRec("license") = carRec("license")
viewRec("type") = carRec.GetParentRow("TypesCars"). _
Item("typeName")
viewRec("make") = carRec.GetParentRow("TypesCars"). _
GetParentRow("ManufacturerTypes").Item("ManufacturerName")
MyGridViewDataSet.Tables("CarView").Rows.Add(viewRec)
Next
本质上,这与我们在 SQL JOIN 中所做的相同。主要优点是客户端/服务器解耦,即无需为网格目的进行服务器端编码。最大的缺点是客户端性能和额外的过程代码。这种方法的示例可以在附带的 ZIP 文件中找到:JoinBasedCars\LoopBasedJoinForm.vb。
对象列表数据绑定
在许多情况下,最好将 DataSet
作为底层数据库的链接,并使用类层次结构(也称为“对象域”)执行数据操作。您的应用程序中的逻辑越多,这种方法就越能为您服务。此外,在某些情况下,对象模型就是我们所拥有的。例如,当我们的服务器坚持以对象形式提供数据时。由于不支持“点表示法”,我们如何显示除“根对象”(即汽车)之外的任何对象的属性?
解决方案很简单,虽然起初可能看起来有些笨拙:我们创建一个新类(通常称为“查看器”类),它封装了根对象并将所有需要的数据作为属性导出。例如,在我们的示例中,我们将编写一个 CarViewer
类,它封装一个 Car
对象并导出 licensePlate
、typeName
和 manufacturerName
属性。然后,我们将这些对象的 ArrayList
绑定到 DataGrid
。
事实证明,这个解决方案非常强大,因为它为我们提供了处理非平凡的用户界面相关代码的自然场所:例如计算属性、复杂用例(例如“与其他汽车交换车牌”)等。事实上,即使您拥有的是 DataSet
而不是对象域中的数据,通常也最好使用基于查看器的网格,而不是执行“过程式 JOIN”。
不出所料,作者并没有发明这个概念。它是对一个称为 MVC(Model-View-Controller)的著名范例的改编,欢迎您阅读互联网上提供的海量优秀文章。这种方法的示例可以在附带的 ZIP 文件中找到:ObjectCars\ViewerBasedForm.vb。
刷新网格
无论您如何执行数据绑定,如果您的应用程序显示动态数据,您总会在某个时候需要刷新网格。事实证明,这又是另一个被“不明确”化的简单任务。方法如下:
- 获取
DataGrid
的CurrencyManager
。 - 调用其
Refresh
方法。
注意 BindingContext
的参数。这是大多数人失败的地方。它应该是一个指向您绑定的确切对象的引用。
' Get currency manager
Dim cs As CurrencyManager = _
CType(MyDataGrid.BindingContext(MyCarsDataSet.Cars), _
CurrencyManager)
' Refresh
cs.Refresh()
对于所有 c#/c++/j# 用户:CType
是 VB.NET 的类型转换运算符。
格式化网格
既然我们已经绑定了所有相关数据,那么让它变得易于人类阅读是个好主意。网格格式化相对容易,但从新闻组中对它的讨论量来看,可以推断出 Microsoft 并未非常整洁地公开它。我将尝试梳理基础知识,并为更高级的主题提供一些提示。
基本列格式化
所有网格格式化都围绕着可以通过网格属性窗口访问的 TableStyles
集合。工作原理如下:
- 样式定义了大多数网格格式化属性,包括列格式(通过一个名为
GridColumnStyles
的集合)。 - 在任何给定时刻,网格都遵循一个根据样式
MappingName
属性选择的样式。
一旦您理解了这一点,许多基本任务就会变得非常简单。以下是一些示例:
- 列标题、宽度、读/写、控件类型(文本框/复选框)等都在列样式中定义。
- 列顺序由
GridColumnStyle
的顺序决定。 - 如果我们想“隐藏”一列,我们不将其映射到任何列样式。
唯一剩下的技巧是确定正确的映射名称。
- 当网格显示
DataSet
DataTable
时,使用架构中定义的表名。 - 当网格显示对象数据结构时,使用结构数据类型(即
ArrayList
)。
这些的示例可以在附带 ZIP 文件中提供的所有 3 个窗体中找到。当然,所有这些属性都可以在运行时访问,从而可以轻松实现诸如“重新排列列”、“隐藏列”等功能。
高级格式化
不幸的是,我们期望 DataGrid
具有的一些非常有用的功能不容易实现,并且需要更高级的编程。其中最主要的是:
- 使用文本框和复选框以外的控件的能力。
- 在单元格级别动态控制颜色和字体的能力。
一旦我们理解格式化的核心是 DataGridColumn
类,那么很明显,要实现高级格式化,我们需要以适合我们的方式对其进行扩展。可以在此处(网格中的组合框)找到关于此类工作的极佳提示。
上下文菜单
在实际应用程序中,网格通常会有多个上下文菜单:列标题菜单与单元格菜单不同;行标题菜单可能与两者都不同,有时菜单可能会受到选择区域的影响。尽管 DataGrid
只有一个 ContextMenu
,但管理此类行为已足够简单:
- 创建所有需要的菜单,可以在设计器中或动态创建。
- 编写一个
DataGrid.MouseDown
事件的处理程序。- 检查是否为右键单击。
- 计算点击的行/列。
- 根据上下文设置
DataGrid.ContextMenu
。
请注意,这是有效的,因为您的处理程序会在上下文菜单显示之前被调用。这里有一个示例,它仅在单击单元格时显示上下文菜单。它会存储单元格坐标供上下文菜单处理程序稍后使用。
Private Sub MyDataGrid_MouseDown(ByVal sender As Object, _
ByVal e As System.Windows.Forms.MouseEventArgs) _
Handles MyDataGrid.MouseDown
Dim hi As System.Windows.Forms.DataGrid.HitTestInfo
hi = MyDataGrid.HitTest(e.X, e.Y)
' Test if the clicked area was a cell.
If hi.Type = DataGrid.HitTestType.Cell Then
Me.MyDataGrid.ContextMenu = Me.GridContextMenu
Me.manipulatedRow = hi.Row
Me.manipulatedColumn = hi.Column
' of course I could have saved the whole "hi" structure.
Else
Me.MyDataGrid.ContextMenu = Nothing
End If
End Sub
附件源代码
好了,各位。附带的 ZIP 文件包含 3 个项目:
SingleQueryCars
是一个基于 SQL JOIN 的只读网格;它主要展示了列的自定义。JoinBasedCars
具有类似的功能,但它也演示了客户端连接多个 DataSet 表。ObjectCars
稍微有趣一些(因为我相信这在大多数情况下是最好的方法);它演示了所有讨论过的内容以及更多内容,包括:- 将数据集映射到对象域的简单方法。
- 查看器管理。
- 列格式化。
- 上下文菜单使用。
- 添加行、删除行、隐藏行。
摘要
市面上已经有几个 .NET 网格比 DataGrid
看起来要好得多,而且还会有更多。然而,如果使用得当,DataGrid
仍然可以为您提供一个可用的用户界面,并且绝对值得您进一步尝试。
历史
- 2003年5月15日 -- 发布了原始版本
- 2003年9月11日 -- 更新