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

类型化编程:类型化数据集 - 面向初学者及其他人士

starIconstarIconstarIconstarIconstarIcon

5.00/5 (7投票s)

2015 年 10 月 3 日

CPOL

18分钟阅读

viewsIcon

25972

downloadIcon

579

“类型化”的数据集是什么意思?以及如何使用它?

目录

  1. 引言
  2. 题外话 1:掌握 ObjectBrowser 及其用法
  3. 探索类型化数据集
  4. 规划的应用增强
  5. 类型化编码与类型化数据集
  6. 避免非类型化数据类型
  7. 跨窗体数据绑定
  8. 题外话 2:输入对话框
  9. 辅助项目
  10. 留给您的一个练习

简介

本文假定您已了解(或愿意查阅)之前的两篇文章

  1. 面向初学者的关系型数据模型 解释了什么是关系型模型,以及如何将其设计为类型化数据集
  2. 面向初学者的数据绑定 介绍了“四视图”作为通用数据呈现模式,并指导如何通过绑定控件到类型化数据集来创建这些视图

本文将涵盖四个主题

  1. 学习使用 ObjectBrowser 探索类型化数据集
  2. 利用已探索的内容(类型化数据集的类型化部分)为示例应用程序提供更丰富的用户体验
  3. 解决跨窗体数据绑定的难题
  4. 创建数据绑定的输入掩码对话框。

题外话 1:掌握 ObjectBrowser 及其用法

ObjectBrowser 可能是 Visual Studio 中被低估最多的工具。您可以在其中找到每个类的文档,以及它所有的属性、方法、事件、字段,并且您可以从一个类型浏览到另一个类型,还可以检查基类和已实现的接口。

在此处获取

记下快捷方式并牢记。我强烈建议经常打开 ObjectBrowser 并浏览代码文档 - 即使您认为自己已经知道所需的内容。

这里搜索 FileInfo

您知道 FileInfo 吗?如果知道,您知道它有五种方法可以打开文件进行读写吗?如果知道,您知道可能发生的所有异常吗?

您可以通过向前和向后导航来改进搜索结果

(工具提示的快捷方式是错误的,正确的应该是:(Alt 左/右))

这样做似乎很愚蠢,但这个假导航会显示 FileInfo 在框架系统中的位置

现在左侧您可以看到其他类似和相关的类,例如 DirectoryInfo, DrifeInfo, Path, Stream, StreamReader, BinaryReader 等可能有趣的内容。当然,您也可以通过超链接进行浏览。

另一种查阅文档的方式是直接从代码编辑器中:右键点击有问题的类成员,然后选择“转到定义”

