UML软件工程组织

解决数据集冲突
Brian A. Randell
MCW Technologies, LLC
摘要:讨论如何在断开状态下处理数据,并为解决数据集冲突提供一种可能的解决方案。该解决方案包含示例代码,其实现是通过 Visual Basic .NET、ADO.NET 和 .NET Framework 来完成的。(本文包含一些指向英文站点的链接。)

下载 DataSetConflicts.msi 示例文件

注意:要运行示例应用程序,需要的环境为安装有 .NET Framework 1.0 SP2 的 Microsoft Windows®。本文中出现的所有代码均为 Visual Basic® .NET 版本,是使用 Visual Studio® 2002 编写并测试的。另外,需要访问计算机上的 SQL Server 2000 SP3 实例,而且示例数据库需要具有至少 3 MB 的可用磁盘空间。测试是通过使用 Windows XP Professional SP1 上的 SQL Server 2000 Personal Edition, SP3 本地实例来完成的。

简介

.NET Framework 第一个发行版中引入的新 ADO.NET 数据集对象为在断开状态下处理数据提供了很多选项。它可以被绑定到用户界面 (UI) 对象,例如数据网格。而且支持通过网络线路按值进行封送处理。在更改被发送回数据源之前,它甚至还支持脱机处理数据。正是由于这种特定脱机处理导致出现问题。

通过断开数据与实时数据库的连接,能够增加应用程序的可缩放性和灵活性。例如,您可以使用拨号方式连接到您的企业网络,将某些数据下载到数据集中,然后断开连接并继续工作。以后,您可以重新连接并更新数据库。但是,如果另一个用户更改了您正在使用的同一个记录,将会发生什么情况?更重要的是,“应该”发生什么情况?

本示例应用程序为这个问题提供了“一种”可能的解决方案。与任何复杂问题一样,每个应用程序都不存在唯一适合的答案。实际上,本示例并不是有意将重点放在一种解决方案上。正如您将看到的那样,需要做出许多决定来为某种情况找出最合适的操作。本示例提供一个起始点来帮助您为自己的应用程序创建正确的解决方案。

使用应用程序

开始时,请运行经过编译的应用程序 DataSetConflicts.exe,或打开 Visual Studio® .NET 解决方案 DataSetConflicts.sln。当首次启动该应用程序时,它看上去有些类似于下面的图 1。在首次启动时,只有一个真正的选项,用于建立运行该应用程序所需的示例数据库。虽然可能比较容易在某个现有的 SQL Server 示例数据库(例如 PUBS)上建立应用程序,但是许多开发者会由于数据库不是自己创建的而觉得不舒服。因此,本示例使用名为 DataConSample 的简单单表数据库。建立数据库需要 SQL Server 账户,此账户具有访问主数据库的必要权限和创建示例数据库的权限,并且可以访问已安装的示例表。

单击此处查看大图像

图 1:数据集冲突解决程序

运行示例应用程序时,从 Database(数据库)菜单中选择 Create(创建)命令。使用显示的对话框来指定数据库文件的存储位置。大概需要 3 MB(兆字节)的磁盘空间。选择位置后,就会显示 Log On Information(登录信息)对话框,如图 2 所示。首先,如果本地主机不正确,请输入服务器的名称。然后,选择验证机制,如有必要,请输入您的姓名和密码。验证凭据时需要该对话框。它将缓存每次会话期间的输入信息。指定所需的信息后,单击 Test Connection(测试连接)按钮检查是否连接到 SQL Server 安装。如果测试成功,则 OK(确定)按钮将启用。单击该按钮以继续。

单击此处查看大图像

图 2:登录信息对话框

这时,当安装了数据库并插入了示例数据后,会显示一个状态对话框。如果安装成功,Get Data(获取数据)按钮将启用,用于检索数据以在显示的 DataGrid 控件中进行本地编辑。为了可以容易地在数据库中创建冲突记录,启用 Simulate 2nd User(模拟第二用户)按钮。您可以现在单击它,也可以先执行您自己的本地编辑。有些记录您将要进行修改以确保发生并发冲突,这些记录是 ID 值为 10、30 和 50 的行。修改这些行的计数列的数值。更改后,单击 Simulate 2nd User(模拟第二用户)按钮(如果前面没有单击它),然后单击 Update(更新)按钮。

