面向对象程序员的 ADO.NET – 第三部分






4.71/5 (23投票s)
在本文中,我们将了解如何使用 ADO.NET 来序列化对象模型。完成序列化后,我们将研究如何将对象模型绑定到用户控件。
引言
在本系列的前两篇文章中,我们探讨了面向对象程序员用来将数据从数据库加载到对象模型的 ADO.NET 操作,以及用于将数据保存回数据库的操作。我们还探讨了如何使用 .NET 2.0 的数据绑定功能将对象模型绑定到表示层中的 UI 控件。
在本系列第三篇也是最后一篇文章中,我们将继续讨论这两个主题。首先,我们将了解如何使用 ADO.NET 来序列化对象模型。事实证明,在我们的模型无法很好地拟合到单个对象图中时,ADO.NET 可以非常有用。完成序列化后,我们将研究如何将对象模型绑定到用户控件。这并不难,但也不是特别有据可查。掌握了这两种技术,面向对象程序员就应该能够应对开发 .NET 应用程序时遇到的大部分日常挑战。
演示应用程序
本文的演示应用程序说明了这两种技术。它基于来自第二部分的作者/书籍数据库。
但这次,我们不打算从数据库加载对象模型——我们将从 XML 文件加载它。并且,与在第二部分中在网格中编辑不同,我们提供了一个单独的“编辑作者信息”面板来更新作者信息。
序列化和反序列化对象模型
您可能知道,只需将 .NET 类标记为 [Serializable]
即可对其进行序列化和反序列化。可序列化的类可以使用适当的 Formatter 对象(我们将称之为“直接序列化”)流式传输到二进制或 XML 文件中。该技术在.NET Framework 开发人员指南的基本序列化中进行了介绍。
该技术对于适合单个对象图的简单对象模型效果很好。对于更复杂的对象模型,还有更高级的序列化技术,即使是具有多个对象图的模型,也可以通过将各种根对象包含在主根对象中来使用基本序列化进行处理。
但是,正如序列化用户都深知的那样,序列化可能不稳定,并且并不总是能正确工作。ADO.NET 提供了一种不同的策略——将对象模型数据传输到 ADO.NET 数据集,然后序列化数据集。这种方法比直接操作直接序列化要容易得多,并且可以处理非常复杂的对象模型而不会出问题。
ADO.NET 数据集具有内置的序列化器。我们不必实例化格式化程序或文件流——我们可以使用简单的命令将数据集保存到 XML 文件中。
// Save the data set to an XML file
dataSet.WriteXml(filePath);
我们可以同样轻松地将 XML 文件加载到数据集中。
// Load the data file into a new dataset
DataSet dataSet = new DataSet();
dataSet.ReadXml(filePath);
这种方法特别有吸引力的是,ADO.NET 数据集可以拥有捕获复杂对象模型所需的任意数量的表。每个根类都可以拥有自己的表,并且其中包含的每个类也可以拥有自己的表。根类表使用 DataRelation
对象与对应于其包含类的表相关联,正如我们在第二部分中所做的那样。
我们将在演示应用程序的演练中更全面地讨论序列化和反序列化。现在,让我们将注意力转向将数据绑定到用户控件的主题。
将数据绑定到用户控件
当您查看演示应用程序的表示层时,您可能会注意到“编辑作者信息”组包含在一个单独的用户控件中。难道不更容易省略用户控件,直接将组放在窗体上吗?
在演示应用程序中,这肯定会更容易,而且您可能永远不会在如此简单的应用程序中使用用户控件。但是,请考虑以下几点:我目前正在处理一个使用六种不同类型事务的应用程序。每种事务类型都派生自 TransactionBase
类,并且每种类型都有大约十几个用户需要填充和编辑的属性。
为了增加挑战,属性并非都是简单的字符串或布尔值。每种事务类型都有相当多的表示逻辑,并且每种类型的逻辑都有些不同。这种情况非常需要一个用户控件。
在实际应用程序中,我的数据录入表单的布局与演示应用程序中的布局非常相似。网格是“客户-事务”对中的详细视图。它显示事务日期和描述;编辑表单显示事务的完整详细信息。
当用户在网格中选择一个项目时,应用程序会捕获 BindingSource
的 CurrentChanged
事件,以确定所选事务的类型。然后,应用程序通过使其可见并将它绑定到 BindingSource
控件(使用控件的 DataBindings
集合)来“激活”所选类型的用户控件。之前的表单通过隐藏它并清除其绑定来“停用”。结果是一个非常简单的数据录入表单,用于一个非常复杂的对象模型。
演示应用程序更简单——它只有一个用户控件,其数据绑定是在设计器而不是代码中设置的。但是,实际应用程序使用与演示应用程序相同的技术。唯一的区别是多个用户控件的运行时激活和停用。
将数据绑定到用户控件:当我们能够绑定到 .NET 控件时,数据绑定就很简单。正如我们在第二部分中看到的,我们只需创建并配置一个 BindingSource
组件,然后将控件的 DataSource
和 DataMember
属性设置为控件。 .NET 会处理其余的事情。
但是,当我们将数据绑定到用户控件时,事情会变得更有趣。我们需要在用户控件中添加一些基础结构,以便它们能够支持数据绑定。要添加的第一部分基础结构是将 [Bindable]
属性添加到每个属性,如下所示:
[Bindable(true)]
[Browsable(true)]
public string FirstName
{
get { return textBoxFirstName.Text; }
set { textBoxFirstName.Text = value; }
}
[Bindable]
属性与 [Browsable]
属性结合使用,将使属性出现在用户控件的可绑定属性列表中。
该属性支持设计时的数据绑定指定,但如果没有更多内容,绑定将不起作用。为了使其功能化,我们需要一种方法来通知 BindingSource
组件属性何时发生更改。
数据绑定会监听一个名为 PropertyNameChanged
的事件,其中 PropertyName
是数据绑定到特定数据的属性的名称(我们将该事件称为 PropertyChanged
事件)。例如,绑定到 FirstName
属性的数据将监听名为 FirstNameChanged
的事件。因此,我们要添加到用户控件的第二部分基础结构是一组 PropertyChanged
事件。
#region Declarations
// Property change events
public event System.EventHandler FirstNameChanged;
public event System.EventHandler LastNameChanged;
public event System.EventHandler SocSecNoChanged;
#endregion
当属性更改时,我们需要触发相应的 PropertyChanged
事件。在演示应用程序中,用户控件的每个属性都连接到控件中的一个文本框,因此我们可以在其中一个文本框中的文本发生更改时触发事件,如下所示:
private void textBoxFirstName_TextChanged(object sender, EventArgs e)
{
if (this.FirstNameChanged != null)
{
this.FirstNameChanged(this, new System.EventArgs());
}
}
因此,有了这两部分基础结构,我们的对象模型就可以绑定到我们的用户控件了。
PropertyChanged
事件的时机:默认情况下,PropertyChanged
事件的触发不会立即导致数据绑定读取已更改的属性。相反,数据绑定会等到控件的 Validated
事件触发。换句话说,无论 PropertyChanged
事件何时触发,数据绑定都要等到控件失去焦点后才会读取更改后的值。
这种方法会导致各种问题。最明显的问题是 Validated
事件因某种原因未触发。用户会说“我输入了值,但程序丢失了它。”那是因为 Validate
事件没有触发。
我更喜欢强制数据绑定在 PropertyChanged
事件触发后立即读取更改后的值。可以通过将每个绑定的 DataSourceUpdateMode
设置为 OnPropertyChanged
来强制执行此行为。
void DataBindings_CollectionChanged(object sender,
CollectionChangeEventArgs e)
{
// Set data bindings to update on property change
foreach (Binding binding in this.DataBindings)
{
binding.DataSourceUpdateMode =
DataSourceUpdateMode.OnPropertyChanged;
}
}
这种方法避免了“数据丢失”问题,并为最终用户创造了更好的体验。请注意,在演示应用程序中,当在用户控件的文本框中键入每个字符时,信息会出现在网格中。此功能为用户提供了即时确认,即他们的输入已被应用程序记录。
将更改通知对象模型
在 .NET 2.0 中,Microsoft 引入了一个新接口来处理从对象模型到数据绑定的通信。INotifyPropertyChanged
接口包含一个名为 PropertyChanged
的单个事件,用于通知绑定对象模型属性已更改。通过用户控件中的 PropertyChanged
事件和对象模型中的 INotifyPropertyChanged.PropertyChanged
事件,开发人员可以实现与数据绑定的完整双向通信。
请注意,演示应用程序不使用 INotifyPropertyChanged
接口。相反,它使用 BindingSource
组件来向作者列表添加和删除项目。这是个好主意吗?纯粹主义者会说不是,因为我在表示层中混合了业务逻辑。相反,我应该为每个操作创建 Command 对象,这些对象会在 AuthorList
对象中调用 Add 和 Delete 操作。
这一点可能是正确的。另一方面,我认为 BindingSource
组件正在对 AuthorList
对象调用相同的操作。(实际上,它在集合的 BindingList<T>
基类中调用 AddNew()
和 Remove()
操作。)所以,唯一的区别是我使用 Command 对象还是 BindingSource
组件来访问我的业务层。
尽管如此,如果您在这些问题上的看法非常严格,您可能更喜欢采用 Command 对象路由到对象模型。在这种情况下,您的集合及其成员应该实现 INotifyPropertyChanged
接口。
演示应用程序演练
这就回到了演示应用程序。它的组织方式与本系列第二部分中的演示应用程序基本相同。两者之间的主要区别是:
- 本文中的应用程序从 XML 文件读取并写入 XML 文件,而不是 SQL Server 数据库。
- 主窗口呈现
AuthorList
集合的摘要-详细视图,而不是作者和书籍的主-详细视图。
应用程序故意将事物简化到过度简化的程度。例如,编辑面板显示的信息与网格相同。在实际应用程序中,编辑面板将显示比网格更多的详细信息。
用户界面:UI 包括一个用于网格的 DataGridView
控件,一个用于编辑面板的 UserControl
,一个 BindingSource
组件,以及四个按钮。代码很简单,无需进一步解释。有关 UI 中实例化的 Command 对象的讨论,请参阅本系列第二部分。
主窗体有四个按钮;打开、保存、添加和删除。打开按钮调用标准的“文件打开”对话框以打开 XML 数据文件。文件位于解决方案文件夹的顶层(与 .sln 文件相同的文件夹)。保存按钮将文件保存到打开它的位置——不提供“另存为”功能。
“添加”和“删除”按钮调用 bindingSourceAuthorList
组件上的添加和删除操作,以将新作者添加到列表中或删除作者。
架构:应用程序的架构与本系列第二部分中的演示应用程序基本相同。应用程序按照模型-视图-控制器 (MVC) 模式组织,Command 对象用于封装 Controller 的功能。使用 XML 文件而不是数据库可以简化应用程序的控制方面,从而比第二部分中的 Command 对象少。
数据访问:此应用程序中的 DAO 对象与第二部分中的不同之处在于,我们不需要低级别的 DataProvider
组件。相反,DAO 类包含加载和保存 XML 文件的内部方法,以及用于初始化这些操作中使用的数据集的私有方法。
DAO 将作者和书籍列表都加载到对象模型中,即使表示层只使用 AuthorList
集合。它这样做是为了提供加载和保存使用 XML 文件存储的包含层次结构的完整代码。DAO 代码已注释,无需进一步解释。
对象模型:对象模型与第二部分相比几乎没有变化。所做的唯一更改是删除了将业务对象链接到其相应 DAO 对象的那些方法。这些方法被 AuthorList
对象中的两个方法替换,用于加载和保存对象图。
用户控件:UserControlEditAuthor
包含三个文本属性,这些属性直接绑定到控件中包含的文本框。在实际应用程序中,这些属性可能不会以这种方式直接绑定。相反,控件将包含用于验证和执行用户输入 UI 处理的方法。用户控件的属性将是这些方法的返回值。
例如,用户控件可能包含一个表示业务规则的属性,以及三个表示该规则选项的单选按钮。当用户选择一个单选按钮时,用户控件代码会将属性设置为适当的选项。因此,包含的控件将仅通过代码间接连接到用户控件属性。
UserControlEditAuthor
订阅了其 DataBindings
集合的 CollectionChanged
事件。该事件的句柄 DataBindings_CollectionChanged()
将集合中所有绑定的 DataSourceUpdateMode
重置为 OnPropertyChanged
。正如我们上面讨论的,当在用户控件的一个文本框中键入文本时,此模式会立即更新对象模型和其他任何已绑定控件(在本例中是网格)。
正如我们上面讨论的,请注意用户控件类顶部的 PropertyChanged
事件声明。这些事件接受不带参数的 System.EventHandler
委托。另请注意,每个属性都标有 [Bindable(true)]
和 [Browsable(true)]
属性。这些属性支持演示应用程序中使用的主题数据绑定。
结论
好了,希望这样就可以了。正如我在本系列开头提到的,我开始它是作为一次学习练习,并在开发实际应用程序时遇到问题时继续进行。那么我们现在在哪里?
此时,您应该能够将数据从数据库或 XML 文件移动到对象模型并从中移动。您应该能够使用 MVC 架构处理该模型,并且您应该能够使用 .NET 2.0 数据绑定在对象模型和应用程序的 UI 之间移动信息。此外,您应该能够创建从简单的“主-详细”模式到涉及多个、交替的数据录入表单的非常复杂的“摘要-详细”模式的 UI。
我实在想不出还有什么需要涵盖的了。当然,这是我在第二部分之后说过的话。如果您阅读了第一部分,您可能还记得我说过 ADO.NET 对对象程序员的意义在于能够花更多时间与配偶和孩子在一起。我是在圣诞节后不久开始这个系列的——当我完成它时,已经是情人节快到了。如果我现在停下来冲去商场,我应该有足够的时间为我的妻子买一份情人节礼物和卡片。再见!