65.9K
CodeProject 正在变化。 阅读更多。
Home

DataGrid101:使用 Windows.Forms DataGrid

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.70/5 (67投票s)

2003年5月15日

CPOL

8分钟阅读

viewsIcon

610933

downloadIcon

3204

关于 Windows.Forms.DataGrid 用法的教程

grid101.gif

引言

Visual Studio .NET 自带了一个名为 DataGrid 的现成网格控件。它支持数据绑定和许多便捷的功能,看起来是一个非常方便的控件。然而,一旦您开始使用它,您可能会发现它的用法有些笨拙,并且在许多实际情况下,甚至是令人费解的。本文旨在引导初学者掌握 DataGrid 的使用方法,包括:

  • 复杂数据集和 OO 类层次结构的数据绑定
  • 调整列和各种“样式”问题
  • 构建可用的上下文菜单,根据位置进行响应
  • 刷新网格外部更新的数据

测试用例

DataGrid 在处理单个表时非常易于使用。它的导航功能提供了非常炫酷(尽管我倾向于怀疑其有用性)的表关系视图。为了避免显而易见的问题,我设计了一个简单但实际的域,其结构和需求不适合 DataGrid 的简单绑定能力。我称这个域为 Cars,并将在本文中用它来举例。

  • 一辆 Car 有 3 个属性:licensePlate(车牌号)、carType(车型)和 price(价格)。
  • 一种 CarType 有 2 个属性:name(名称)和 manufacturer(制造商)。
  • 一个 Manufacturer 有一个 name(名称)。

此域可在附带的 ZIP 文件中找到,有 3 种形式:

  1. 一个 Jet 数据库(Cars.mdb
  2. 一个 XML 架构(ObjectCars\CarDataSet.xsd
  3. 一组类文件(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 中。优点:速度快、简单、易于维护。请不要在此停止阅读,因为存在一些缺点……

  1. 虽然您轻松获得了要显示的数据,但更新现在变得很麻烦:您需要“打破”回到原始表结构,然后才能更新数据库。
  2. 在三层架构场景中,要求服务器为屏幕专门定制的数据结构存在一些根本性的问题。在您不拥有服务器的情况下,这不仅是错误的,而且是不可能的。
  3. JOIN 所固有的数据冗余(相同数据存在于多行中)在行创建时容易出错。

这种方法的示例可以在附带的 ZIP 文件中找到:SingleQueryCars\SQLJoinBasedForm.vb

多表数据绑定

Microsoft 的教程强调 DataSet 存储复杂表结构的能力,这允许更高的客户端独立性并减少往返次数。一旦我们接受了这个理念,我们就需要客户端进行表连接:没有 SQL。不幸的是,.NET 不包含 Joiner 实用类,所以我们需要在代码中进行表连接。这个过程很简单:

  1. 定义一个新的 DataSet,并在其中定义一个具有所需结构的表。
  2. 通过循环处理子表(在本例中为 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 对象并导出 licensePlatetypeNamemanufacturerName 属性。然后,我们将这些对象的 ArrayList 绑定到 DataGrid

事实证明,这个解决方案非常强大,因为它为我们提供了处理非平凡的用户界面相关代码的自然场所:例如计算属性、复杂用例(例如“与其他汽车交换车牌”)等。事实上,即使您拥有的是 DataSet 而不是对象域中的数据,通常也最好使用基于查看器的网格,而不是执行“过程式 JOIN”。

不出所料,作者并没有发明这个概念。它是对一个称为 MVC(Model-View-Controller)的著名范例的改编,欢迎您阅读互联网上提供的海量优秀文章。这种方法的示例可以在附带的 ZIP 文件中找到:ObjectCars\ViewerBasedForm.vb

刷新网格

无论您如何执行数据绑定,如果您的应用程序显示动态数据,您总会在某个时候需要刷新网格。事实证明,这又是另一个被“不明确”化的简单任务。方法如下:

  1. 获取 DataGridCurrencyManager
  2. 调用其 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 集合。工作原理如下:

  1. 样式定义了大多数网格格式化属性,包括列格式(通过一个名为 GridColumnStyles 的集合)。
  2. 在任何给定时刻,网格都遵循一个根据样式 MappingName 属性选择的样式。

一旦您理解了这一点,许多基本任务就会变得非常简单。以下是一些示例:

  • 列标题、宽度、读/写、控件类型(文本框/复选框)等都在列样式中定义。
  • 列顺序由 GridColumnStyle 的顺序决定。
  • 如果我们想“隐藏”一列,我们不将其映射到任何列样式。

唯一剩下的技巧是确定正确的映射名称。

  • 当网格显示 DataSet DataTable 时,使用架构中定义的表名。
  • 当网格显示对象数据结构时,使用结构数据类型(即 ArrayList)。

这些的示例可以在附带 ZIP 文件中提供的所有 3 个窗体中找到。当然,所有这些属性都可以在运行时访问,从而可以轻松实现诸如“重新排列列”、“隐藏列”等功能。

高级格式化

不幸的是,我们期望 DataGrid 具有的一些非常有用的功能不容易实现,并且需要更高级的编程。其中最主要的是:

  • 使用文本框和复选框以外的控件的能力。
  • 在单元格级别动态控制颜色和字体的能力。

一旦我们理解格式化的核心是 DataGridColumn 类,那么很明显,要实现高级格式化,我们需要以适合我们的方式对其进行扩展。可以在此处(网格中的组合框)找到关于此类工作的极佳提示。

上下文菜单

在实际应用程序中,网格通常会有多个上下文菜单:列标题菜单与单元格菜单不同;行标题菜单可能与两者都不同,有时菜单可能会受到选择区域的影响。尽管 DataGrid 只有一个 ContextMenu,但管理此类行为已足够简单:

  1. 创建所有需要的菜单,可以在设计器中或动态创建。
  2. 编写一个 DataGrid.MouseDown 事件的处理程序。
    1. 检查是否为右键单击。
    2. 计算点击的行/列。
    3. 根据上下文设置 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 个项目:

  1. SingleQueryCars 是一个基于 SQL JOIN 的只读网格;它主要展示了列的自定义。
  2. JoinBasedCars 具有类似的功能,但它也演示了客户端连接多个 DataSet 表。
  3. ObjectCars 稍微有趣一些(因为我相信这在大多数情况下是最好的方法);它演示了所有讨论过的内容以及更多内容,包括:
    • 将数据集映射到对象域的简单方法。
    • 查看器管理。
    • 列格式化。
    • 上下文菜单使用。
    • 添加行、删除行、隐藏行。

摘要

市面上已经有几个 .NET 网格比 DataGrid 看起来要好得多,而且还会有更多。然而,如果使用得当,DataGrid 仍然可以为您提供一个可用的用户界面,并且绝对值得您进一步尝试。

历史

  • 2003年5月15日 -- 发布了原始版本
  • 2003年9月11日 -- 更新
© . All rights reserved.