如果一切正常(并发冲突的频率为多高时,事件的标记正常?),您将看到一个消息框,告诉您出现了冲突(请参阅图 3)。

单击此处查看大图像

图 3:数据并发冲突消息框

单击 OK(确定)。每个存在冲突的行的开头都有一个红色的圆,圆内部带有一个感叹号,如图 4 所示。

单击此处查看大图像

图 4:冲突已发生

要解决冲突,选择某行(请参阅图 5)并单击 Resolve Selected Row(解决选定行)按钮。

单击此处查看大图像

图 5:存在冲突的一行

将出现 Choose a Row to Keep(选择要保留的一行)对话框,如图 6 所示,该对话框显示存在冲突的行的三个版本:

  1. 您修改后的版本
  2. 服务器的当前版本
  3. 原始版本
单击此处查看大图像

图 6: Choose a Row to Keep (选择要保留的一行)对话框

决定要保留行的哪个版本并单击相应的按钮。有三种选择:

  • 保留您的更改。
  • 接受另一个用户所作的更改。
  • 启动时,将行回滚到原始状态。

这三个选项对应的三个命令按钮分别为 Keep My Changes(保留我的更改)、Keep Server Changes(保留服务器更改)和 Keep Original Data(保留原始数据)。每行都显示在一个只读网格中,以便您比较每列的更改。确定保留哪行后,窗体将关闭,并允许您纠正剩余的行。重复操作,直到所有行旁边不再有警告图标。

解决了所有行的冲突后,再次单击 Update(更新)按钮以提交纠正后的行。如果不再存在冲突,而且也不应该再存在,您将收到一条消息,提示已成功完成更新。如果完成了所有操作,可以退出示例应用程序,通过从 Database(数据库)菜单中选择 Reload Data(重新加载数据)命令将表格数据重置到起始状态,或者通过从 Database(数据库)菜单中选择 Remove Database(删除数据库)命令完全删除数据库。

将示例应用程序分成不同的部分,以便将它的各个部分集成到自己的应用程序中。为此,您必须了解示例的结构及其设计和实现中的内在限制。

应用程序设计问题

为了提供一个基础供您以后在其上建立应用程序,已将用户界面和脱机数据管理代码同实际与数据库交互的代码分离。另外,支持函数(例如安装数据库所需的代码)、加载示例数据和删除数据库被分割成附加的外部 DLL。打开 DataSetConflicts.sln(如果尚未打开),注意有五个项目 - 一个 Windows 应用程序和四个类库。

用户体验

核心 UI 在 DataSetConflicts 项目中定义。它包含三个窗体 - frmMain(请参阅图 1)、frmDiffData(请参阅图 4)和 frmAbout。显然,本示例的重点是 frmMain 的 UI 设计。我不能确定您是否要将 Simulate 2nd User(模拟第二用户)按钮包含到您的应用程序中。但是,其他两个窗体在您的应用程序中将会有用。

为了增强用户体验,示例应用程序在用户对用户基础上存储配置数据。.NET 启用的应用程序支持 XML 配置文件。文件名与程序集相同,以 .config 为后缀。正常情况下,该文件与其父程序集存储在相同的目录中。如果该文件存在,.NET 运行时会自动加载并分析它。使用 System.Configuration 名称空间中定义的 ConfigurationSettings 类可以得到 appSettings 元素中存储的值。配置文件的 appSettings 部分存储名为 add 的元素中定义的名称/值对,名称存储在 key 属性中,值存储在 value 属性中。这些数值被加载并成为 ConfigurationSettings 集合中的内容,其属性为只读。此数据为“应用程序数据”,通常只能由管理员更改。用户特定信息应该存储在位于 Windows 可以使用的某个标准目录中的用户特定配置文件中。