(我也建议您记住快捷方式)
不幸的是,“转到定义”仅在 VB.Net 中有效。在 c# 中,它只会显示更原始的信息,与 ObjectBrowser 相比很不方便。C# 程序员必须像上面所示那样使用 OB 搜索。
(给 c# 程序员的提示:打开一个 Vb 项目,并使用 Vb-ObjectBrowser(即使是试用的)。它的搜索结果通常更有意义,例如,当搜索一个类时,并不是它所有的构造函数都会填充结果视图。此外,VB_OB 的成员视图可以一目了然地显示返回类型 - c#_OB 则不行。)

我再次用 FileInfo 的截图来打扰您,现在我已扩展了一些类的基类型,这可以一窥它们之间的亲缘关系以及实现的接口

适当的 OB 设置非常重要

我推荐“显示容器”、“显示基类”、“显示公共成员”。
在某些情况下,“显示继承的成员”也很有用,但通常会使显示过于拥挤,因此,如果您想查看继承的成员,最好导航到基类(如上面 FileInfo 截图所示)。

作为“浏览”设置,我推荐“我的解决方案” - 但也要注意许多其他可用的设置选项,特别是“自定义组件集”在某些情况下可能很有用。

一个重要的设置可以通过成员视图上的上下文菜单访问

“按成员类型分组”可以选择折叠不感兴趣的组,例如,如果您特别寻找某个属性或相关内容。

我再次拜托您:**_经常使用 ObjectBrowser_**。如果您还没有,请立即使用它,作为一项练习。
探索 List(Of T) 类的 .BinarySearch 功能。探索它的 Find()FindAll()FindIndex()FindLastIndex() 函数(和重载)以及它的 ForEach() 方法、InsertRange(),当然还有它的四种 Sort() 重载。
别忘了检查实现的接口,浏览它们,并弄清楚它们的用途。

再来一个提示:在 ObjectBrowser 中按 F1 可以打开 MSDN 上的目标文档。例如 .BinarySearch() 的 MSDN 代码示例可能很有启发性。

**_探索_** 这些成员:选择它们,阅读摘要,浏览参数类型(如果存在),并了解它们的工作原理。再说一遍 - 如果您还没有,请立即去做 - 我保证:这绝对不是浪费时间。
接下来找出 Array 类是如何实现相同功能的。
然后切换到 DateTime,探索添加内容到 DateTime 的 10 种 (!) 方法。并注意从日期减去的方法(四种),并注意它们的区别。查看日期解析/尝试解析的基础结构(9 个成员),并比较 IntegerDoubleTimeSpan 是如何完成的。
不要成为那种只知道 IntelliSense 说什么,然后谷歌搜索,最后在论坛里提问的程序员!X|

换句话说:您不能认为自己是“.Net 程序员”,只要您对如此基础的数据类型(即 List(Of T)ArrayStringIntegerDoubleDateTimeTimeSpan)如此不熟悉。

抱歉这个题外话太长了,但我必须确保 ObjectBrowser 被充分了解 - 否则我们将无法继续探索我们的类型化数据集。

探索类型化数据集

首先回顾一下我们在上一篇文章中开发的数据模型

它可以看作是邮件仓库数据模型的一部分,该模型代表客户如何订购商品:每个订单有许多订单条目,订单条目主要说明客户需要多少件哪种商品。

现在在 ObjectBrowser 中看看我们自己的应用程序

得益于我们的 OB 设置“显示容器”,我们自己的应用程序(作为一个容器)显示在左侧的顶级节点上。看:我自己的代码非常少:我只创建了四个不同的窗体,其中三个用作对话框。

相比之下,数据集设计器创建了 21 个类和五个委托。其创建的系统结构如下:
首先,它创建类型化数据集本身 - OrderDts。您可以看到(右下角),它继承自(非类型化的)System.Data.Dataset
所有其他内容都实现为嵌套类 - 因此,不是 ArticleDataTable,而是 OrderDts.ArticleDataTable(这是一个重要的区别!)。
这些嵌套类是我们设计过的实体的代码体现。
为每个实体生成:

  • 一个类型化的 DataTable 类
  • 一个类型化的 DataRow 类
  • 一个类型化的 ChangeEventArgs 类
  • 一个类型化的 EventHandler 委托

后两者目前既不复杂也不重要。
首先,仔细查看我们的 OrdersDts 成员上面,特别是属性:每个表都可以直接作为 OrdersDts 的类型化属性访问,即 Article, Category, Customer, Order, OrderEntry

之后,进入 OrderDataTable

重新查看数据集设计器中的 Order 实体,您会发现每个实体属性都生成了一个相应的数据列。
但更重要的是,如何将 OrderRows 添加到 OrderDataTable:有 3 种重载

  1. Sub AddOrderRow(row As OrderRow)(最上面的第一个成员)
    这个方法很不方便,因为它需要一个新创建且有效的 OrderRow。您可以通过 OrderDataTable.NewOrderRow() 函数创建这样一个对象,然后填充其属性,但如前所述:与另一个 AddOrderRow 重载相比,它有点繁琐且不安全。(但在某些情况下可能有用。)
    不幸的是,IntelliSense 总是将此成员显示为首选 - **_但要避免使用它_** - 选择第二个选项:
  2. Function AddOrderRow(parentCustomerRow As CustomerRow, OrderDate As Date, DeliveryDate As Date, ShipDate As Date, ShipCosts As Decimal) As OrderRow
    看起来要复杂得多,但事实恰恰相反:它提供了一种便捷的方式,让您填写构建有效 OrderRow 所需的所有内容。
    相信我:这种方法更方便,而且最重要的是更安全,因为您不可能忘记分配任何实体属性。
    请注意,它会将新的 OrderRow 添加到表中,同时还将它返回给调用者。这非常方便,因为代码经常需要继续处理刚刚添加的类型化 DataRows。
  3. 第三个 AddOrderRow() 重载无关紧要,它之所以出现,只是因为我在 Order 实体中添加了一个计算表达式 - 我稍后会回到这一点。

另外请注意,提供强大事件的是DataTable(DataRow 则不是)。这些事件(以及基类提供的一些其他事件)在您开发高级业务逻辑时可能至关重要,例如当您需要在更改/删除之前/之后做出反应时。

现在探索将填充我们 OrderDatatable 的特定 OrderRow

这里您可以看到每个实体属性都作为类型化属性体现:IDCustomerIDOrderDateShipDatePriceSumShipCosts
注意它们的强类型 - 基类 - DataRow - **1)** 只检索 Object 类型的**值,并且 **2)** 只能通过正确的字符串键访问。
(这两点结合起来是非类型化 DataRow 的主要代码异味 - 第三点异味是 **3)** 混合不同实体的数据行的风险)

