简介 | |
数据源 | |
构建以数据为中心的窗体 | |
自定义生成代码 | |
小结 |
在之前的 ADO.NET 2.0 中的新 DataSet 特性 这篇文章中,我讨论了即将对 ADO.NET DataSet 类及其相关类(例如 DataSet、DataTable 和 DataView)作出的一些改动以及这些类的增强功能。所有这些类都是属于 Microsoft .NET Framework 基类库的类。
本文,我将致力于在 Microsoft Visual Studio 2005 开发环境下使用这些类以及派生的类进行开发工作。对于类型化 DataSet 所作的更改和由 Visual Studio 2005 生成的新类型化 TableAdapter,是本文将要讨论的具体内容。除此之外,为开发以数据为中心的应用程序而使用的设计器和工具也是本文要讨论的内容,这些设计器和工具提供了很高的灵活性和工作效率。为了解释不同的概念和特性,我将逐步介绍开发人员在实现应用程序数据处理部分时通常要经历的过程。代码示例使用 Northwind 数据库,该数据库是 Microsoft SQL Server(以及 MSDE)7.0 版本和 Microsoft SQL Server 2000 自带的示例数据库。
Visual Studio 2005 引入了项目数据源的概念。数据源代表在应用程序中可以使用的数据。数据库不是这些数据的唯一来源,定义数据源所使用的 Data Source Configuration Wizard 允许您从三种不同的源获取数据:
1. |
数据库 — 既可以是基于服务器的数据库(例如 SQL Server 或 Oracle),也可以是基于文件的数据库(例如 Access 或 SQL Server Express)。Visual Studio 自动生成类型化 DataSet 及其他类,并将它们添加到项目。 |
2. |
对象 — 任何具有公共属性的对象都可以是数据源。不一定非要实现某个特殊接口。 |
3. |
Web 服务 — 从 Web 服务创建的数据源,将创建与 Web 服务返回的数据类型相对应的对象。 |
数据源具有事半功倍的作用。一方面,它可以使指定、设计和生成代表应用程序数据的强类型类更容易。另一方面,它提供一种灵活而统一的机制,可非常快速地构建功能丰富、运行高效的 WinForm 和 WebForms 用户界面。在阅读本文的过程中,您将了解到这是多么得快速、简单和灵活。
数据库 (DataSet) 数据源的创建及其在 WinForms 应用程序中的使用是本文的重点内容。但是,以下两点也不容忽视:
• | 一旦创建了数据源,就将以相同的方式使用每个数据源,而无需考虑其数据来自何处。也就是说,就像您可以轻松地将基于 Database 的数据源(以图形方式)绑定到网格控件或一组控件上一样,真正来自 Web 服务或您自定义业务对象的数据也要绑定到控件上。 |
• | 无论是在 WinForms 还是在 WebForms 应用程序中使用,数据源均以同一种方式进行定义。不同的数据提供程序也进行了抽象,以便在只使用 DataSets 和 TableAdapters 公开数据访问的情况下,只需更改连接字符串并重新生成类就可以更改实际使用的数据库。 |
类型化 DataSet 和 TableAdapter
数据库数据源就是一个强类型 DataSet 与一个或多个强类型 DataTables 和 TableAdapters 对的组合。类型化 DataSet 的概念并不陌生,在 Visual Studio 2002/2003 中就已经提出。虽然它是从 .NET Framework 的一般 DataSet 类派生的生成类,但它具有定义好的架构以及特定于该架构的属性和方法。与此同时,DataSet 中的每个表还要生成其他三个特定于该 DataSet 的派生类 — DataTable、DataRow 和 DataRowChangeEvent 类。在这些类中,每个类都有针对相关表的特定架构、属性和方法。例如,如果根据 Northwind Employees 表定义一个数据源,最终将生成以下类:
• | NorthwindDataSet |
• | EmployeesDataTable |
• | EmployeesDataRow |
• | EmployeesRowChangeEvent |
这四个类就组成了一个类型化 DataSet。在 Visual Studio 2005 中还将生成第五个类,名为 EmployeesTableAdapter 的类型化 TableAdapter,稍后我们将讨论这个类。当然,如果您以动态方式定义查询,那么就不能生成类型化 DataSet,而是需要使用标准的 DataSet。
为什么要为类型化DataSet 而烦恼呢?因为除了促使您预先考虑数据的架构,而非仓促决定之外,类型化 DataSet 还提供了一些实际的优点:
1. |
DataSets、DataTables、DataRows 和 RowChangeEvent 都特定于正在处理的架构。 |
2. |
表、列和关系是以命名的属性方式公开的,而非一般的集合元素。 |
3. |
由于 (2) 的原因,Visual Studio 代码编辑器将支持完整的智能感知和语句结束支持。 |
4. |
可以实现编译时类型检查,例如在编译时而不是等到运行时就可以捕捉到字段名拼写错误,这同样是由于 (2) 的原因。 |
5. |
代码更简洁,可读性更强。例如以下代码行: country = dsNorthwind.Tables ("Employees").Rows (row) ("Country") 可以替换为 country = dsNorthwind.Employees (row).Country |
总而言之,类型化 DataSets 所提供的设计时和编译时支持不但可以大大地减少启动开发时间,而且可以减少调试和稳定应用程序所需的时间。
此外,TableAdapter 是 Visual Studio 2005 新提出的概念。它意味着强类型 TableAdapter 就是标准 DataAdapter 的强类型等价类。您可以使用 TableAdapter 连接到数据库,执行对数据库的查询或存储过程,并将数据填写到相关的 DataTable。每个 DataTable-TableAdapter 对都以一个 TableAdapter 指代。
TableAdapter 实质上就是一个标准的 DataAdapter 的外部包装程序,它提供以下一些好处:
• | 同一个 TableAdapter 类可在多个窗体或组件上使用,因此任何对查询/命令进行的更改都将自动在所有实例中反映出来。这不同于现有情形,在现有情形下,每个访问数据库的组件都必须拥有各自单独配置的 DataAdapter。但是如果要确保 DataTables 和 DataAdapters 保持同步,这样做将更容易。 |
• | 如果要为某个指定的 DataTable 定义多个查询/命令,使用一个 TableAdapter 即可轻松完成,而不必使用多个 DataAdapters 或手动切换代码。 |
• | Fill 命令的名字可读性强、较友好,而且 TableAdapter 包含的代码可以自动为这些命令方法中的所有参数填加类型和值信息。您再也不需要将其以特定于提供程序的数据类型(例如 SqlInt)进行传递了。 |
举一个简单的代码片断示例会有助于解释这些特性。在 Visual Studio 2002/2003 中,即使使用了类型化 DataSet,执行一个带有两个参数的简单查询也需要不少的代码。对于下列查询
SELECT FirstName, LastName from Employees WHERE Country = @country AND City = @city
我们必须按以下方式编写代码:
Me.SqlAdapter1.SelectCommand.Parameters ("@country").value = Me.CountryListbox.SelectedValue.Trim() Me.SqlAdapter1.SelectCommand.Parameters ("@city").value = Me.CityTextbox.Text.Trim() Me.SqlAdapter1.Fill (Me.NorthwindDataSet.Employees)
毫无疑问,随着参数数量的增长,代码行数也将不断增加。但更重要的是,正确地记忆和输入所有参数名的机率却大大降低了。即使准确记住了参数名,也仍然需要记住参数的数据类型。这还不是最糟糕的情况,如果错误地输入了字段名或者为值赋了错误的类型,那么很可能到运行时才会察觉。
如果使用 Visual Studio 2005 中的 TableAdapter,一旦定义了命令 FillByCountryAndCity,在任何地方使用它时只需编写一行代码,将其以参数值形式传递即可:
Me.EmployeesTableAdapter.FillByCountryAndCity ( _ Me.NorthwindDataSet.Employees, Me.CountryListbox.SelectedValue.Trim(), _ Me.CityTextbox.Text.Trim() )
不容忽视的是,我们不仅可以从一个 TableAdapter 那里取得多个命名命令,而且这些命令都是强类型的。这就意味着在 Visual Studio 中编写代码时,可以将这些命令视为 TableAdapter 的方法,因为我们获得了完整的智能感知。我们还要对这些命令的参数进行编译时类型检查,同时,方法和参数类型定义的工具提示也可以助我们一臂之力。TableAdapter 可以包含多个执行不同命令、接收不同参数的方法。现在我们要构建示例窗体,稍后再详细介绍 TableAdapter。
开始行动 — 创建数据源
本文,我们将构建一个显示 Northwind 数据库中每个定单定购信息的窗体。打开一个新的 Visual Basic WinForms 项目后,首先必须做的事情是向项目中添加一个新数据源(在创建这一示例的过程中,我们将使用 Visual Basic,但所有的操作对于 C# 也适用)。
添加一个数据源:
1. |
在 Visual Studio 主菜单中,选择 Data 菜单项中的 Show Data Sources,即可显示数据源窗口(在该窗口未显示出来的情况下)。 |
2. |
在数据源窗口上,单击 Add New Data Source 工具栏按钮。该操作将启动 Data Source Configuration Wizard,该向导将 DataAdapter Configuration Wizard 的大部分功能和 Visual Studio 2002/2003 中的 DataSet 生成工具结合在一起。 |
3. |
如果您的 Visual Studio 版本仍然包含向导的欢迎页面,请选择 Next。然后将显示 Choose a Data Source Type 页面。 |
4. |
选择 Database。 |
5. |
选择 Next,显示 Choose Your Data Connection 页面。 |
6. |
选择 New Connection,显示 Add Connection 对话窗口。 |
7. |
输入要连接到 SQL Server(或 MSDE)实例以及 Northwind 数据库所需的信息。 |
8. |
选择 OK,关闭对话窗口。 |
9. |
注意,现在的连接字符串是保存为一个设置属性,可以通过下列语句访问 My.Settings.NorthwindConnectionString 在 C# 中,该语句为以下形式 VSDataSets.Properties.Settings.Default.NorthwindConnectionString; |
10. |
选择 Next,显示 Choose Your Database Objects 页面。 |
11. |
注意,可以选择 TablesViews、Stored、Procedures 或 Functions。 展开 Tables 节点,然后选择 Orders 和 Order Details 表。我们将使用表中的所有列,但您也可以只选择应用程序所需的那些列。 |
12. |
选择 Finish,退出向导。 |
图 1 显示 Order Details 表展开后的数据源窗口,图中显示了该表的所有列。
图 1. 数据源窗口中的 Order Details 表
如果要作出一些改动,重新输入 Data Source Configuration Wizard,可以选择数据源窗口工具栏的 Configure DataSet with Wizard,或者右键单击该窗口中任何元素,从得到的上下文菜单中选择向导。然而,编辑生成的 DataSet 和(多个)TableAdapter 更好的工具是 DataSet Designer。
DataSet 设计器
Visual Studio 2005 包含的 DataSetDesigner 是一个专门为指定和编辑 DataSets 及其相关 TableAdapters 而设计的工具。与 Visual Studio 2002/2003 中的情形比较,它有了明显的改进,因为以前我们必须使用 XML 架构编辑器来定义强类型 DataSets。在这里,DataSet 和 TableAdapter 的定义仍然保存在 .XSD 文件中,如果一定要编辑 XML 架构,也仍然是使用 XML 编辑器。不过,相似之处仅此而已。而且这样做只是为了文件格式方便起见,并不是为了使 DataSetDesigner 支持任意的 XSD 文件。
选择数据源窗口工具栏中的 Edit DataSet with Designer,或者右键单击该窗口中任何元素,从得到的上下文菜单中选择使用设计器进行编辑,将进入 DataSet 设计器。在设计 DataSets 及其 DataTables 的过程中,设计者很可能会觉得该工具与过去设计数据库使用的其他工具十分相似。要为连接的数据库添加表,可以将数据库对象(例如表、视图、存储过程)从 Server Explorer 中拖放到设计器表面,或者在设计器上下文菜单的 Data 主菜单选项中选择 Add TableAdapter,启动 TableAdapter Configuration Wizard。因为 DataSet 还可以包含不与数据库连接而直接加载的表,所以还可以选择菜单中的 Add DataTable 来添加一个单独的表。在编辑器中同样可以添加和/或重命名列。另一个干净利落的特性是,编辑器自动识别数据库中各表之间的关系,在 DataSet 中为您定义各表之间对应的 Relations。
图 2 显示在 DataSet 设计器中,由 Orders 和 Order Details 表组成的数据源。注意,与每个 DataTable 密切相关的是其各自对应的 TableAdapter,用于为表填写数据,或者(可选)用改动后的数据更新数据库。
图 2. 数据源中的 Orders 和 Order Details 表
TableAdapter Configuration Wizard
选择 Data 主菜单中的 Add Query 或 Configure(一个现有查询),或者在设计器中右键单击 TableAdapter,从得到的上下文菜单中选择这两项,可以启动 DataSet 设计器中的 TableAdapter Configuration Wizard。此向导等同于 Visual Studio 2002/2003 的 DataAdapter Configuration Wizard,只不过多了两个附加页面。一个是图 3 所示的 Choose a Query Type 页面。由于 TableAdapter 汇集了针对给定表的所有命令,因此该页面允许您定义多个 Select/Fill 命令和查询 — 任何类型的查询都可以,例如 Update、Insert、Delete 或返回单个值类型的查询。切记,这些更新查询是 TableAdapter 的命名方法,必须直接调用。而且它们还是执行 TableAdapter 的 Update 方法时自动调用的更新查询,这与执行 DataAdapter.Update 方法一样。
向导中另一个增加的页面是 Choose Methods to Generate 页。在该页中可以选择您所定义的每个查询/命令方法名(一个或多个)。向导为每个命令都提供一个 Fill 和 Get 方法,如图 3 所示。Fill 要求提供需要填写数据的 DataTable,而 Get 方法将返回填写后新创建的 DataTable。
图 3. Choose Methods to Generate 页面中的 Fill 和 Get 方法
按照典型的用法,您可以为一个 TableAdapter 定义多个 Fill 命令,它将返回同一种架构(多个列),只是 WHERE 子句不同而已。这也是默认情况下向导会为方法名提供一个 FillBy 和 GetDataBy 前缀的原因。不过,您当然可以选择任何名字作为方法名。
虽然 TableAdapter 可以包含多个 Fill 命令,但在调用其中的 Update 方法时却只能执行一组更新命令。这组命令根据 TableAdapter 的主查询自动生成。第一次创建 TableAdapter 时定义的查询被认为是它的主查询。如果随后定义的任何查询返回的架构与主查询的架构不同,则设计器将显示一个消息框进行提醒。另一方面,如果您修改了主查询的架构,Visual Studio 将修改其他查询的架构以与之匹配。
通过 TableAdapter Configuration Wizard 添加新命令
现在,我们将另一个命令添加到 Orders 表的 TableAdapter。
1. |
选择数据源窗口工具栏中的 Edit DataSet with Designer,打开 DataSet 设计器。 |
2. |
选择 Orders 表的 TableAdapter,再从该表的上下文菜单中选择 Add Query。 |
3. |
如果您的 Visual Studio 版本中仍然有 Welcome 页,请选择页面上的 Next。之后,显示 Choose a Command Type 页面。 |
4. |
要接受默认的 SQL 语句,选择 Next。这时显示 Choose a Query Type 页面。 |
5. |
要接受默认的 SELECT 语句,选择 Next。然后,显示 Specify a SQL SELECT Statement 页面。 |
6. |
输入下列 SQL 语句。 SELECT OrderID, CustomerID, EmployeeID, OrderDate, RequiredDate, ShippedDate, ShipVia, Freight, ShipName, ShipAddress, ShipCity, ShipRegion, ShipPostalCode, ShipCountry FROM Orders WHERE CustomerID = @CustID 此后,返回由进行查询的 @CustID 参数指定的所有客户 Orders。 |
7. |
选择 Next,显示 Choose Methods to Generate 页。 |
8. |
选中页面上的两个复选框。将方法名分别改为 FillByCustomer 和 GetDataByCustomer。 |
9. |
选择 Next,然后选择 Finish,完成该过程。 |
现在请您看一下 DataSet 设计器中的 OrdersTableAdapter,这时出现了第二个命令对 — FillByCustomer 和 GetDataByCustomer,CustomerID 的值就是该命令的一个参数。那么,该方法的参数属于哪种 .NET 类型呢?让我们来讨论一下。
后台简介
在 DataSet 设计器或任何相关向导中进行修改时,Visual Studio 都会为一组类型化的类生成代码。具体而言,它将生成以下类:
1. |
DataSet 类 |
2. |
DataTable 类 |
3. |
TableAdapter 类 |
4. |
DataRow 类 |
5. |
DataRowChangeEvent 类 |
除了 DataSet 类(每个数据源只有一个)之外,其余四个类对于 DataSet 中定义的表而言是相同的。可通过以下操作查看这些类的代码:
1. |
在 Solution Explorer 窗口中,单击工具栏上的 Show All Files 按钮,显示与项目相关的所有文件。 |
||||||||||
2. |
展开 NorthwindDataSet.xsd 文件的节点。 |
||||||||||
3. |
双击文件节点上的 NorthwindDataSet.Designer.vb。这就是为实现组成 DataSet 的那些类而生成的代码。 |
||||||||||
4. |
打开代码窗口左上角的 Listbox,将看到本文所用类的列表:
|
||||||||||
5. |
选择左侧 Listbox 中的 OrdersTableAdapter 类。 |
||||||||||
6. |
再选择右侧 Listbox 中的 FillByCustomer 方法。此时将显示该方法的代码,如下所示: Public Overloads Overridable Function FillByCustomer(ByVal dataTable As NorthwindDataSet.OrdersDataTable, ByVal CustID As String) As Integer Me.Adapter.SelectCommand = Me.CommandCollection(1) If (CustID Is Nothing) Then Throw New System.ArgumentNullException("CustID") Else Me.Adapter.SelectCommand.Parameters(0).Value = CType(CustID,String) End If If (Me.m_clearBeforeFill = true) Then dataTable.Clear End If Dim returnValue As Integer = Me.Adapter.Fill(dataTable) Return returnValue End Function |
从该代码片断中我们可以了解几件事情。
• | FillByCustomer 方法实际上带有两个参数 — 一个是要填写的 OrdersDataTable,另一个是字符串类型的 CustID 参数。 |
• | 不用检查完文件中的所有代码,我们就可以发现 TableAdapter 类维护着一个命令集合,正是从这个集合中自动将正确的命令指派给 .NET DataAdapter,供其使用这些命令自动与数据库通讯。 |
• | 如果状态参数的 AllowDBNull 属性设置为 False,该方法会不断检查是否有状态参数的实例传递进来。 |
• | 如果 DataAdapter 的 SelectCommand 属性已经配置好,则 CustID 的参数值将赋给该属性的参数。 |
• | 所有配置设置完毕后,即可调用 DataAdapter.Fill () 方法来填写 OrdersDataTable。 |
切记,这些代码都不需要您动手编写。它们是已经生成并配置好的代码。花些时间查找一下为您生成的其他类。您将更深入、更广泛地理解这些类的实现方式,甚至还可能学到几个有用的编程技巧。
注 尽管类型化 DataSet 及其相关类(包括 TableAdapters)均在一个源文件中生成,但 TableAdapters 是在某个独立的命名空间中生成的。这说明应该将实体对象 (DataSets) 与真正的数据访问对象 (TableAdapters) 区分开来。
生成了类型化 DataSet 之后,接下来要构建一个显示其数据的窗体。对于 .NET Framework 2.0 和 Visual Studio 2005 中的 WinForms 以及数据绑定,我不会深入研究它们的所有细节和新特性(因为可研究的东西太多了),但如果要了解以下这方面所取得的一些进展 — 如何能更简单、更灵活地建立功能上以数据为中心的窗体,这到是一个难得的机会。
工具箱中的数据组件
如果您常常使用工具箱“Data”选项卡上的标准数据组件来构建以数据为中心的代码,那么打开 Visual Studio 2005 后,可能会有些沮丧和担心,因为您无法找到它们。这当然是“设计”好的,Microsoft 希望引导用户利用新的类型化 DataSets 和 TableAdapters。如果实在想使用以前没有类型化的组件,您可以手动将它们添加到工具箱中。我强烈建议您不要将新的 TableAdapters 与部分类技术(新的类型化 DataSets)一起使用;TableAdapters 的使用和扩展更容易。
在 Visual Basic 的团队日记中,可以找到更多各种各样的设计决策信息以及这些决策的深层思想。
特别是您可以浏览一下 Steve Lasker 的两篇文章: Why are the Data Components no longer on the Toolbox? 和 Why can't I drag from Server Explorer to my form?。
在 Visual Studio 设计器中打开一个窗体时,Visual Studio 工具箱会出现一个以项目名为标签的选项卡。在项目中添加一个数据源并至少将其编译一次后,该选项卡中将出现创建的 DataSets 和 TableAdapters。您可以将这些组件拖放到窗体设计器上,在使用设计器实现数据访问组件的情况下,这是很好的做法,但通常情况下往往不这么做。而是使用以下三种不同方法中的一种来构建以数据为中心的窗体。首先,逐步介绍第一种方法,这种方法最简单,甚至可能最普遍。它就是所谓的“一次拖动”数据绑定。
1. |
双击解决方案资源管理器中的 Form1.vb,打开 Visual Studio 窗体设计器中的 Form1。 |
2. |
展开数据源窗口中的 Orders 表节点。 注意,DataSet 中的每个表和每行列都有一个与之相关的图标。在将表或列拖放到窗体上时,这些图标表示用于绑定数据的 WinForm 控件类型(或者是“放置”类型)。从相关联的下拉列表中选择某个控件类型选项,再选择一个该类型的控件,可以改变控件类型。注意,该列表还包含 None(显示为空)和 自定义(指定您喜欢的任何控件)这两个选项。 仅在当前活动窗口是窗体(或组件)设计器时,这些图标和放置类型列表可见。此外,您不能在数据源窗口中拖放控件,图标的改变会指示出这一点。 |
3. |
将 Orders 表的控件类型从 DataGridView 更改为 Details。这就是说,将整个 Orders 表拖动到窗体上时,会构建一个每次显示一行详细信息的窗体,而不是显示网格中所有数据(一次显示所有行)的窗体。Details 视图会为每个列都添加一个标签和一个控件,控件类型是在数据源窗口中指定的类型。 |
4. |
将 Orders 表从数据源窗口拖动到设计器中的 Form1。 |
5. |
选择后七个 (7) 字段及其标签,将它们拖放到与前七个字段并排的位置上,这时的窗体外观如图 4 所示。 图 4. 设计器中的 Form1 |
6. |
启动应用程序,使用窗体顶部工具栏上的导航按钮逐个浏览记录,启动并确认应用程序可以正常运行。 接下来回顾一下将数据源表拖动到窗体上时 Visual Studio 的动作。查看窗体下面的组件栏,会看到它向窗体添加了四个组件。其中的两个组件 NorthwindDataSet 和 OrdersTableAdapter 我们已经熟悉了(您喜欢它们吗?)。OrdersTableAdapter 用于将数据库中的数据填写到 NorthwindDataSet 的 OrdersDataTable。甚至连执行 Fill 的一行代码也已经编写好,并自动添加到 Form1 的 Load 事件处理程序中了。 Me.OrdersTableAdapter.Fill(Me.NorthwindDataSet.Orders) |
对于数据绑定至关重要的类是 BindingSource 类,在当前示例中,它被命名为含义清晰的 OrdersBindingSource。BindingSource(在 Beta 1 版本中称为 DataConnector)提供将控件绑定到窗体所需的服务。它在数据源和绑定到其上的控件之间提供了一个中间层。通过设置 BindingSource 的 DataSource 和 DataMember 属性,可将它连接到数据源,然后将控件添加到控件的 DataBindings 集合中,从而将控件绑定到 BindingSource。所有与数据的交互(例如,记录导航、排序、筛选和编辑)均通过 BindingSource 完成。此外,还允许通过 List、Item 和 Current 属性访问底层数据。
添加的另外一个组件是 OrdersBindingNavigator。BindingNavigator 类是提供标准用户界面的工具栏,用于导航和操作窗体上的数据。BindingNavigator(Beta 1 版本中称为 DataNavigator)是一个具有一组预配置按钮的 ToolStrip 控件。它可以连接到 BindingSource,将其作为自身的数据源,并且可以提供控制导航可用数据的工具栏按钮。如果您希望响应一些导航事件(而不是控制导航),那么应该挂钩 BindingSource 对象的事件。
构建主-从窗体
有了显示单个表数据的窗体之后,如果要以主-从形式显示另一个相关表,还会很容易吗?还是变得很难了?不如继续在 Form1 上完成下列步骤吧:
1. |
现在我们将使用“连接点”数据绑定来构建窗体 — 将一个控件从工具箱中拖动到窗体并调整它的位置,然后将一个元素从数据源窗口中拖放到该控件上,从而将两者互相连接起来. |
2. |
选择工具箱中 All Windows Forms 选项卡上的 DataGridView 控件。将其拖置到 Form1 并调整它的位置,使其占据窗体下半部分空间。 |
3. |
返回到图 5 所示的数据源窗口,注意 Order Details 表实际上同时出现在该窗口中的两个位置。第一个位置是 NorthwindDataSet 的第一级子节点上,而且是 Orders 表的同辈节点。另一个位置是 Orders 表的子节点上,这表示它是相关的表。如果希望单独在窗体上显示 Order Details 表,可以选择 NorthwindDataSet 正下方 Order Details 表的匹配项。但在本示例中,我们希望显示 Order Details 表,因为它与主 Orders 表有关,所以我们选择 Orders 表正下方 Order Details 表的匹配项。 图 5. 数据源窗口中的 Order Details 表 |
4. |
选择 Orders 表下方出现的 Order Details 表,将其拖动到 Form1 上的 DataGridView。 |
5. |
这时 Order_DetailsBindingSource 和 Order_detailsTableAdapter 已经添加到 Form1 的组件栏中。 |
6. |
运行应用程序,使用 BindingNavigator 移动 Orders 表中的记录,如图 6 所示。注意观察 DataViewGrid 中显示的 Order Details 记录如何自动调整来只显示那些与当前 Orders 记录相关的记录。 图 6. Orders 表中 BindingNavigator |
在前文中查看 DataSet 的类代码时,您可能注意到了事实上有两个 Visual Basic 代码文件 — NorthwindDataSet.Designer.vb 和 NorthwindDataSet.vb。如果没有 NorthwindDataSet.vb 文件,请返回到 DataSet Designer 并双击设计器背景,创建该文件。
这些文件都用于实现组成 DataSet 的类。使用两个文件是为了利用简单且功能非常强大的新特性 — partial classes。它是一个编译器功能,它允许在几个声明之间拆分类(或结构)的定义。不同的声明可能保存在不同的源代码文件中,只要声明都属于同一个程序集和命名空间就可以。Visual Studio 广泛采用了该功能,从而将由设计器生成的某个类的代码与开发人员编写的该类的代码区分开来。在 Visual Studio 2002/2003 中,所有窗体代码都是该窗体类声明的一部分,例如:
Public Class Form1 Inherits System.Windows.Forms.Form
该窗体(包括窗体上放置的任何控件)的初始化代码由 Visual Studio 生成。这些代码位于 InitComponent() 方法中,默认情况下,该方法出现在名为“Windows 窗体设计器生成代码”的代码区域中用户编写的代码之前。该区域通常关闭,这是为了尽量避免与您编写的代码搞混而分散您的注意力。在 Visual Studio 2005 中,代码保存在 Form1.Designer.vb 这一完全不同的文件中,这将更有助于您集中注意力。此外,如果想查看该文件的内容,在该窗体中就可以 — 单击解决方案资源管理器工具栏上的 Show All Files,展开 Form1.vb 节点,然后双击 Form1.Designer.vb,代码编辑器中将显示代码。文件 Form1.vb 只包含您(作为开发人员)为 Form1 类编写的代码。
至于 DataSet 及其相关类的代码,使用部分类和不同文件区分设计器代码和开发人员编写的代码则具有更重大的意义。这种区分不止可使代码更整洁,而且解决了在 Visual Studio 2002/2003 中使用类型化 DataSets 时存在的一个主要问题。
通常,您还希望在为 DataSet 及其相关类自动生成的代码基础上进行扩展或添加,例如附加属性或自定义验证代码。那么,您可以放心地在生成代码中添加。只要不更改架构就可以,否则您需要重新生成 DataSet 代码。在 Visual Studio 2002/2003 中,由于代码和生成代码都添加在某一个文件中,因此重新生成代码时将清除添加的代码。正是由于使用了部分类,Visual Studio 2005 才得以避免出现这种情况。新生成的代码将覆盖现有的设计器代码并保存在以 .Designer.vb 为扩展名的文件中,而 .vb 文件中开发人员编写的代码仍然完整无缺。
要扩展使用部分类的 DataSet 功能,一种方法是添加自定义的验证代码。这时可以在生成的类型化 DataSet 中添加一些应用程序逻辑。我们将自定义的验证代码和初始化添加到 NorthwindDataSet 中 Orders 表内增加的新行中。添加一个新行时,我们希望检查传递的邮政编码值是否与实际传递的城市相匹配。如果不是,则将 ShipPostalCode 字段的值更改为 Invalid。假定实现了以下这样一个函数,该函数在给定的邮政编码与给定城市匹配的情况下返回 True:
Function IsPostalCodeInCity (ByVal PostalCode as string, ByVal City as string) As Boolean
通过以下方式可将该检验条件添加到 NorthwindDataSet:
1. |
选择数据源窗口工具栏中的 Edit DataSet with Designer,打开 DataSet 设计器。 |
2. |
双击设计器背景的空白区域。在代码编辑器中打开文件 NorthwindDataSet.vb。 |
3. |
输入以下代码来替换默认代码: Partial Public Class NorthwindDataSet Partial Class OrdersDataTable Protected Sub ValidateNewRow(ByVal sender As Object, _ ByVal e As System.Data.DataTableNewRowEventArgs) _ Handles Me.TableNewRow ' Create a strongly typed instance of the row ' This helps us avoid code in quotes, ' eg, e.Row("ShipPostalCode") Dim ordersRow As NorthwindDataSet.OrdersRow ordersRow = e.Row If Not ordersRow.IsShipPostalCodeNull And _ Not ordersRow.IsShipCityNull Then If Not IsPostalCodeInCity(ordersRow.ShipPostalCode, _ ordersRow.ShipCity) Then ' Set the value of the Ship Postal Code ordersRow.ShipPostalCode = "Invalid" ' Typically, changing a users data is a bad user experience ' So indicate an error with the ErrorProvider ' We are illustrating both approaches here ordersRow.SetColumnError( _ ShipPostalCodeColumn.ColumnName, "Invalid Postal Code") Else ' we always need to reset the error when the value is valid ordersRow.SetColumnError( _ ShipPostalCodeColumn.ColumnName, String.Empty) End If End If End Sub End Class Private Shared Function IsPostalCodeInCity(ByVal postalCode As String, _ ByVal city As String) As Boolean ' This is a stub, just to check functionality If city = "Rio de Janeiro" Then Return False Else Return True End If End Function End Class |
注意,DataSet 的所有相关类(例如,OrdersDataTable)在其内部均作为嵌套类实现。前面代码中的部分类声明就反映了这一实现过程。
为了让错误提供程序来提示错误,以补充或代替将邮政编码值更改为 Invalid 这一操作,可执行以下步骤:
1. |
将 Error Provider 控件从工具箱拖放到 Form1,将其放置在“Ship Postal Code”文本框右侧。 |
2. |
在 Error Provider 属性窗口中,将 DataSource 属性设置为 OrdersBindingSource。 |
要查看验证代码的执行情况,可以运行应用程序并导航到城市为“里约热内卢”的那条记录,图 7 对此做出说明。
图 7. 错误提供程序控件
该示例仅示范如何轻松地扩展类型化 DataSet 及其相关类的功能,这里的相关类指由 Visual Studio 自动生成的类。您或许还希望在生成的类中添加一些其他的方法和属性。当设计应用程序以及使用 DataSets 时,您会想到多种多样的可能性。关键是要记住,由于在 Visual Studio 2005 中可以使用部分类,您编写的代码是保存在另一个单独文件中的,因此重新生成 DataSet 类时您的代码不会受影响。
使用 Visual Studio 2005 生成的类型化 DataSets 从未如此之简单而灵活。DataSetDesigner 为定义 DataSets 而提供了一个更简单、更自然的工具。在 DataSetDesigner 内就可以配置新的 TableAdapter 类,使用该类提供的单一而集中的机制,能够轻松地维护和执行针对某个特定数据表的多个不同查询和命令。利用部分类编译器功能,可以分离设计器生成的代码和开发人员编写的代码,同时在重新生成 DataSet 类时,避免了对为扩展这些类而编写好任意自定义代码的影响。此外,新的 .NET 数据绑定类和机制在结合了 Visual Studio 2005 内部提供的工具,从而使开发以数据为中心的应用程序更加快速、简便。
感谢 Microsoft 的 Steve Lasker、Alan Griver 和 Pablo Castro 为策划本文所给予我的帮助。
关于作者
Jackie Goldstein 是 Renaissance Computer Systems 的负责人,也是进行与 Microsoft 工具和技术有关的开发、咨询和培训工作的专家。Jackie 是 Microsoft 的区域主管,是位于以色列的 Visual Basic 用户组创始人,还是包括 TechEd、VSLive!、Developer Days 和 Microsoft PDC 在内的国际开发人员大会上重要的演讲者。同时,他也是 Database Access with Visual Basic.NET (Addison-Wesley, ISBN 0-67232-3435) 一书的作者和 INETA Speakers 办公署的成员。2003 年 11 月,Microsoft 将 Jackie 选为 .NET 软件风云人物!