对于本示例应用程序,用户配置文件存储在 System.Environment.GetFolderPathSystem 函数返回的路径中,调用该函数时 Environment.SpecialFolder.ApplicationData 被作为参数传递。例如,在我的 Windows XP Professional 计算机上,使用本地帐户 Brian 登录,返回的值为 C:\Documents and Settings\Brian\Application Data。在此基础上,应用程序追加常量 CONFIG_FILE_DIR_BASE (\Microsoft\VB.NET Samples\{0}\v{1}\{2}) 中定义的字符串,通过 String.Format 来用适当的值替换格式规范;应用程序标题替换 {0},版本号替换 {1},集合名称替换 {2}。这样,在我的计算机上,配置文件的存储位置为 C:\Documents and Settings\Brian\Application Data\Microsoft\VB.NET Samples\DataSet Conflicts Resolver\v1.0.0.0\DataSetConflicts.exe.config。表 1 列出了通过示例应用程序能够存储在用户的 XML 配置文件中的数据。

表 1:用户配置文件值

项名称 默认值 示例 说明
DBFilesExist False True 如果两个示例数据库文件存在,则为 True;否则为 False。
DBPath "" C:\Samples 存储两个示例数据库文件的路径。
DBServerName localhost localhost 示例数据库所在的 SQL Server 的名称。
TrustedConnection True True 如果与 SQL Server 的连接为受信任连接,则为 True;否则为 False。如果值为 False,首次进行连接时,应用程序将提示提供用户 ID 和密码,而且仅在应用程序会话期间缓存连接。

AppSettings 类提供了加载(和必要时创建)用户配置文件时需要的所有函数。该类用一些简单的过程来抽象 XML 访问方法 (XML DOM)。在 frmMain 的 Load 事件中,创建了 AppSettings 类的一个实例。加载或创建文件后,文件的内容被加载到 ConfigItems 类的属性中。如果该项不存在,将插入上面表 1 中所示的默认值。

加载配置数据后,将运行函数 CheckDBFilesExistance 以验证数据库文件是否存在。

Private Function CheckDBFilesExistance() As Boolean
Try
Dim strDBFile As String = ConfigItems.DBPath & Me.DB_MAIN_FILE_NAME
Dim strLogFile As String = ConfigItems.DBPath & Me.DB_LOG_FILE_NAME
Dim blnRetVal As Boolean = _ 
File.Exists(strDBFile) AndAlso File.Exists(strLogFile)

mAppSettings.SetValue("DBFilesExist", blnRetVal.ToString())
ConfigItems.DBFilesExist = blnRetVal
mAppSettings.Save()

Return blnRetVal
Catch exp As Exception
MessageBox.Show(exp.Message, exp.Source, _ 
MessageBoxButtons.OK, MessageBoxIcon.Error)
End Try
End Function

在数据库文件存在的基础上,设置 UI。请注意 FormatGrid 方法,它定义主数据网格的外观。随后,frmDiffData 使用该例程来确保查看网格窗体中的数据时具有一致的外观。此时,存在两个代码路径:数据库管理和数据管理。数据库管理代码非常有趣,但不是本示例应用程序的重点。您可以按照自己的意愿研究它。您将发现大多数数据库管理代码都是在 BuildDBSS 项目中定义的。数据管理代码可以在 DataSetConflicts 或 DataSS 中定义。

访问数据库

DataSS 程序集封装了检索和更新示例 SQL Server 2000 数据库中存储的数据时所需的 ADO.NET 代码。在本 frmMain 示例中,客户端代码使用 IExData 接口引用(在 SyncDataIntf 项目中定义)实例化了 SQLServer2000 类的一个实例。使用该设计,您无需过多地修改客户端代码就可以将 UI 挂钩到另一个数据库,例如 Microsoft Jet。

工作原理

至此,您可以运行应用程序并基本了解了程序结构,下面需要了解示例应用程序的核心设计问题。如何处理并发冲突?

获取数据