然后特别注意 CustomerRow 属性:在代码中,您永远不需要访问任何外键 - 如果这样做,您就做错了。这是错误的,因为使用父行本身比使用(充其量只是一个愚蠢的数字的)外键总是更容易。
请记住这一点以供将来使用:**每个子行都知道其父行(s)**(回想关系型原理:一个实体可以有多个父项)。

同样仔细地注意 GetOrderEntryRows() 函数:**每个父行都知道其所有子行**。

接下来注意可为空设计的属性带来的繁琐影响:如前所述,当数据集设计器中配置了 AllowDbNull=True 时,您永远无法直接检索值。您必须始终先进行 IsNull 检查,而这些繁琐的 IsXyNull() 函数就是为此而设的。
(现在您也明白了 SetXyNull() 方法的用途。)
请注意,与上述情况相反,OrderDate 属性没有伴随这种不方便的“乱七八糟”的东西。
由于 OrderDate.AllowNull=False,这种迂回的做法是不必要的。

我来总结一下类型化数据集通常是如何组成的 - 有:

  1. 一个类型化数据集,继承自 System.Data.Dataset
  2. 每个实体都有一个特定的类型化 DataTable,继承自 System.Data.DataTable
  3. 每个实体都有一个特定的类型化 DataRow,继承自 System.Data.DataRow

这些生成的类及其成员提供了**_您所需的一切_**,以避免出现代码异味(如类型转换、字符串键访问及其所有组合)。
挑战在于实际使用它们,并严格避免使用它们的非类型化基类 - (我稍后会对此大加批判;))

(侧边提示:您也可以直接检查生成的代码:只需打开 OrderDts.Designer - 文件。但由于代码量巨大,这无法提供良好的概览。)

规划的应用增强

我们在第一篇文章中已经进行了关于使用类型化数据集进行编码的简短“手指练习”。现在我将提供一个更实用的示例,它将为示例应用程序实现一些用户体验增强。

上面的 Customer=>Order=>OrderEntry -(父子子)视图现在可以通过专门的 Order 编辑对话框添加或编辑订单。例如,在主窗体上双击会打开“编辑订单”对话框,并显示当前选定的订单。

“编辑订单”窗体的顶部是主窗体中选定订单的详细视图。下方是一个通用的 父子视图 Category=>Article,如上一篇文章中所介绍。但在这里,ArticleGrid 额外为用户提供了蓝色的“已订购”列,用户只需在此列中输入特定商品的订购数量。
这就是他订购的 :-D
请亲自查看,并满意地发现它比翻阅真实仓库目录来选择您想要的商品还要容易!:-D

为了支持这一点,我在数据集设计器中添加了两个 Article 列,它们是临时的:TemporaryCountTempSumPrice

在 OrderEdit 对话框打开之前,TemporaryCount 的值应从当前订单的 OrderEntries 填充,这些 OrderEntries 可能已经存在。
因此,它们作为对话框的默认值显示在蓝色列中,用户可以更改。
另一个新添加的列 Article.TempSumPrice 是一个计算,它计算价格总计如下:TemporaryCount * Price - 我希望不需要解释。
现在用户可以浏览所有类别的所有商品,要订购某个商品,他只需输入所需的数量。

