第 2 章 — 处理数据
发布日期: 8/20/2004 | 更新日期: 8/20/2004
本页内容
在智能客户端中,可在客户端上使用应用程序数据。要使您的智能客户端有效工作,很重要的一点是对该数据进行适当的管理,以确保其有效、一致和安全。
可以通过服务器端应用程序(例如,通过 Web 服务)向客户端提供应用程序数据,或者应用程序可以使用它自己的本地数据。如果数据是由应用程序提供的,则智能客户端应用程序可以缓存数据以改善性能或者支持脱机使用。在这种情况下,您需要决定客户端应用程序应该如何处理就该服务器而言已经过时的数据。
如果智能客户端应用程序提供在本地修改数据的能力,则必须在以后将客户端更改与服务器端应用程序进行同步。在这种情况下,您必须决定客户端应用程序如何处理数据冲突,以及如何跟踪需要发送到服务器的更改。
在设计您的智能客户端应用程序时,您需要认真考虑这些问题以及其他许多问题。本章分析了在客户端上处理数据时的各种注意事项,包括:
• |
数据类型。 |
• |
缓存数据。 |
• |
数据并发。 |
• |
使用 ADO.NET 数据集来管理数据。 |
• |
Windows 窗体数据绑定。 |
本章未讨论其他许多与处理数据有关的问题。具体说来,在第 5 章:安全性注意事项中讨论了数据处理安全性问题,在第 4 章:偶尔连接的智能客户端中讨论了脱机注意事项。
数据类型
智能客户端通常必须处理两种类别的数据:
通常情况下,需要以不同的方式处理这些类型的数据,因此更详细地分析一下每种类型将是很有用的。
只读引用数据
只读引用数据是不会由客户端更改并且被客户端用于引用目的的数据。因此,从客户端的观点看来,该数据为只读数据,并且客户端不会对其执行更新、插入或删除操作。只读引用数据很容易在客户端上进行缓存。引用数据在智能客户端应用程序中具有许多种用途,包括:
• |
提供静态引用或查找数据。这方面的示例包括产品信息、价格表、发货选项和价格。 |
• |
支持数据验证,允许检查用户输入数据的正确性。示例有针对交货时间表检查输入的日期。 |
• |
帮助与远程服务进行通讯。示例在本地将用户选择转化为产品 ID,然后将该信息发送到 Web 服务。 |
• |
呈现数据。示例包括呈现帮助文本或用户界面标签。 |
通过在客户端上存储和使用引用数据,您可以减少需要从客户端传输到服务器的数据量,改善应用程序的性能,帮助启用脱机功能,并提供早期数据验证以提高应用程序的可用性。
尽管客户端无法更改只读引用数据,但可以在服务器上进行更改(例如,由管理员或超级用户更改)。您需要确定在发生数据更改时用于更新客户端的策略。此类策略可能涉及到在发生更改时将更改推到客户端上,或者按照特定的时间间隔或在客户端上执行某些操作之前从服务器拉入更改。但是,因为数据在客户端上是只读的,所以您无须跟踪客户端更改。这就简化了需要对只读引用数据进行处理的方式。
瞬态数据
瞬态数据既可以在服务器上更改,也可以在客户端上更改。通常情况下,瞬态数据作为用户输入和操作的直接或间接结果而发生更改。在此情况下,在客户端或服务器进行的更改都需要在某个时刻进行同步。这种类型的数据在智能客户端中具有许多种用途,包括:
• |
添加新信息。示例包括添加银行业务交易或客户详细信息。 |
• |
修改现有信息。示例更新客户详细信息。 |
• |
删除现有信息。示例从数据库中删除客户。 |
在智能客户端处理瞬态数据的最困难的方面之一在于,这些数据通常可能在多个客户端上同时进行修改。当数据非常不稳定时,该问题将恶化,因为所做更改更有可能互相冲突。
您需要跟踪您对瞬态数据进行的任何客户端更改。在与服务器同步数据并且已经解决任何冲突之前,您不应该认为瞬态数据已被确认。您应该非常小心以避免依赖未确认的数据进行重要决策,或者在未认真考虑如何保证数据一致性(甚至在同步失败时)的情况下使用该数据作为其他本地更改的基础。
有关围绕脱机时处理数据的问题以及如何处理数据同步的详细信息,请参阅第 4 章:偶尔连接的智能客户端。
缓存数据
智能客户端通常需要在本地缓存数据(无论是只读引用数据还是瞬态数据)。通过缓存数据,有可能改善应用程序的性能并提供脱机工作所需的数据。但是,您需要认真考虑在客户端缓存哪些数据、如何管理这些数据以及可以在哪个上下文中使用这些数据。
要启用数据缓存,您的智能客户端应用程序应该实现某种形式的缓存基础结构,以便透明地处理数据缓存细节。您的缓存基础结构应该包括下列缓存机制中的一种或两种:
• |
短期数据缓存。在内存中缓存数据对性能有益,但不能持久,因此您可能需要在重新运行应用程序时从源拉入数据。这样做可能会妨碍您的应用程序脱机操作。 |
• |
长期数据缓存。通过在持久性媒体(如独立存储或本地文件系统)中缓存数据,可以在没有连接到服务器时使用应用程序。您可以选择将长期存储与短期存储结合起来以改善性能。 |
无论您采用哪种缓存机制,都应该确保仅将用户有权访问的数据提供给客户端。而且,在客户端缓存的敏感数据要求进行认真处理以确保它的安全。因此,您可能需要在将数据传输到客户端以及在客户端存储数据时,对数据进行加密。有关详细信息,请参阅第 5 章:安全性注意事项中的“处理敏感数据”。
当您设计智能客户端以支持数据缓存时,您应该考虑为客户端提供一种请求新数据的机制,而无论缓存的状态如何。这意味着您可以确保应用程序随时能够执行新的事务,并且不会使用过时的数据。您还可以将客户端配置为预先获取数据,以便减少在缓存数据到期时处于脱机状态的风险。
只要有可能,您都应该将某种形式的元数据与该数据关联起来,以便使客户端能够以聪明的方式管理这些数据。此类元数据可用于指定数据的标识和任何约束,或者指定所需的与该数据关联的行为。您的客户端缓存基础结构应该消耗该元数据,并且使用它来适当处理缓存的数据。
客户端缓存的所有数据都应该是可以唯一标识的(例如,通过版本号或日期戳),以便在确定是否需要更新数据时,可以正确地识别相应的数据。这样,您的缓存基础结构就能够询问服务器它所具有的数据当前是否有效,并且确定是否需要进行更新。
元数据还可以用来指定与缓存数据的使用和处理相关的约束或行为。示例包括:
• |
时间约束。这些约束指定可以使用缓存数据的时间或日期范围。当该数据过时或到期时,可以将其从缓存中丢弃,或者通过从服务器获取最新数据来自动刷新数据。在某些情况下,合适的做法可能是让客户端使用过时的引用数据,并且在与服务器进行同步时将过时数据映射到最新数据。 |
• |
地理约束。某些数据可能仅适用于特定地区。例如,您可能对于不同的地点有不同的价格表。可以使用您的缓存基础结构分别针对不同的地点来访问和存储数据。 |
• |
安全性要求。可以将专门提供给特定用户的数据加密,以确保只有相应的用户可以访问这些数据。在此情况下,所提供的数据已经进行了加密,并且用户必须向缓存基础结构提供凭据以便对数据进行解密。 |
• |
业务规则。您可能拥有与缓存数据关联的业务规则,用来规定如何使用这些数据。例如,您的缓存基础结构可能考虑用户的角色,以便确定向该用户提供哪些数据以及如何处理这些数据。 |
您的缓存基础结构可以通过与数据关联的元数据来适当地处理这些数据,从而使您的应用程序无须关心数据缓存问题或实现细节。您可以在引用数据本身内部传递与这些数据关联的元数据,或者您可以使用带外机制。用于将元数据传输到客户端的确切机制取决于您的应用程序与网络服务的通讯方式。当使用 Web 服务时,利用 SOAP 头将元数据传输到客户端是一种很好的解决方案。
只读引用数据与瞬态数据之间存在的区别有时意味着您需要使用两个缓存,一个用于引用数据,一个用于瞬态数据。引用数据在客户端是只读的,并且不需要回过头来与服务器进行同步,但它的确需要偶尔进行刷新以反映在服务器上进行的任何更改和更新。
瞬态数据既可以在服务器上更改,也可以在客户端上更改。既然有时在客户端更新缓存中的数据,有时在服务器更新,有时在这两个位置更新,那么对客户端数据进行的更改需要在某个时刻与服务器进行同步。如果数据同时在服务器上发生了更改,则会发生数据冲突,需要对其进行适当的处理。
要帮助确保维持数据一致性,并且避免不适当地使用数据,您应该小心地跟踪您在客户端对瞬态数据进行的任何更改。在成功地与服务器进行同步或确认之前,此类更改是未提交的 或暂定的。
您应该对您的智能客户端应用程序进行适当的设计,以使其能够区分已经成功地与服务器进行同步的数据和仍然暂定的数据。这一区分过程可以帮助应用程序更加容易地检测和处理数据冲突。而且,您可能需要禁止应用程序或用户基于暂定数据进行重要决策或者启动重要操作。在将此类数据与服务器进行同步之前,不应该依赖它们。通过使用适当的缓存基础结构,可以跟踪暂定数据和已经确认的数据。
缓存应用程序块(Caching Application Block)
缓存应用程序块是一个 Microsoft? .NET 框架扩展,它使开发人员可以容易地缓存来自服务提供程序的数据。生成和设计它的目的是将 Microsoft 建议的缓存准则封装在 .NET 框架应用程序中,如位于 http://msdn.microsoft.com/library/en-us/dnbda/html/CachingArch.asp 的 Caching Architecture Guide for .NET Framework Applicationss 所述。
缓存块的总体体系结构如图 2.1 所示。
图 2.1 缓存块工作流
缓存工作流包含下列步骤:
1. |
客户端或服务代理向 CacheManager 发出对缓存数据项的请求。 |
2. |
如果该数据项已被缓存,则 CacheManager 会从存储中检索该项,并将其作为 CacheItem 对象返回。如果该项尚未缓存,则会通知客户端。 |
3. |
在从服务提供程序检索未缓存的数据之后,客户端将该数据发送给 CacheManager。CacheManager 会将一个签名(即,元数据)如密钥、到期时间或优先级等添加到该数据项中,并将其加载到缓存中。 |
4. |
CacheService 监控 CacheItems 的生存期。当 CacheItem 到期时,CacheService 会删除它并根据情况调用回调委托。 |
5. |
CacheService 还可以将所有数据项从缓存中清除出去。 |
缓存块提供了多种缓存到期选项,如表 2.1 所述。
表 2.1 缓存块到期选项
AbsoluteTime |
用于设置到期时间的绝对时间。 |
ExtendedFormatTime |
用于基于表达式(如 every minute 或 every Monday)设置到期时间。 |
FileDependency |
用于基于文件是否更改来设置到期时间。 |
SlidingTime |
用于设置项的生存期,方法是基于项的上次访问时间来指定到期时间。 |
下列存储机制可供缓存块使用:
• |
内存映射文件 (MMF)。MMF 最适合于基于客户端的高性能缓存方案。您可以使用 MMF 来开发可在同一台计算机中的多个应用程序域和进程之间共享的缓存。.NET 框架不支持 MMF,因此 MMF 缓存的任何实现都以非托管代码的形式运行,并且不会从任何 .NET 框架功能中受益,包括内存管理功能(如垃圾回收)和安全性功能(如代码访问安全性)。 |
• |
Singleton 对象。可以使用 .NET 远程处理 singleton 对象来缓存可在一台或多台计算机中的进程之间共享的数据。方法是使用通过 .NET 远程处理为多个客户端提供服务的 singleton 对象来实现缓存服务。单例缓存的实现很简单,但它缺乏基于 Microsoft SQL Server™ 的解决方案所提供的性能和可伸缩性。 |
• |
Microsoft SQL Server 2000 数据库。SQL Server 2000 存储最适合于应用程序要求具有高持续性或者您需要缓存大量数据的场合。因为缓存服务需要通过网络访问 SQL Server,并且使用数据库查询检索数据,所以数据访问的速度相对比较慢。 |
• |
Microsoft SQL Server 桌面引擎 (MSDE)。MSDE 是 SQL Server 2000 的轻型数据库替代产品。它提供了可靠性和安全性功能,但具有比 SQL Server 更小的客户端足迹,因此它需要较少的设置和配置。因为 MSDE 支持 SQL,所以开发人员可以得到数据库的很多功能。如有必要,您可以将 MSDE 数据库迁移到 SQL Server 数据库。 |
数据并发
正如前面所提到的,使用智能客户端的一个问题是:在将任何客户端更改与服务器进行同步之前,服务器上保存的数据可能发生更改。您需要采用某种机制来确保在对数据进行同步时,数据冲突能够得到适当的处理,并且最后得到的数据是一致和正确的。数据能够由多个客户端进行更新的能力称为“数据并发”。
您可以使用两种方法来处理数据并发:
• |
保守式并发。保守式并发允许一个客户端保持数据上的锁,以禁止任何其他客户端修改数据,直至客户端自己的更改完成为止。在这种情况下,如果另一个客户端尝试修改数据,则在锁的拥有者释放该锁之前,这些尝试将失败或者被阻止。 |
• |
保守式并发可能有问题,因为单个用户或客户端可能由于疏忽而长时间地保持锁定。所以,该锁可能会妨碍重要资源(如数据库行或文件)及时得到释放,从而严重影响应用程序的可伸缩性和可用性。但是,当您需要完全控制对重要资源所做的更改时,保守式并发可能是适当的。请注意,如果您的客户端要脱机工作,则不能使用这种并发,因为客户端无法对数据加锁。 |
• |
开放式并发。开放式并发不会锁定数据。要判断是否实际需要更新,可以将原始数据随更新请求和已更改的数据一起发送。随后,将针对当前数据检查原始数据,以查看是否同时对原始数据进行了更新。如果原始数据和当前数据匹配,则执行更新;否则,拒绝请求,并产生开放式失败。要优化该过程,您可以在数据中使用时间戳或更新计数器,而不必发送原始数据,此时只需要检查时间戳或计数器。
开放式并发提供了一种良好的机制,可用来更新不会非常频繁更改的主数据,如客户的电话号码或地址。开放式并发允许每个人读取数据,在发生更新的概率小于读取操作的情况下,开放式失败的风险或许是可以接受的。在数据频繁更改以及开放式更新可能经常失败的情况下,开放式并发可能并不适合。 |
在大多数智能客户端方案(包括客户端将要脱机工作的方案)中,开放式并发是正确的方法,因为它允许多个客户端同时使用数据,而不会不必要地锁定数据和影响所有其他客户端。
有关开放式和保守式并发的详细信息,请参阅 .NET Framework Developer's Guide 中的“Optimistic Concurrency”,网址为:http://msdn.microsoft.com/library/en-us/cpguide/html/cpconoptimisticconcurrency.asp。
使用 ADO.NET 数据集来管理数据
DataSet 是一个表示一个或多个关系数据库表的对象。数据集在断开连接的缓存中存储数据。DataSets的结构与关系数据库类似:它公开了一个由表、行和列组成的层次结构对象模型。另外,它还包含为DataSets定义的约束和关系。
ADO.NET DataSet 包含零个或更多个由 DataTable 对象表示的表组成的集合。DataTable 在 System.Data 命名空间中定义,并且表示单个由内存驻留数据组成的表。它包含由 DataColumnCollection 表示的列和由 ConstraintCollection 表示的约束组成的集合,它们共同定义了该表的架构。DataTable 还包含由 DataRowCollection(它包含该表中的数据)表示的行组成的集合。与其当前状态一起,DataRow 保留其当前版本和原始版本,以便标识对该行中存储的值所做的更改。
DataSets可以强类型化或非类型化。类型化的 DataSet 从 DataSet 基类继承,但是向 DataSet 中添加了强类型化的语言功能,从而使用户可以用更加强类型化的编程方式访问内容。在生成应用程序时,可以使用任一种类型。但是,Microsoft Visual Studio ® 开发系统对类型化DataSets具有更多支持,它们使得用DataSets编程变得更加容易,而且更不容易出错。
DataSets在智能客户端环境中尤其有用,因为它们提供了能够帮助客户端在脱机状态下使用数据的功能。它们可以跟踪对数据进行的本地更改,这有助于与服务器同步数据以及协调数据冲突,并且它们还可用于合并来自不同源的数据。
有关如何使用DataSets的详细信息,请参阅 Visual Basic and Visual C# Concepts 中的“Introduction to DataSets”,网址为:http://msdn.microsoft.com/library/en-us/vbcon/html/vbconDataSets.asp。
用DataSets合并数据
DataSets具有将 DataSet、DataTable 或 DataRow 对象的内容合并到现有DataSets的能力。对于跟踪在客户端上进行的更改以及与服务器的已更新内容进行合并而言,该功能尤其有用。图 2.2 显示了一个从 Web 服务请求更新的智能客户端,新数据作为数据传输对象 (DTO) 返回。DTO 是一种企业模式,它使您可以将所有需要与 Web 服务进行通讯的数据打包到一个对象中。使用 DTO 通常意味着您可以对 Web 服务进行单个调用而不是多个调用。
图 2.2 通过使用DataSets合并客户端上的数据
在该示例中,当 DTO 被返回到客户端时,该 DTO 将被用于在客户端上以本地方式创建一个新的DataSets。
注 在合并操作之后,ADO.NET 不会自动将行状态从 modified 更改为 unchanged。因此,在将新的DataSets与本地客户端DataSets合并之后,您需要调用DataSets上的 AccceptChanges 方法,将 RowState 属性重置为 unchanged。
有关如何使用DataSets的详细信息,请参阅 .NET Framework Developer's Guide 中的“Merging DataSet Contents”,网址为:http://msdn.microsoft.com/library/en-us/cpguide/html/cpconmergingDataSetcontents.asp。
提高DataSets的性能
DataSets通常可以包含大量数据,如果通过网络传递这些数据,则可能导致性能问题。幸而,通过 ADO.NET DataSets,您可以使用DataSets上的 GetChanges 方法来确保只在客户端和服务器之间传送在DataSets中更改过的数据,并且将该数据打包到 DTO 中。该数据随后将被合并到其目的地的DataSets中。
图 2.3 显示了一个智能客户端,它对本地数据进行更改,并且使用DataSets上的 GetChanges 方法仅将已更改的数据提交给服务器。出于性能原因,该数据被传输给 DTO。
图 2.3 使用 DTO 改善性能
可以将 GetChanges 方法用于需要脱机工作的智能客户端应用程序。当应用程序重新联机时,您可以使用 GetChanges 方法确定哪些信息已经更改,并且随后生成一个与 Web 服务通讯的 DTO,以便确保将更改提交给数据库。
Windows 窗体数据绑定
通过 Windows 窗体数据绑定,您可以将应用程序的用户界面连接到该应用程序的基础数据。Windows 窗体数据绑定支持双向绑定,因此您可以将数据结构绑定到用户界面,向用户显示当前数据值,使用户可以编辑数据,然后使用用户输入的值自动更新基础数据。
您可以使用 Windows 窗体数据绑定将几乎任何数据结构或对象绑定到用户界面控件的任何属性。您可以将单个数据项绑定到控件的单个属性,还可以将更为复杂的数据(例如,数据项集合或数据库表)绑定到该控件,以便它可以在数据网格或列表框中显示所有数据。
注 您可以绑定任何支持一个或多个公共属性的对象。您只能绑定到类的公共属性而不是公共成员。
通过 Windows 窗体数据绑定,您可以随您的应用程序一起提供灵活的、数据驱动的用户界面。您可以使用数据绑定提供对用户界面外观的自定义控制(例如,通过绑定到某些控件属性,如背景或前景颜色、大小、图像或图标)。
数据绑定具有许多种用途。例如,可以使用它完成下列任务:
• |
向用户显示只读数据。 |
• |
使用户可以从用户界面更新数据。 |
• |
提供数据上的主从视图。 |
• |
使用户可以浏览复杂的相关数据项。 |
• |
提供查找表功能,使用户界面可以连接用户友好的显示名称。 |
本节分析数据绑定的一些功能,并讨论一些您经常需要在智能客户端应用程序中实现的数据绑定功能。
有关数据绑定的详细信息,请参阅“Windows Forms Data Binding and Objects”,网址为:http://msdn.microsoft.com/library/en-us/dnadvnet/html/vbnet02252003.asp。
Windows 窗体数据绑定体系结构
Windows 窗体数据绑定提供了一种用于将数据双向连接到用户界面的灵活的基础结构。图 2.4 显示了 Windows 窗体数据绑定的总体体系结构的示意图。
图 2.4 Windows 窗体数据绑定的体系结构
Windows 窗体数据绑定使用下列对象:
• |
数据源。数据源是包含要绑定到用户界面的数据的对象。数据提供程序可以是任何具有公共属性的对象,可以是支持 IList 接口的数组或集合,还可以是复杂数据类(例如,DataSet 或 DataTable)的实例。 |
• |
CurrencyManager。CurrencyManager 对象用于跟踪绑定到用户界面的数组、集合或表内的数据的当前位置。通过 CurrencyManager 可以将数据集合绑定到用户界面以及在相应的数据中导航,同时更新用户界面以反映集合内当前选择的项。 |
• |
PropertyManager。PropertyManager 对象负责维护绑定到控件的对象的当前属性。PropertyManager 类和 CurrencyManager 类都从公用基类 BindingManagerBase 中继承。所有绑定到控件的数据提供程序都具有一个关联的 CurrencyManager 或 PropertyManager 对象。 |
• |
BindingContext。每个 Windows 窗体都具有一个默认的 BindingContext 对象,该对象跟踪相应窗体上的所有 CurrencyManager 和 PropertyManager 对象。通过 BindingContext 对象可以容易地检索特定数据源的 CurrencyManager 或 PropertyManager 对象。您可以将特定的 BindingContext 对象分配给包含数据绑定控件的容器控件(如 GroupBox、Panel 或 TabControl)。这样做可以使窗体的每个部分都由它自己的 CurrencyManager 或 PropertyManager 对象管理。 |
• |
Binding。Binding 对象用于在控件的单个属性与另一个对象的属性或某个对象列表中当前对象的属性之间创建和维护简单绑定。 |
将数据绑定到 Windows 窗体控件
有许多可用于绑定到特定 Windows 窗体控件的属性和方法。表 2.2 显示了其中一些比较重要的属性和方法。
表 2.2 用于绑定到 Windows 窗体控件的属性和方法
DataSource 属性 |
ListControls(例如,ListBox 或 Combo Box)、
DataGrid 控件 |
使您可以指定要绑定到用户界面控件的数据提供程序对象。 |
DisplayMember 属性 |
ListControls |
使您可以指定要显示给用户的数据提供程序的成员。 |
ValueMember 属性 |
ListControls |
使您可以指定与显示值相关联的、供您的应用程序内部使用的值。 |
DataMember 属性 |
DataGrid 控件 |
如果数据源包含多个数据源(例如,如果您指定了包含多个表的DataSets),请使用 DataMember 属性来指定要绑定到网格的数据源。(参阅表后面的备注。) |
SetDataBinding 方法 |
DataGrid 控件 |
使您可以在运行时重置 DataSource 方法。 |
注 如果 DataSource 是 DataTable、DataView、集合或数组,则无须设置 DataMember 属性。
您还可以使用所有 Windows 窗体控件对象上提供的 DataBindings 集合属性将 Binding 对象显式添加到任何控件对象。Binding 对象用于将控件上的单个属性绑定到数据提供程序的单个数据成员。下面的代码示例在一个文本框控件的 Text 属性和一个数据集的 customers 表中的客户名称之间添加了绑定。
textBox1.DataBindings.Add(
new Binding( "Text", DataSet, "customers.customerName" ) );
当您用 Binding 构造函数构建 Binding 示例时,您必须指定要绑定到的控件属性的名称、数据源以及可解析为该数据源中的列表或属性的导航路径。该导航路径可以是空字符串、单个属性名或句点分隔的名称层次结构。您可以使用分层的导航路径在 DataSet 对象中的数据表和关系中导航,或者在对象的属性向其他对象返回实例的对象模型中导航。如果您将导航路径设置为空字符串,则会在基础数据源对象上调用 ToString 方法。
注 如果属性是只读的(即,对象不支持对该属性进行的设置操作),则数据绑定默认情况下不会使绑定的 Windows 窗体控件成为只读的。这可能给用户带来混乱,因为用户可以编辑用户界面中的值,但绑定对象中的值将不会得到更新。所以,请确保将所有被绑定到只读属性的 Windows 窗体控件的只读标志设置为 true。
将控件绑定到DataSets
将控件绑定到数据集通常是有用的。这样做使您可以在数据网格中显示数据集数据,并且使用户可以容易地更新数据。您可以使用以下代码将数据网格控件绑定到 DataSet。
DataSet newDataSet = webServiceProxy.GetDataSet();
this.DataGrid.SetDataBinding( newDataSet, "tableName" );
有时,在已经建立与您的控件的所有绑定之后,您需要替换数据集的内容。但是,在用新的集合替换现有集合时,所有绑定仍将指向旧的数据集。
比用新的数据源手动重新创建数据绑定更好的办法是,您可以使用 DataSet 类的 Merge 方法将新数据集中的数据导入现有数据集,如下面的代码示例所示。
DataSet newDataSet = myService.GetDataSet();
this.DataSet1.Clear();
this.DataSet1.Merge( newDataSet );
注 要避免线程化问题,您应该只在 UI 线程上更新绑定的数据对象。有关详细信息,请参阅第 6 章:使用多个线程。
在数据集合中导航
如果您的数据源包含项集合,则可以将该数据集合绑定到 Windows 窗体控件,并且在该数据集合中逐项导航。用户界面将自动更新以反映集合中的当前项。
您可以绑定到任何支持 IList 接口的集合对象。当您绑定到对象集合时,您可以让用户导航该集合中的每个项,并自动更新每个项的用户界面。.NET Framework 提供的许多集合和复杂数据类已经支持 IList 接口,因此您可以容易地绑定到数组或复杂数据,如数据行或数据视图。例如,任何作为 System.Array 类的实例的数组对象默认情况下都实现了 IList 接口,因而可以绑定到用户界面。许多 ADO.NET 对象还支持 IList 接口或它的派生接口,从而使这些对象也可以容易地绑定。例如,DataViewManager、DataSet、DataTable、DataView 和 DataColumn 类都以这种方式支持数据绑定。
实现了 IList 接口的数据源由 CurrencyManager 对象管理。该对象通过它的 Position 属性维护数据集合的索引。该索引用于确保绑定到该数据源的所有控件都读/写数据集合中的相同项。
如果您的窗体包含绑定到多个数据源的控件,则它将具有多个 CurrencyManager 对象,分别对应于各个独立的数据源。BindingContext 对象提供对该窗体上的所有 CurrencyManager 对象的方便访问。下面的代码示例显示了如何在 customers 集合内部递增当前位置。
this.BindingContext[ DataSet, "customers" ].Position += 1;
您应该像以下代码示例中所示的那样,使用 CurrencyManager 对象上的 Count 属性来确保不会设置无效位置。
if ( this.BindingContext[ DataSet, "customer" ].Position <
( this.BindingContext[ DataSet, "customer" ].Count – 1 ) )
{
this.BindingContext[ DataSet, "customers" ].Position += 1;
}
CurrencyManager 对象还支持 PositionChanged 事件。您可以创建该事件的处理程序,以便更新您的用户界面以反映当前绑定位置。下面的代码示例显示了一个标签,以说明当前位置和记录总数。
this.BindingContext[ DataSet, "customers" ].PositionChanged +=
new EventHandler( this.BindingPositionChanged );
方法 BindingPositionChanged 的实现方式如下所示。
private void BindingPositionChanged( object sender, System.EventArgs e )
{
positionLabel.Text = string.Format( "Record {0} of {1}",
this.BindingContext[dsPubs1, "authors"].Position + 1,
this.BindingContext[dsPubs1, "authors"].Count );
}
自定义格式和数据类型转换
您可以使用 Binding 类的Format 和 Parse 事件为绑定到控件的数据提供自定义格式。通过这些事件,您可以控制在用户界面中显示数据的方式以及从用户界面中获取数据和分析数据的方式,以便更新基础数据。还可以使用这些事件来转换数据类型,以便源数据类型和目标数据类型兼容。
注 如果控件上绑定属性的数据类型与数据源中数据的数据类型不匹配,则会引发异常。如果您需要绑定不兼容的类型,则应该使用 Binding 对象上的 Format 和 Parse 事件。
当从数据源中读取数据并将其显示在控件中时,以及当从控件中读取数据并使用它来更新数据源时,将发生 Format 事件。当从数据源中读取数据时,Binding 对象将使用 Format 事件在控件中显示格式化数据。当从控件中读取数据并使用它来更新数据源时,Binding 对象将使用 Parse 事件来分析数据。
Format 和 Parse 事件使您可以创建用于显示数据的自定义格式。例如,如果表中的数据的类型是 Decimal,则您可以通过将 ConvertEventArgs 对象的 Value 属性设置为 Format 事件中的格式化值,以本地货币格式显示数据。因此,您必须在 Parse 事件中格式化显示的值。
下面的代码示例将订单金额绑定到文本框。Format 和 Parse 事件用于在文本框期望的 string 类型和数据源期望的 decimal 类型之间进行转换。
private void BindControl()
{
Binding binding = new Binding( "Text", DataSet,
"customers.custToOrders.OrderAmount" );
// Add the delegates to the event.
binding.Format += new ConvertEventHandler( DecimalToCurrencyString );
binding.Parse += new ConvertEventHandler( CurrencyStringToDecimal );
text1.DataBindings.Add( binding );
}
private void DecimalToCurrencyString( object sender, ConvertEventArgs cevent )
{
// The method converts only to string type. Test this using the
DesiredType.
if( cevent.DesiredType != typeof( string ) ) return;
// Use the ToString method to format the value as currency ("c").
cevent.Value = ((decimal)cevent.Value).ToString( "c" );
}
private void CurrencyStringToDecimal( object sender, ConvertEventArgs cevent )
{
// The method converts back to decimal type only.
if( cevent.DesiredType != typeof( decimal ) ) return;
// Converts the string back to decimal using the static Parse method.
cevent.Value = Decimal.Parse( cevent.Value.ToString(),
NumberStyles.Currency, null );
}
使用模型-视图-控制器模式来实现数据验证
通过将数据结构绑定到用户界面元素,用户可以编辑数据并确保所做更改随后被写回到基础数据结构。通常,您需要检查用户对数据所做的更改,以确保输入的值有效。
上一节中介绍的 Format 和 Parse 事件提供了一种用于截获用户对数据所做更改的方法,以便可以检查数据的有效性。但是,该方法要求与自定义格式代码一起实现数据验证逻辑(通常是在窗体级别)。如果在事件处理程序中同时实现这两种职责,则会使您的代码难以理解和维护。
更为雅致的办法是对代码进行设计,以使其使用模型-视图-控制器 (MVC) 模式。该模式提供了在通过数据绑定编辑和更改数据时涉及到的各种职责的自然分隔。您应该在负责以特定格式呈现数据的窗体内实现自定义格式,然后将验证规则与数据本身相关联,以便在多个窗体中重新使用这些规则。
在 MVC 模式中,数据本身被封装在模型对象中。视图对象是数据所绑定到的 Windows 窗体控件。对该模型所做的所有更改都由一个中间控制器对象处理,该对象负责提供对数据的访问,并且负责控制通过视图对象对数据所做的任何更改。控制器对象提供了一个用于验证对数据所做更改的自然位置,所有用户界面验证逻辑都应该在这里实现。
图 2.5 描绘了 MVC 模式中的三个对象之间的结构关系。
图 2.5 模型-视图-控制器模式中的对象
以这种方式使用控制器对象具有许多优点。您可以配置一个普通的控制器以提供自定义验证规则,这些规则可以在运行时根据某些上下文信息(例如,用户的角色)进行配置。或者,您还可以提供许多个控制器对象,每个控制器对象都实现特定的验证规则,然后在运行时选择适当的对象。无论采用哪种方法,因为所有验证逻辑都被封装在控制器对象中,所以视图和模型对象都不需要更改。
除了分隔数据、验证逻辑和用户界面控件以外,MVC 模型还为您提供了一种在基础数据更改时自动更新用户界面的简单方法。控制器对象负责在发生通过其他某些编程手段对数据进行更改时通知用户界面。Windows 窗体数据绑定侦听由绑定到控件的对象生成的事件,以便用户界面可以自动响应对基础数据所做的更改。
要实现用户界面的自动更新,您应该确保控制器为每个可能更改的属性实现一个更改通知事件。事件应该遵循命名约定<property>Changed,其中 <property> 是属性的名称。例如,如果控制器支持 Name 属性,则它还应该支持 NameChanged 事件。如果名称属性的值更改,则应该激发该事件,以便 Windows 窗体数据绑定可以处理它并更新用户界面。
下面的代码示例定义了一个 Customer 对象,该对象实现了 Name 属性。CustomerController 对象处理 Customer 对象的验证逻辑并支持 Name 属性,而该属性又表示基础 Customer 对象上的 Name 属性。每当该名称更改时,此控制器都将激发一个事件。
public class Customer
{
private string _name;
public Customer( string name ) { _name = name; }
public string Name
{
get { return _name; }
set { _name = value; }
}
}
public class CustomerController
{
private Customer _customer = null;
public event EventHandler NameChanged;
public Customer( Customer customer )
{
this._customer = customer;
}
public string Name
{
get { return _customer.Name; }
set
{
// TODO: Validate new name to make sure it is valid.
_customer.Name = value;
// Notify bound control of change.
if ( NameChanged != null )
NameChanged( this, EventArgs.Empty );
}
}
}
注 Customer 数据源成员在声明时需要进行初始化。在前面的示例中,需要将 customer.Name 成员初始化为空字符串。这是因为在数据绑定发生之前,.NET 框架没有机会与该对象进行交互并设置默认的空字符串设置。如果未初始化 customer 数据源成员,则在尝试从未初始化的变量中检索值时,将导致运行时异常。
在下面的代码示例中,窗体具有一个 TextBox 对象 textbox1,它需要绑定到客户的名称。代码将 TextBox 对象的 Text 属性绑定到控制器的 Name 属性。
_customer = new Customer( "Kelly Blue" );
_controller = new CustomerController( _customer );
Binding binding = new Binding( "Text", _controller, "Name" );
textBox1.DataBindings.Add( binding );
如果更改了客户的名称(使用控制器上的 Name 属性),则会激发 NameChanged 事件,并且自动更新文本框以反映新的名称值。
在基础数据更改时更新用户界面
您可以使用 Windows 窗体数据绑定在相应的基础数据更改时自动更新用户界面。通过在绑定的对象上实现一个更改通知事件,可以完成该任务。更改通知事件按照以下约定命名。
public event EventHandler Changed;
因此,假设您将某个对象的 Name 属性绑定到用户界面,然后该对象的名称由于其他某种处理而更改,则您可以通过实现绑定对象上的 NameChanged 事件来自动更新用户界面,以反映新的 Name 值。
小结
在确定如何在智能客户端处理数据时,涉及到许多不同的注意事项。您需要确定是否缓存以及如何缓存您的数据,并且确定如何处理数据并发问题。您将经常决定使用 ADO.NET 数据集来处理您的数据,并且您还可能将决定利用 Windows 窗体数据绑定功能。
在许多情况下,只读引用数据和瞬态数据需要进行不同的处理。因为智能客户通常使用这两种类型的数据,所以您需要确定在应用程序中处理各个类别数据的最佳方式。
转到原英文页面