ADO.NET 断开数据模型的核心是数据集对象。数据集对象是数据源不可知的对象,该对象能够将矩形数据缓存在一个或多个 DataTable 对象中。数据集对象支持值封送语义,这就允许使用 .NET Remoting、Web 服务或其他协议在各层中移动数据集对象。另外,您可以使用自己的格式化程序将其序列化为 XML 格式或自定义格式。虽然是通过在同一台计算机上运行所有代码完成了本示例应用程序的编写和测试,但是,并不表示不能将该程序分成不同的部分来利用 Web 服务。

正如前面提到的那样,SQLServer2000 类的实例用于实现本示例应用程序中的所有数据访问。客户端应用程序执行 GetDataDSCB 函数。GetDataDSCB 函数具有两个重载版本:一个版本有六个参数,另一个有七个参数。公用参数如下:

  • SQL:一个字符串,包含从数据库中选择记录的动态 SQL。
  • DataSetName:用于命名数据集的字符串。
  • DataTableName:用于命名 DataTable 的字符串。
  • PrimaryKeyName:用于定义表示表中主关键字列的列名的字符串。
  • RecordsReturned:一个传址整数,将被设置成从数据库中检索到的记录数目。
  • Return Value:一个数据集对象,包含从数据库检索到的数据。

另外,七个参数的版本还使用 ConnectionString 参数。该版本包含用于访问 SQL Server 数据库的核心代码。首先,设置连接字符串。然后,使用 ValidateSelectSQL 函数验证动态 SQL 字符串。该函数执行一个简单的验证,以确保所有单引号被正确换码。觉得合适您可以随意增强验证。

此时,GetDataDSCB 将使用内部方法 OpenDataSource 来初始化模块级 SQLConnection 实例。如果成功,将使用用户的经过验证的 SQL 和连接对象来创建一个 SQLCommand 对象。然后,使用上面设置的 SQLCommand 对象作为输入来初始化 SqlDataAdapter。接着,实例化并命名本地数据集对象。最后,使用 SqlDataAdapter 中的 Fill 方法为数据集填充数据,来在进程中创建一个名为 DataTable 的对象。一旦加载了数据集,就定义了主关键字。虽然 SqlDataAdapter 确实提供了 FillSchema 方法,但由于性能方面的原因,通常应该避免使用它。另外,本示例中不存在任何要处理的关系或约束。数据集填充完后,该方法将其返回到能够操作它的客户呼叫方。

客户方更新

客户检索数据后,设置模块级 DataTable 实例。本示例具有挂钩到 DataTable 的 ColumnChanged 事件的事件处理程序。当用户更改可编辑的列时,该事件会更新 LastUpdatedByLastUpdatedDate 列。请注意,这两个列被指定添加到表的设计中,以便帮助您查看谁更改了记录并导致产生并发冲突。然后,获取 CurrencyManager 对象。该对象提供了 PositionChanged 事件,当用户使用数据网格从一行浏览到另一行时,该事件用于确保 UI 处于适当的状态。一旦挂钩了 CurrencyManager,代码将访问当前的 DataRowView 对象,以便掌握 CurrencyManager 正在使用的 DataView。将引用 mdvCM 存储起来,以便以后使用 cboRowFilter 组合框来更改网格中显示的记录。

至此,还有一些事情没有做。其一,要初始化三个模块级 DataView 对象,以供以后查看数据更改时使用。其二,使用 DataSourceDataMember 属性将检索到的数据绑定到网格。其三,将 cboRowFilter 的 SelectedItem 设置成 mdvCM 的当前 RowStateFilter,默认值为 CurrentRows。现在只剩下更新用户界面了。最有趣的例程是 DisplayRowInfo。该例程由 CurrencyManager 的 PositionChanged 事件调用,如有必要,也可以由 btnGetData_Click 等其他事件调用。

Private Sub DisplayRowInfo()
   If Me.mobjCurrMgr.Position = -1 AndAlso Me.mdt.Rows.Count > 0 Then
      Me.mobjCurrMgr.Position = 0
   End If

   Dim localRow As DataRow = _ 
CType(Me.mobjCurrMgr.Current, DataRowView).Row
   mstrCurrentRowFilter = _ 