类型化编码与类型化数据集

绝大部分工作仍然由数据绑定完成,但在打开和关闭 Order-Edit 对话框之前,仍然有一些逻辑需要处理。

  • 在打开之前,所选订单的每个 OrderEntry 都必须将其 .Count 值复制到其父 Article 的 .TemporaryCount 中。
  • 在关闭之后,每个 OrderEntry 要么将其父 Article 的 .TemporaryCount 复制回其 .Count
    或者 - 如果 .TemporaryCount 被更改为 0 - 删除 OrderEntry。
    此外,必须查询所有 .TemporaryCount 被从 0 更改为大于零的数字的 Article - 对于这些 Article,需要创建新的 OrderEntries。
    (顺便说一下 - 识别CRUD:创建、读取、更新、删除;))

我必须承认,实现这些目标不再是真正的程序员初学者级别的问题。
希望您对 vb.net/c# 语言的掌握程度足以理解扩展方法、匿名方法以及 LINQ 的一些基础知识。

代码

Private Sub LaunchOrderEditDialog()
   Dim rwOrder As OrderRow = bsCustomerOrder.At(Of OrderRow)()
   Dim orderEntries As OrderEntryRow() = rwOrder.GetOrderEntryRows
   For Each rwEntry In orderEntries                      'prepare TemporaryCounts
      rwEntry.ArticleRow.TemporaryCount = rwEntry.Count
   Next
   If bsCustomerOrder.EditCurrent(Of dlgOrder)() <> DialogResult.OK Then Return
   'process committed User-Input 
   For Each rwEntry In orderEntries
      Dim rwArt = rwEntry.ArticleRow
      If rwArt.TemporaryCount > 0 Then
         rwEntry.Count = rwArt.TemporaryCount
         rwArt.TemporaryCount = 0       'cleanup
      Else
         rwEntry.Delete()
      End If
   Next
   For Each rwArt In OrderDts.Article.Where(Function(rw) rw.TemporaryCount > 0)
      OrderDts.OrderEntry.AddOrderEntryRow(rwOrder, rwArt, rwArt.TemporaryCount)
      rwArt.TemporaryCount = 0       'cleanup
   Next
End Sub

我将逐行讲解

2)从适当的 BindingSource 获取当前选定的 OrderRow。
   BindingSource.At(Of OrderRow)() 是我扩展的一个功能,用于简化 BindingSource 的使用 - 基本上是 的快捷方式。

   DirectCast(DirectCast(bsCustomerOrder.Current, DataRowView).Row, OrderRow)

抱歉,由于某些原因,BindingSource 中包含的数据封装在 DataRowView 中,这使得访问包含的类型化 DataRows 非常麻烦。

3)使用前面“探索数据集”段落中提到的精彩的类型化 GetXYChildRows() 函数。

4 - 6)将每个 OrderEntry 的 .Count 属性复制到其父 Article 的 .TemporyCount
(利用了每个 OrderEntryRow 都知道其 Article 父项这一事实,如探索数据集中所述)。

7)一个真正的高级 BindingSource 扩展启动了 OrderInput 对话框(我稍后会讲到)。
    如果返回的 DialogResult 不是 OkLaunchOrderEditDialog() 将中止。

10)获取每个 OrderEntry 的 Article 父项。

11 - 16)更新 OrderEntry 的 .Count 或删除它。注意清理(第 13 行) - 这对于稍后查询新订购的 Article 至关重要。

18)查询所有 .TemporyCount>0 的 Article,如果还有的话。这些 Article 是用户新订购的。

19)使用前面“探索数据集”中提到的精彩的 AddXyRow() 函数创建新的 OrderEntries。

您看:在 20 行代码内,我们解决了一个非平凡的业务逻辑问题。
为此,我们总共使用了 14 次(如果我没数错的话)生成的类型化数据集成员。
所有访问都是类型安全的 - 不需要类型转换或字符串键异味。
只有对 BindingSource 的两次访问被视为非类型化的,因此它们需要额外的类型信息。(当然 - 因为 BindingSource 不是数据集设计器类型化代码生成的主题。)

