您是否知道空格的 ASCII 代码?如果您从上世纪八十年代就开始从事计算机编程,就有可能知道。如果您的经验仅限于基于 Internet 的系统,您很有可能会问,“我为什么要知道它?”。(顺便说一句,答案是 8。)
任何严谨的微机程序员都曾经需要了解如何直接使用 ASCII 代码。解释用户输入、设置文件格式和打印文档都需要它。当然,现在已经不需要这样了。即使我们使用的许多数据最终都仍以 ASCII 形式存储,但是只有系统级开发人员才需要考虑所使用的实际代码。其他用户可以使用一些 Visual Basic 内部常量(例如,vbCrLf)来结束一行文本,但是他们很少需要进一步了解数据的 ASCII 表示形式。
我们正与另一种数据表示技术走在同一条路上。XML 只是在近四年中才变得常用。迄今为止,要使用它,仍需要使用特定于 XML 的概念(例如,节点)进行低级别的交互。但是,这正在开始发生变革。
在 ADO.NET 中,可以看到使 XML 的使用变得更加透明的第一步。使用 Dataset,可以操作包含在 XML 文件中的较复杂的数据集,而无需本机接触 XML,或者甚至无需确切了解如何以 XML 形式存储数据。
我们仍未做好忘记节点、元素和属性等概念的准备。在许多编程情形下,仍需要在低级别与 XML 进行交互。我们打算在以后几个月的专栏中讨论这些情形。不过,目前,让我们看一下如何使用 XML 这个方便的方法。
我们的测试平台
本文中的所有示例都是通过使用一个测试平台程序来完成的,此程序是一个 Windows 窗体应用程序,其中包含一个 DataGrid、一个 RichTextBox 和五个按钮。下面是其屏幕快照:
图 1. 测试平台程序的可视布局
在创建了如图 1 所示的可视布局之后,更改 DataGrid 的 Anchor 属性,以将其定位到所有四个边上。然后更改 RichTextBox 的Anchor 属性,以将其定位到上边、左边和右边(但是不定位到下边)。
为了更便于记住在各个点使用哪个按钮,请按如下方式对按钮进行命名:
按钮上的文本
• Write XML
• Show Dataset Structure
• Change XML With Code
• Show Data With For Each
• Create New XML File
按钮名称
• btnWrite
• btnShowStructure
• btnCode
• btnShowData
• btnNewXML
返回页首
在网格中使用 XML
首先,让我们看一下如何将 XML 引入网格中、如何操作它以及如何重新写出结果。我们将使用比一组简单的记录复杂的数据,其中包含多个客户以及每个客户的定单。换句话说,我们将对具有主-从结构的数据进行操作。
下面是我们将使用的 XML 文件:
<?xml version="1.0" standalone="yes"?>
<root>
<Customer>
<CompanyName>Northern Lights, Ltd</CompanyName>
<Order>
<OrderDate>12-19-2001</OrderDate>
<OrderTotal>102.13</OrderTotal>
</Order>
<Order>
<OrderDate>01-07-2002</OrderDate>
<OrderTotal>127.63</OrderTotal>
</Order>
</Customer>
<Customer>
<CompanyName>Southern Accents, Inc.</CompanyName>
<Order>
<OrderDate>12-22-2001</OrderDate>
<OrderTotal>291.74</OrderTotal>
</Order>
</Customer>
</root>
如果您下载了本文的代码项目,则上面的 XML 文件就在该项目中,该文件的名称为 XMLData.xml。
要将该数据放到某个网格中,我们将其放到一个 Dataset 中,然后将该 Dataset 绑定到该网格。因为 Dataset 类有一个负责所有工作的 ReadXml 方法,所以将数据放到 Dataset 中会比较容易。我们不必通读 XML 节点或者进行其他低级访问,甚至无需修复 Dataset 的任何结构。ReadXml 方法为该 Dataset 推断适当的结构,并填充该 Dataset 的表和行。
我们将需要一个可在整个测试平台上使用的 DataSet 引用,以便将下面的一行放在 Inherits System.Windows.Forms.Form 一行的正下方:
Dim XMLDataset As DataSet
让我们放置该代码,以便在窗体的Load 事件中加载网格。该代码看上去如下所示:
XMLDataset = New DataSet()
' Change location of XML file as necessary in next line
XMLDataset.ReadXml("C:\Development\XMLFiles\XMLData.xml")
DataGrid1.DataSource = XMLDataset
' Tell the grid to display the Customer table initially
DataGrid1.DataMember = "Customer"
在本例中,我们需要执行的最后一项任务是包括一小段代码,让它将对数据进行的更改写入到一个 XML 文件中。由于我们反复使用同样的数据,所以我们会将所做的更改写入到不同的 XML 文件中。在普通的应用程序中,往往将所做的更改写回到从中读取数据的同一个 XML 文件中。
用来保存所做更改的代码放入 btnWrite 的 Click 事件中,它看上去如下所示:
' Change the location of the XML file as necessary
DataGrid1.DataSource.WriteXml("C:\Development\XMLFiles\Output1.xml")
在执行了上述步骤之后,启动程序。网格中最初显示两个客户,并在每个客户的旁边显示一个加号,单击这个加号,可以访问该客户的定单。下面是该网格的两个视图。第一个视图显示客户列表,第二个视图显示第一个客户的定单。
图 2. 网格中可以显示客户(左)或客户的定单(右)。
如果您未使用过具有主-从数据的 DataGrid,则可能不了解如何从查看定单返回到查看客户。这通过单击图 2 右侧网格屏幕中指向左侧的箭头来完成。
现在,我们可以根据需要随意操作网格中的数据。我们可以更改行、删除行和添加新行。然后,可以通过单击 Write XML 按钮将所做的更改保存回一个 XML 文件中,该按钮使用 DataSet 的WriteXml方法来保存所做的更改。
假设我们为 Northern Lights, Ltd 添加一个新定单,随后添加一个名为 Eastern Shores, LLC 的新客户,并为这个新客户添加一个定单。然后单击Write XML 按钮。最终的 XML 文件看上去与下面的代码片段非常相似:
<?xml version="1.0" standalone="yes"?>
<root>
<Customer>
<CompanyName>Northern Lights, Ltd</CompanyName>
<Order>
<OrderDate>12-19-2001</OrderDate>
<OrderTotal>102.13</OrderTotal>
</Order>
<Order>
<OrderDate>01-07-2002</OrderDate>
<OrderTotal>127.63</OrderTotal>
</Order>
<Order>
<OrderDate>03-03-2002</OrderDate>
<OrderTotal>555.55</OrderTotal>
</Order>
</Customer>
<Customer>
<CompanyName>Southern Accents, Inc.</CompanyName>
<Order>
<OrderDate>12-22-2001</OrderDate>
<OrderTotal>291.74</OrderTotal>
</Order>
</Customer>
<Customer>
<CompanyName>Eastern Shores, LLC</CompanyName>
<Order>
<OrderDate>04-04-2002</OrderDate>
<OrderTotal>444.44</OrderTotal>
</Order>
</Customer>
<Customer />
</root>
非常容易,是不是?新数据就在那里,而且它的结构也正确无误。但是在幕后却执行了许多工作,因此让我们更仔细地看一下。
首先,为了解释 XML 文件中的数据并以关系方式在网格中显示它,ReadXml 方法做了相当多的工作。它不仅构造了一个用来保存客户和定单数据的 DataSet,而且还在客户和定单之间设置了一种关系。
如果您不熟悉 ADO.NET,则可能不知道 DataGrid 如何解释 Dataset 中的数据。为了显示由客户细分的定单,网格需要在数据集中定义一种关系。即,为了使网格能够查找与某个客户相关联的定单,在 DataSet 中必须为客户和定单定义一种父-子或主-从关系。
ReadXml 方法基于它所读取的 XML 文件的结构自动创建了一种关系。要查看这种关系,请将下面的代码放到Show Dataset Structure 按钮的Click事件中:
Dim relRelation As DataRelation
RichTextBox1.Text &= "Relationships in dataset:" & vbCrLf
For Each relRelation In XMLDataset.Relations
RichTextBox1.Text &= "Relationship: " & _
relRelation.RelationName & vbCrLf
RichTextBox1.Text &= vbTab & "Parent table: " & _
relRelation.ParentTable.TableName & vbCrLf
RichTextBox1.Text &= vbTab & "Parent column: " & _
relRelation.ParentColumns(0).ColumnName & vbCrLf
RichTextBox1.Text &= vbTab & "Child table: " & _
relRelation.ChildTable.TableName & vbCrLf
RichTextBox1.Text &= vbTab & "Child column: " & _
relRelation.ChildColumns(0).ColumnName & vbCrLf
Next
如果您现在运行该程序并单击Show Dataset Structure 按钮,所显示的 RichTextBox 将具有如图 3 所示的外观。
图 3. 由 Dataset 的 ReadXml 方法自动创建的关系的相关信息。
网格使用我们现在看到的关系来提供数据的关系视图。Customer 表通过一个名为 Customer_Id 的列与 Order 表相关联,该列在这两个表中都存在。
但是,这些 Customer_Id 列是从哪里来的呢?原始的 XML 文件中没有它们。它们是如何进入 Dataset 中的呢?
ReadXml 方法为我们创建了它们。原始的 XML 文件的结构暗示了在客户和定单之间存在一种关系,但是该 XML 文件不包含主键和外键,而这些键是这样的关系在关系表示中所必需的。因此,ReadXml方法创建了适当的字段,并根据需要为它们赋值。
要了解有关这些字段的更多信息,请在Show Dataset Structure 按钮的Click事件底部添加下面的代码:
Dim dtCustomers As DataTable = XMLDataset.Tables(0)
Dim dtOrders As DataTable = XMLDataset.Tables(1)
Dim iCustField As Integer
RichTextBox1.Text &= "Columns in Customer datatable:" & vbCrLf
For iCustField = 0 To dtCustomers.Columns.Count - 1
RichTextBox1.Text &= vbTab & _
dtCustomers.Columns(iCustField).ColumnName & " - "
RichTextBox1.Text &= vbTab & _
dtCustomers.Columns(iCustField).DataType.ToString & vbCrLf
RichTextBox1.Text &= vbTab & vbTab & "AutoIncrement: " & _
dtCustomers.Columns(iCustField).AutoIncrement & vbCrLf
RichTextBox1.Text &= vbTab & vbTab & "Column Mapping: " & _
dtCustomers.Columns(iCustField).ColumnMapping.ToString & _
vbCrLf
Next iCustField
Dim iOrderField As Integer
RichTextBox1.Text &= "Columns in Order datatable:" & vbCrLf
For iOrderField = 0 To dtOrders.Columns.Count - 1
RichTextBox1.Text &= vbTab & _
dtOrders.Columns(iOrderField).ColumnName & " - "
RichTextBox1.Text &= vbTab & _
dtOrders.Columns(iOrderField).DataType.ToString & vbCrLf
RichTextBox1.Text &= vbTab & vbTab & "AutoIncrement: " & _
dtOrders.Columns(iOrderField).AutoIncrement & vbCrLf
RichTextBox1.Text &= vbTab & vbTab & "Column Mapping: " & _
dtOrders.Columns(iOrderField).ColumnMapping.ToString & _
vbCrLf
Next iOrderField
此代码片段将所有的字段转储到 Customer Datatable 和 Order Datatable 中。对于每个字段,都包括数据类型,并列出了两个键属性(AutoIncrement 和ColumnMapping 属性)中的值。
如果您运行该程序并单击Show Dataset Structure 按钮,将看到在 RichTextBox 中列出了下列内容(除上述有关关系的信息之外):
Columns in Customer datatable:
CompanyName - System.String
AutoIncrement: False
Column Mapping: Element
Customer_Id - System.Int32
AutoIncrement: True
Column Mapping: Hidden
Columns in Order datatable:
OrderDate - System.String
AutoIncrement: False
Column Mapping: Element
OrderTotal - System.String
AutoIncrement: False
Column Mapping: Element
Customer_Id - System.Int32
AutoIncrement: False
Column Mapping: Hidden
上述列表显示 Customer Datatable 和 Order Datatable 都有一个名为 Customer_Id 的字段,该字段的数据类型是 32 位整数。在 Customer Datatable 表中,由于此字段的AutoIncrement设置为 True,因此此字段的值会自动创建。对于每个 Order 记录,此值会根据相关父 (Customer) 记录的值自动插入到 Customer_ID 中。Dataset 和网格的ReadXml 方法都知道根据需要将父 Customer_Id 字段自动插入到子 Order 记录中。
为什么在网格中看不到这些 Customer_ID 字段呢?ColumnMapping 属性被设置为一个名为 MappingType.Hidden 的枚举值。这会禁止 Customer_Id 字段在网格中显示,而且在用 Dataset 的WriteXml方法将已更改的数据写入到 XML 文件时,也会禁止包括这些字段。ColumnMapping属性还具有其他枚举,这些枚举可用来设置在将 Dataset 作为 XML 写入时,字段是显示为元素还是属性。
返回页首
在代码中使用 XML 数据
为简单起见,我们在上例中使用了一个网格。我们还可以读取原始的 XML 文件并用代码更改其中的信息。
为此,我们像以前那样将 XML 读取到 Dataset 中,然后对 DataSet 中的表和行执行操作。每个 DataSet 都有一个代表这些表的DataTable 对象集,而且此集合按表名进行索引。每个DataTable 对象都有一个用来保存数据的行集合,这些行只按一个数值索引进行索引。这些行拥有可通过列名访问的字段集合或列集合。
下面是此类代码的示例。它应当放在Change XML With Code 按钮的Click 事件中。此逻辑更改 Customer 表中第一行的 CompanyName,还更改此客户第二个定单的 OrderAmount:
' Start with a fresh copy of the data.
XMLDataset.Clear()
XMLDataset.ReadXml("C:\Development\XMLFiles\XMLData.xml")
' Get first customer row and change name of company.
Dim CustRow As DataRow
CustRow = XMLDataSet.Tables("Customer").Rows(0)
CustRow.Item("CompanyName") = "Western Styles, Ltd."
' Get orders related to above row, and change the
' the order amount in the second one.
Dim OrderRows As DataRow()
OrderRows = CustRow.GetChildRows("Customer_Order")
OrderRows(1).Item("OrderTotal") = 333.33
让我们遍历一下此代码。首先,我们清除 Dataset 并从 XML 文件中重新加载它。(请记住,我们从未写回到该 XML 文件,因此,它从被首次写入以来未发生过变化。)
接着,我们将一个客户行引入到一个名为CustRow的对象中。为了获得该行,我们在 Dataset 中访问表集合以获得名为 Customer 的行,然后在该表的行集合中获得行号为 0 的行。CustRow现在有一个字段集合,其中一个字段的名称为 CompanyName。我们更改该集合中的项目,以保存一个新值。
该代码将定单与客户相关联,它使用我们以前从未见过的 DataRow 方法,此方法的名称为GetChildRows。此方法使用一种特殊关系(该关系的名称以参数形式传递)来获取能够满足这种关系的子行。
GetChildRows返回相关行的数组,因此我们必须首先声明 DataRow 对象的数组。然后,使用针对客户行执行的GetChildRows 方法来加载该数组。
在数组中有了这些行之后,我们可以像以前那样访问它们的字段。第二个定单(在从 0 开始的集合中,其索引号为 1)的OrderTotal字段将更改为 333.33。
在将该代码放到Change XML With Code 按钮的事件中之后,启动该程序,然后单击该按钮。您将看到,所做的更改立即反映到网格中(因为网格已被绑定到 DataSet 上)。
添加新行
我们还需要能够添加新客户以及新客户定单。下面的逻辑为第一个客户(我们在上面更改的客户)添加一个新定单,然后添加一个新客户,并为该客户添加两个定单。它应当放在我们已在 btnCode 的Click事件中所放置的逻辑的正下方:
' Place a new order for the first customer
Dim NewOrderRow As DataRow
NewOrderRow = XMLDataset.Tables("Order").NewRow
NewOrderRow.Item("OrderDate") = "04-04-2002"
NewOrderRow.Item("OrderTotal") = 444.44
NewOrderRow.Item("Customer_ID") = CustRow.Item("Customer_ID")
XMLDataset.Tables("Order").Rows.Add(NewOrderRow)
' Insert a new customer.
Dim NewCustomerRow As DataRow
NewCustomerRow = XMLDataset.Tables("Customer").NewRow
NewCustomerRow.Item("CompanyName") = "Eastern Shores, LLC"
' We don't need to set Customer_Id for customer because
' it is AutoIncremented.
XMLDataset.Tables("Customer").Rows.Add(NewCustomerRow)
' Insert a new order for the new customer.
NewOrderRow = XMLDataset.Tables("Order").NewRow
NewOrderRow.Item("OrderDate") = "05-05-2002"
NewOrderRow.Item("OrderTotal") = 234.56
NewOrderRow.Item("Customer_ID") = NewCustomerRow.Item("Customer_ID")
XMLDataset.Tables("Order").Rows.Add(NewOrderRow)
要插入新记录,我们获得一个具有适当结构的行(使用 DataTable 的NewRow方法),根据需要向该行的字段中插入数据,然后将该行添加到 DataTable 的 Rows 集合中。
此逻辑看上去与对来自关系数据库的数据进行更改的等效逻辑没有区别。当数据位于 Dataset 中之后,就与数据源不相关了。 最后一行代码将数据重新放到 XML 文件中。编写的文件看上去如下所示:
<?xml version="1.0" standalone="yes"?>
<root>
<Customer>
<CompanyName>Northern Lights, Ltd</CompanyName>
<Order>
<OrderDate>12-19-2001</OrderDate>
<OrderTotal>102.13</OrderTotal>
</Order>
<Order>
<OrderDate>01-07-2002</OrderDate>
<OrderTotal>127.63</OrderTotal>
</Order>
<Order>
<OrderDate>03-03-2002</OrderDate>
<OrderTotal>555.55</OrderTotal>
</Order>
</Customer>
<Customer>
<CompanyName>Southern Accents, Inc.</CompanyName>
<Order>
<OrderDate>12-22-2001</OrderDate>
<OrderTotal>291.74</OrderTotal>
</Order>
</Customer>
<Customer>
<CompanyName>Eastern Shores, LLC</CompanyName>
<Order>
<OrderDate>04-04-2002</OrderDate>
<OrderTotal>444.44</OrderTotal>
</Order>
</Customer>
<Customer />
</root>
返回页首
在代码中访问主-从记录
我们在上面讨论的GetChildRows方法还有另一个有趣的用途。如果我们希望遍历 XML 文件中的所有记录,并要首先获取客户数据,然后获取该客户的定单,则可以使用嵌套的 For Each 循环来完成上述操作。下面列出了执行此操作的代码,您应当将该代码放在Show Data With For Each 按钮的Click事件中:
RichTextBox1.Clear()
Dim dtCustomers As DataTable = XMLDataset.Tables("Customer")
Dim dtOrders As DataTable = XMLDataset.Tables("Order")
Dim CustomerRow As DataRow
Dim OrderRow As DataRow
For Each CustomerRow In dtCustomers.Rows
RichTextBox1.Text = RichTextBox1.Text & "Customer: " & _
CustomerRow(0) & vbCrLf
For Each OrderRow In CustomerRow.GetChildRows("Customer_Order")
RichTextBox1.Text &= vbTab & "OrderDate: " & _
OrderRow("OrderDate") & vbCrLf
RichTextBox1.Text &= vbTab & "OrderTotal: " & _
OrderRow("OrderTotal") & vbCrLf
Next OrderRow
Next CustomerRow
这种用来访问主-从记录的方法对于来自关系数据库的数据也相当有用,但是为了使其奏效,必须手动设置关系。正如我们已经看到的那样,在从 XML 文件进行读取时,关系会自动设置。
返回页首
从 XML 文件派生 DataSet
ReadXml 方法使用某些规则来基于所读取的 XML 文件派生 Dataset 的结构。本文没有太多篇幅来介绍全部细节,但是最重要的规则是:
• 如果某个元素发生重复,或者如果它包含属性或者除一段简单的数据以外的任何内容,它将变成一个表。否则,它将变成一个列。
• 元素的属性变成列。
• 如果元素是嵌套的,则它们之间的关系会自动创建(如上所述)。
在派生结构的过程中,首先(按照上面的规则)查看哪些元素需要有与之相关的表。然后,使用其余的元素和属性,在派生表中创建字段。
返回页首
使用 ADO.NET 从头开始创建 XML 文件
我们甚至可以在不定义 XML 输出格式的情况下,从头开始创建新的数据集并将它另存为 XML。我们只需定义 Dataset 的结构,在其中放一些数据,然后用WriteXml 方法保存它。在下例中,我们创建和保存一个包含客户和定单的 Dataset。此代码应当放在Create New XML File 按钮的事件中:
' Create DataSet
Dim MyDataSet As New DataSet("ManualDataSet")
' Create a new customer DataTable and add it to the DataSet
Dim CustomerDataTable As New DataTable("Customer")
MyDataSet.Tables.Add(CustomerDataTable)
' Create columns for the table, set their properties
' and add them to the Columns collection for the table.
Dim NewDataColumn As New DataColumn("CompanyName")
NewDataColumn.DataType = System.Type.GetType("System.String")
CustomerDataTable.Columns.Add(NewDataColumn)
NewDataColumn = New DataColumn("Customer_ID")
NewDataColumn.DataType = System.Type.GetType("System.Int32")
NewDataColumn.AutoIncrement = True
NewDataColumn.ColumnMapping = MappingType.Hidden
CustomerDataTable.Columns.Add(NewDataColumn)
' Create a DataRow, add it to the table, and set its values.
Dim NewCustomerRow As DataRow
NewCustomerRow = MyDataSet.Tables("Customer").NewRow
MyDataSet.Tables("Customer").Rows.Add(NewCustomerRow)
NewCustomerRow("CompanyName") = "New company"
MyDataSet.AcceptChanges()
' Now create order DataTable with appropriate structure
Dim OrderDataTable As New DataTable("Order")
MyDataSet.Tables.Add(OrderDataTable)
NewDataColumn = New DataColumn("OrderDate")
NewDataColumn.DataType = System.Type.GetType("System.String")
OrderDataTable.Columns.Add(NewDataColumn)
NewDataColumn = New DataColumn("OrderTotal")
NewDataColumn.DataType = System.Type.GetType("System.Currency")
OrderDataTable.Columns.Add(NewDataColumn)
NewDataColumn = New DataColumn("Customer_ID")
NewDataColumn.DataType = System.Type.GetType("System.Int32")
NewDataColumn.ColumnMapping = MappingType.Hidden
OrderDataTable.Columns.Add(NewDataColumn)
' Add an order for the customer created earlier.
Dim NewOrderRow As DataRow
NewOrderRow = OrderDataTable.NewRow
NewOrderRow.Item("OrderDate") = "04-15-2002"
NewOrderRow.Item("OrderTotal") = 777.77
NewOrderRow.Item("Customer_ID") = NewCustomerRow.Item("Customer_ID")
OrderDataTable.Rows.Add(NewOrderRow)
' Create the relationship between customer and order. Have to
' specify the name of the relationship, the column in the
' parent table and the column in the child table.
Dim NewRelationship As New DataRelation("Customer_Order", _
CustomerDataTable.Columns("Customer_Id"), _
OrderDataTable.Columns("Customer_Id"))
MyDataSet.Relations.Add(NewRelationship)
DataGrid1.DataSource = MyDataSet
MyDataSet.WriteXml("C:\Development\XMLFiles\Output2.xml")
如果您运行该程序并单击Create New XML File 按钮,该网格将转而显示新创建的 Dataset。除了其中的记录较少之外,它的行为与旧的 Dataset 完全一样。
如果您在最后一行中检查由WriteXml 方法创建的 XML 文件,它看上去将如下所示:
<?xml version="1.0" standalone="yes"?>
<ManualDataSet>
<Customer>
<CompanyName>New company</CompanyName>
</Customer>
<Order>
<OrderDate>04-15-2002</OrderDate>
<OrderTotal>777.77</OrderTotal>
</Order>
</ManualDataSet>
尽管它在导出为 XML 之前完全是从头开始创建的,但它看上去与在本文前面手动创建的 XML 文件非常相似。
返回页首
小结
本文中的任何示例都不要求代码使用节点、元素、命名空间、属性或其他 XML 概念。它们演示了如何使用与操作关系数据库所用的逻辑同样的逻辑来访问和操作 XML 格式的数据。这与在许多开发情形下常用的编程模型的 .NET 原理一致。
请记住,并非所有的情形都适合使用这些技术。对于一些 XML 应用程序,仍需要认真考虑 XML 文件的系统级结构。但是,对于常见的数据操作任务,如果使用本文中的技术从 XML 中读取数据并将所做的更改另存为 XML,则所需的代码和工作会少很多。XML 最终会成为与 ASCII 一样透明的基础数据存储标准,本文讨论的方法为这一天的到来指明了方向。