String.Format(Me.BASE_ROW_FILTER, Me.mstrPrimaryKeyName, _ 
localRow(Me.mstrPrimaryKeyName).ToString())

   Const BASE_STATUS_TEXT As String = "Current Row's State: {0}"

   Me.lblStatus.Text = _ 
String.Format(BASE_STATUS_TEXT, localRow.RowState.ToString())

   If localRow.HasErrors Then
      Me.lblStatus.Text &= vbCrLf & "Error: " & _ 
localRow.RowError & vbCrLf & _ 
"Click the 'Resolve Selected Row' button to fix."
If mdsConflicts Is Nothing Then
Me.btnResolve.Enabled = False
Else
         Dim svrRow As DataRow
Try
Dim objKey As Object = _ 
CType(localRow(Me.mstrPrimaryKeyName), Object)
svrRow = mdsConflicts.Tables(0).Rows.Find(objKey)
         Catch exp As Exception
MessageBox.Show(exp.Message, "DisplayRowInfo", _ MessageBoxButtons.OK, _ 
MessageBoxIcon.Error)
         End Try
         Me.btnResolve.Enabled = (Not svrRow Is Nothing)
      End If
   Else
      Me.btnResolve.Enabled = False
   End If

   Me.btnUpdate.Enabled = ((Not localRow.Table.HasErrors) _ 
AndAlso (localRow.Table.DataSet.HasChanges))

   Me.BindingContext(Me.mdvOriginal).Position = Me.mobjCurrMgr.Position
End Sub

DisplayRowInfo 首先检查是否存在当前行。创建行过滤字符串并将它存储在 mstrCurrentRowFilter 变量中,以便以后比较本地数据和服务器数据时使用。接下来的一个代码块用于处理冲突。每个 DataRow 对象都具有 HasErrors 属性,该属性已经通过服务器更新代码(后面的“获取服务器中的最新版本”小节对此进行了详细说明)设置为 True。如果该属性为 True,则检查是否已初始化 mdsConflicts 数据集。如果已初始化,则创建包含当前行主关键字值的 Object 变量。该值用于定位包含服务器中最新版本的匹配行。这要使用 DataTable 的 Rows 集合的 Find 方法来完成。将要使用的 DataTable 存储在 mdsConflicts 中。当更新数据集时(后面的“获取服务器中的最新版本”小节也对此进行了详细说明),数据集中填充有存在冲突的行。如果发现冲突行,则 Resolve Selected Row(解决选定行)按钮会启用。评估该行中的冲突后,将根据主数据集是否更改“和”当前行的 DataTable 中是否存在错误,来设置 Update(更新)按钮的启用属性。

正如前面提到的那样,当用户在行之间浏览时将调用 DisplayRowInfo。这样,一旦行被更改,Update(更新)按钮将启用,用于将更改发送到服务器。

将更改发送到服务器

将更改打包发送到服务器是在 btnUpdate_Click 事件处理程序中完成的。首先,仔细检查 mds 数据集以查看是否存在更改。然后,对于本示例,检查是否单击了 Simulate 2nd User(模拟第二用户)按钮。调用 mdata 接口引用的 UpdateTableDSCB 方法,并将 mds 数据集对象作为参数传递,该对象含有更改后的数据以及要更新的 DataTable 的名称。UpdateTableDSCB 方法将返回包含冲突行(如果存在)的数据集对象,并将冲突行存储在客户端的 mdsConflicts 对象中。方法调用完成后,通过检查 mds 数据集对象的 HasErrors 属性来检查是否还有错误。如果存在错误,由您决定如何处理。如果不存在错误,则调用数据集的 AcceptChanges 方法来将修改后的记录设置为当前记录,但设置之前必须进行实际更新。

UpdateTableDSCB 内幕