题外话:避免非类型化数据类型

像瘟疫一样避开它们(实际上它就是!X|)
无论您在代码中的任何地方看到这三个词中的任何一个:Dataset, DataTable, DataRow - 您都做错了**_完全错了_**。
您有一个**_类型化_**的数据集,所以使用它 - 参见上面的代码:它处理三个不同的 DataTable 和五个不同的 DataRow,但 nowhere 出现了 DatasetDataTableDataRow
OrderDts 完成工作,以及它的类型化表:OrderDts.Article As ArticleDataTableOrderDts.OrderEntry As OrderEntryDataTable 等等。
不要退回到像这样的代码异味:

Dim tbOrder = OrderDts.Tables("Order")

您不会对它感到满意,因为它没有安全舒适的 AddOrderRow() As OrderRow 函数,并且它检索的行同样是非类型化的,这意味着:没有类型化成员,如 rw.CustomerRow,或 rw.GetOrderEntryRows() As OrderEntryRow(),甚至 rw.OrderDate As Date

进入这些有异味的代码 - 您将陷入泥潭,并继续处理类似以下的内容:

Dim orderEntries As DataRow() = rwOrder.GetChildRows("FK_Order_OrderEntry")

异味会持续下去。

再次:无论何时您的目光落在 Dataset, DataTable, DataRow 上 - 立即停止编码,打开 ObjectBrowser,并了解数据集设计器为您生成了哪些类型化的替代方案。

跨窗体数据绑定

您已经学会了将数据绑定视为一种魔力,可以通过同步控件来呈现数据模型数据。这意味着,所见即所得,并且不再可能出现您拥有一系列数据,而 Listbox 显示其他内容的情况。即使多个控件以不同的视图呈现相同的数据,使用一个控件更改它也会同时更改其他控件的呈现 - 它们是绑定在一起的。

这在单个窗体上效果完美,但我们通常需要多个窗体。例如,本文的示例应用程序需要四个窗体,即 frmMain 和三个对话框窗体,以支持用户安全、用户友好地编辑客户、商品和订单。

但是,在设计使用数据绑定的其他窗体时,我们会遇到一个严重的问题。
它们的控件绑定到其他类型化数据集实例 :wtf:

窗体设计器对此无能为力:将实体从“数据源”窗口拖到窗体上 - 它会生成一个类型化数据集,并像魅力一样配置绑定 :thumbsup:。对两个窗体这样做 - 您就有两个数据集 :thumbsdown:
这是一团糟(说得委婉点)。因为在运行时您打开 Form2 - 里面没有数据 :wtf:(Form1 中有)。
现在重复加载数据更糟糕,因为您可能在 Form1 中有一些更改,在 Form2 中有其他更改。实际上没有办法以令人满意的方式将两个版本保存回数据库。
数据并发冲突 - 数据库最糟糕的情况。

解决方案在于“高地人原则”:只有一个! ;)
整个应用程序只有一个类型化数据集 - 无论您如何实现。
是的,这很复杂,因为它需要更改每个 BindingSource 的连接,从第二个数据集更改到 Form1 的主数据集实例。

特别是这些更改连接的骚乱并不那么可怕,它只会影响顶级的 BindingSource - 其他连接到其他 BindingSource 的 BindingSource 会“继承”其父 BindingSource 的连接更改。
此外,您可以在设计器中重新组织所有绑定,并将它们连接到一个“主 BindingSource”,这样在运行时您只需更改一个 BS。

但总的来说,纯粹性和优雅性严重受损,因为那种拖放式绑定构建不再那么容易工作了。

通过反射进行的连接更改

我创建了一个解决方案,其中反射扫描整个窗体,并连接找到的每个 BindingSource。代码相当复杂,所以我将其封装在名为“Register()”的扩展方法中,现在留给您挑战,以正确导入我的辅助项目。
如果您做到了,您就可以继续按惯例构建绑定,并调用 Me.Register(myDataset) 来消除所有邪恶的并发类型化数据集,并将所有 BindingSources 连接到唯一且唯一的“高地人实例”