必须完成的首要任务是检查 SqlDataApdater(即 mdsa)是否具有有效的 UpdateCommandUpdateCommand 指定如何提交更改。您可以提供自己的自定义 SQL、存储过程,或者您可以以前面用来填充数据集的 SELECT 语句为基础,用 ADO.NET SqlCommandBuilder 创建动态 SQL 字符串。本示例使用 SqlCommandBuilder。生成器对象创建后,将调用它的 GetUpdateCommand 方法,来将返回值指定到 msda 对象的 UpdateCommand 属性。至此,所有将更新发送到服务器的准备工作都已完成,还有另外一个任务需要完成。

与 ADO 记录集对象不同,ADO.NET 数据集对象不提供 Resync 方法。该方法可用于同步本地缓存和服务器中更改的记录 - 处理冲突无疑是一种自制的方案。要使这种情况发生,您首先需要另一个 SqlDataAdapter 来检索冲突行。本示例应用程序中的代码提供参数化的 SQL 语句,它的成功是以假设不会更改主关键字列为基础的(总的说来是一个好假设,不过您也知道假设的含义)。设置第二个模块级数据适配器并将其命名为 msdaCon 后,就又创建了一个数据集对象 mdsCon

至此,几乎可以将更改实际发送到 SQL 服务器,但还需要完成最后一个任务。默认情况下,ADO.NET 将每个更改的记录发送到服务器,每次发送一个。如果出现错误,它将停止发送数据并抛出一个异常。这种情况存在的问题是一些记录已经被成功更新。许多开发者错误地认为 SqlDataAdapter 执行的是事务性的批处理更新。您可以做到这一点,但是您必须手动创建事务对象并亲自管理事务。使用事务存在的问题是用户没有办法在行到行的基础上决定结果。事务要么更改所有行,要么不进行任何更改。遇到一个冲突行,将停止更新进程并回滚对数据库所作的所有更改。成功的关键是无论是否出现错误,让 SqlDataAdapter 试着更新每一行。当行中存在错误时,从服务器中检索冲突记录,这样您就可以决定如何处理。要执行非默认操作,SqlDataAdapter 对象(本例中为 msda)的 ContinueUpdateOnError 属性必须设置成 True。完成此任务后,就可以调用 Update 方法。

获取服务器中的最新版本

自制更新工作的唯一方法是知道哪些记录更新失败。幸运的是(或者是经过设计的?),SqlDataAdapter 对象提供了可用于监视更新进程的一组事件。您感兴趣的是 RowUpdated 事件。在更新每一行后,该事件将启动。您真正感兴趣的仅是更新失败的行。要解决这个问题,您要使用 SqlRowUpdatedEventArgs 对象的 StatusErrors 属性。如果 StatusErrorsOccured 并且 Errors 实例是 System.Data.DBConcurrencyException,则告诉您需要从服务器中获取冲突记录。这可以使用前面初始化的第二个数据适配器 msdaCon 来完成。使用当前行的主关键字值,您可以设置由数据适配器的 SelectCommand 的参数集合提供的唯一参数。执行 Fill 方法,返回存在冲突的行。随后,通过初始化 RowError 属性,将错误行标记为存在错误。最后但并非最不重要的一点是,您需要通过将 e.Status 属性设置到 Continue 来告诉 msda 数据适配器继续进行处理。

选择要保留的版本

当更新命令完成时,通过检查发送到服务器的数据集是否存在错误,将 UI 更新到适当的状态。如果存在错误,根据需要启用或禁用控件。另外,数据网格会打开行错误指示器图标以显示存在冲突的行。您必须做的就是为用户提供一种查看错误的方法。数据网格本身能够向您显示具有当前值的已修改的记录,还可以显示具有原始值的已修改的记录。与 ADO 记录集对象不同,没有规定数据集显示第三个条件 - 当前服务器值。用户希望同时看到三个版本,实现这一点非常明智。这正是 frmDiffData 所做的。

当用户选择具有错误指示器的行时,Resolve Selected Row(解决选定行)按钮将启用。单击它将会传递要初始化的 frmDiffData 实例和冲突行的数据。三个 DataView 对象将传递到 frmDiffData 的 LoadGrids 方法。一个用于过滤以显示具有原始行数据的当前行。另一个用于显示处于修改状态的当前行数据。这两个 DataViewmds DataSet 对象为基础。第三个 DataView 以冲突数据集 mdsConflicts 为基础。通过使用 RowFilter 属性和设置编辑选项,每个视图都将被传递到本地的 SetDiffView 方法以过滤记录。加载网格后,窗体将以对话框模式显示。

现在用户可以看到三种状态的冲突行。如果有必要进行不同更改,标签为 My Changes 的网格允许用户交互式调整值。其他两个网格为只读。用户可以单击三个按钮中的一个来选取自己需要的行版本。每个按钮的事件处理程序以一种相似但不特定的格式来处理行解决方案。

如果单击 Keep My Changes(保留我的更改),则当前行将被修改。设置行的关键是让行进入一种状态,处于这种状态时,SqlDataAdapter 能够在服务器上找到该行并发送更改。正如前面提到的那样,本示例应用程序使用 SqlCommandBuilder 对象来定义更新命令。定义的 SQL 字符串创建 SQL WHERE 子句,该子句指定所有列必须匹配。服务器中的当前值需要加载到当前行,以便它们显示为原始值。然后需要在“新的”原始值顶端进行更改分层。为此,将当前修改和当前服务器值从各自的行对象中复制到独立数组 currDatasvrData 中。完成这一操作后,告诉数据行对象使用 RejectChanges 方法回滚修改,并通过调用 ClearErrors 来重置错误状态。此时,行的外观与本地用户更改以前的外观一样。现在复制原始数据,通过使用行的 ItemArray 方法,将服务器中的数据(存储在 svrData 数组中)复制到行中。然后调用 AcceptChanges 以使新的修改显示为当前值。最后,通过 ItemArray 方法将 currData 中存储的数据(即用户的真正更改)传递到行,并关闭窗体。

接收最新服务器更改的过程与单击 Keep Server Changes(保留服务器更改)按钮后发生的过程十分相似。主要的区别是您不必担心行的现有更改。为了支持服务器数据,它们不被接受。创建名为 svrData 的数组,其中包含当前服务器值。当前修改的行被告知转储其更改并重置其错误标记。使用 ItemArray 将服务器数据复制到行中。然后,与 Keep My Changes 代码不同,将调用 AcceptChanges 并关闭窗体。现在用户能够看到主窗体的数据网格中的最新值。

最后的选择是将行回滚到它的原始状态,不接受用户的本地更改和其他用户所做的任何更改。该程序的运行与 Keep My Changes 中的代码极为相似,并带有一个严重警告。当将当前行的数据复制到临时数组中时,使用 ItemArray 方法。问题是您需要 DataRowView 的值。该对像没有提供 ItemArray 方法,因此该代码只能进行手动复制。从这里开始,几乎全部相同。使服务器数据成为原始数据,然后将真正的原始数据置于服务器数据的顶端,使它看起来像进行了修改,然后关闭窗体。

重复此步骤,直到解决了所有的行。没有错误后,主窗体上的 Update(更新)按钮将再次启用,用于重新提交更改后的行。重复此步骤,直到不再有错误或用户不希望继续解决冲突,并关闭应用程序。

小结

检查本示例应用程序中的代码时,请注意这可能是一个具有侧重点的示例。它仅说明使用 Visual Basic® .NET、ADO.NET 和 .NET Framework 可以做什么。作为练习,您可以试着将数据访问代码更改成使用 Microsoft Jet 或其他一些数据源。也可以试着将数据访问代码更改成使用存储过程,而不是动态 SQL。

参考站点

有关其他信息,请参阅 .NET Framework SDK 和/或 Visual Studio .NET 附带的文档。另外,如果您的经济条件较好,或您的家人准备送给您假期礼物,下面是我特别喜欢的两本 ADO.NET 书:

  • Microsoft ADO.NET by David Sceppa [Microsoft Press, 2002, 0735614237]
  • Essential ADO.NET by Bob Beauchemin [Addison Wesley Professional, 2002, 0201758660]
 

版权所有:UML软件工程组织