请检查一下:下载示例应用程序,启动它,点击菜单“打开另一个实例”,并在其中一个窗体中执行更改。
并享受其他窗体如何随之更新 :D。

题外话 2:输入对话框

还有一个 WinForms 功能,也鲜为人知,并且可能值得专门写一篇教程:即如何构建标准对话框。对话框通常提供以下功能:

  1. 打开前,可以设置默认数据
  2. 打开是模态的 - 意味着:用户必须显式关闭对话框才能返回主应用程序
  3. 用户可以输入数据或更改给定的默认值
  4. 可以取消输入
  5. 之后,输入的数据可供进一步处理

此模式与 WinForms 设计支持完美集成:您可以设计任何窗体,提供一个取消按钮和一个确定按钮,并设置它们的 .DialogResult 属性为 DialogResult.Cancel / .Ok
然后将窗体的 .AcceptButton 属性设置为确定按钮,将其 .CancelButton 设置为取消按钮,您就完成了 - 所有这些都在设计器中完成,无需一行代码。

我为此构建了一个模板窗体,我可以从我的辅助项目中将其复制拖放到当前项目中,重命名它,然后立即开始设计其内容。

 

请看 dlgCustomerdlgArticle,它们是简单的详细视图如前所述,放置在精心配置的 TableLayoutPanels 上。

第三个、更复杂的 - dlgOrder - 您已经从上面知道了,但现在看看它的设计。

并且牢记一条最重要的通用规则:不要在对话框本身中处理用户输入。
处理用户的对话框输入是对话框调用者的责任,因为它持有需要集成输入的上下文。
这是有问题的,因为对话框的代码隐藏非常空,而主窗体可能会变得拥挤。但不要通过犯下架构上的错误设计来使其不拥挤。

辅助项目

做两次同样的事情是糟糕的风格,做两次复杂的事情是糟糕的风格。这就是为什么我通常会在我开发的应用程序中包含一个(甚至多个)辅助项目。

此处附加的源代码包含其一个小型版本,但我担心,它足够大,会让一些初学者感到绝望 :-(。
它主要包含通用的扩展函数,大部分是自解释的,但有些内容并不那么直观,并且相当高级。
其他内容 - 特别是调试辅助工具 - 可能看起来很晦涩,因为它们完全未被使用,因为我后来删除了它们的用法。

其中最复杂和最晦涩的函数我已经提到了:Form.Register(Dataset),它通过反射扫描窗体并强制执行类型化数据集的“高地人原则”

接下来的高级功能是 BindingSource.EditCurrent(Of T As Form)BindingSource.EditNew(Of T As Form),它们都做大致相同的事情:

  1. 创建一个 T 实例 - 注意:T 派生自 Form
  2. 注册其数据集,以启用跨窗体数据绑定。
  3. 通过反射扫描 T 以查找适当的目标 BindingSource。
  4. 将调用者 BindingSource 的当前记录设置为目标 BindingSource 的数据源。
  5. 模态打开 T,并将 .ShowDialog() 的结果返回给调用者。

这种方法很棘手:不是为对话框填充几个特定的默认数据值,而是简单地分配一个记录(体现为 DataRowView),数据绑定会完成其余的工作。
由于 DataRowView 实现 IEditableObject,因此可以轻松地获得取消选项,无需我做任何额外工作。
通过使用这个,我可以用一行代码处理四种用户体验:1) 添加、2) 编辑客户、3) 添加、4) 编辑商品。

体验 5) 和 6) - 添加/编辑订单 - 对于单行代码来说过于复杂 - 如所示,这需要 20 行代码(这可能是解决该特定问题的最优雅的解决方案了)。

留给您的一项练习,如果您愿意的话

如您所见,有用于添加/编辑商品、客户和订单的对话框。如果将用户体验以同样的方式附加到添加/编辑类别呢?
基本上这很简单 - 只要复制例如客户对话框即可。但是,您也可以接受挑战,将CRUD 实现到 Category.Image 属性(使用 FileOpenDialog 等)。

© . All rights reserved.