数据绑定入门






4.75/5 (23投票s)
包括作为呈现数据的通用原则的“四视图”概念。
目录
- 引言
- 数据建模练习
- 开发模型
- 模型指南
- 遍历所有属性
- 数据模型总结 - 以及关于 AllowDbNull
- 布局准备
- 调出数据源窗口
- 构建“原始视图”
- BindingSources(请移除 BindingNavigator)
- BindingSource 命名
- 四视图
- 1 - 父子视图
- 2 - 连接视图
- 3 - m:n 视图
- 4 - 详细视图
- 自定义详细信息控件
- 自定义列表控件作为“选择器”
- 自定义列表控件作为“连接器”
- DataExpression - 计算数据列
- 示例应用程序
- 结论
- 仍有缺失的主题
引言
本文是我上一篇文章 关系型数据模型入门 的续篇,在那篇文章中,我介绍了关系型数据建模的基本思想,以及如何使用 Visual Studio 的类型化数据集设计器实现一个模型。
那篇文章以一个小示例应用程序结尾,该程序演示了强类型关系数据模型(即类型化数据集)的两个主要优点。
- 你可以用面向对象的、类型化的方式对其进行编码,通过循环遍历数据,或通过 Linq 表达式等方式收集数据。不需要任何类型转换,也不存在任何通过“属性名称字符串”进行非类型化数据请求的代码坏味。在后续文章中,我将对此进行更深入的探讨。
- 你可以通过在窗体设计器中配置数据绑定来呈现数据。“呈现”意味着:不仅是查看数据,还支持完整的 CRUD(增删改,只要你不限制)。
这种呈现方式工作可靠、一致,并且代码量少得惊人——如果你还没见过类似的东西,你真的会感到惊讶。
尽管那篇文章的示例应用程序演示了数据绑定,但文章并未解释它是如何配置来实现其功能的。
而这正是本文的主题:解释这一点,并在一个更通用的背景下进行解释——我称之为“四视图”概念。
但在开始设计一个数据绑定驱动的 GUI 之前,有几项准备工作需要完成。
如果你对一些技术术语(实体、模型、关系……)不是非常熟悉,我建议你再次参考上一篇文章,尤其是我对术语的定义尝试。
你也可以去那里学习数据集设计器的基本用法——这里我不再重复。
与此同时,我发表了第三篇文章,以涵盖这里仍然缺失的一些主题:面向类型化数据集的编程
数据建模练习
每个数据应用程序的开发都应该从设计数据模型开始。这对于习惯于将程序看作是文本框、数据网格视图、组合框等的程序员来说,可能是一个思维上的巨大转变。——所有这些东西都无关紧要——你必须考虑数据,而且更要考虑现实世界。
数据建模确实如其名:你创建一个现实世界的模型。当我在上一篇文章中试图解释“实体”这个术语时,我遇到了一些麻烦,因为我无法准确地区分“实体”是指模型的一部分还是现实世界的一部分。
但是,在这种情况下,我破例地*喜欢*这种双重含义,因为——在设计模型时——当我做出关于现实的陈述时,同时这也是关于模型的陈述,反之亦然。
开发模型
假设有一家邮购公司。它提供商品,客户订购这些商品——会出现哪些实体?事实上,其中三个已经由这个简单的陈述定义了:“客户订购商品”。
1) `Customer` (客户) 2) `Order` (订单) 3) `Article` (商品)
为了让它更复杂*一点*,让我们将商品按类别进行结构化:4) `Category` (类别)
你现在就可以打开数据集设计器,并将这些实体放上去。
一个好习惯是同时配置主键——因为这是第一条建模指南:“没有实体可以没有主键!”
现在来考虑关系:一个类别包含许多商品——对吗?
一个客户可能会多次下单——意味着:一个客户有多个订单。
但订单*是*什么,它包含什么?简单地想,从具体的现实出发——一个客户可能会下单说:*“我想要5瓶啤酒和16根香肠!”*
你看,他的订单提到了2种不同的商品(也可能更多)。这意味着:一个订单包含多个条目。每个条目都指向一个商品——并与之关联一个数量。
所以这里我们有了第五个实体,名为:5) `OrderEntry` (订单条目)。
正如上一篇文章所解释的,当一个实体可以包含多个其他实体时,就存在一对多关系,即父子关系。我们已经找到了其中的三个:
1) `Category=>Article` (类别=>商品)
2) `Customer=>Order` (客户=>订单)
3) `Order=>OrderEntry` (订单=>订单条目)
还有第四个关系,我们已经陈述过:“*一个订单条目指向一个商品*”——这同时也是我们模型中的一个定义:4) `Article=>OrderEntry` (商品=>订单条目)
事实上,“**A** 被许多 **B** 引用”这句话是“关系验证声明”。如果它说得通,那么就存在一对多关系,否则你就得三思。
为了练习,让我们来验证一下我们的关系概念是否合理:
- 一个类别被多个商品引用 - 没问题
- 一个客户被多个订单引用 - 没问题
- 一个订单被多个订单条目引用 - 没问题
- 一个商品被多个订单条目引用(也是) - 没问题
所以我们的数据模型最终是这样的
你看,我已经添加了最简单的属性:类别和商品有名称,商品有价格,客户被视为公司(在这个模型中),订单有日期,以及——从我们客户的具体电话中看到的:一个订单条目有一个数量。
当然,我也为子表添加了必要的外键——原因在我上一篇文章中有详细解释。
作为重复,我展示一下一个普通一对多关系的推荐配置
用文字描述:启用外键约束,更新规则.级联,删除规则.级联,接受/拒绝规则.无
完整的带所有实体属性的数据模型
我建议你自己动手构建这个模型,如果你不确定怎么做,可以参考上一篇文章。
我特意一步一步地展示了这个模型,因为数据集设计器可以*在你思考时*陪伴你,并*在*设计过程展开时提供帮助。
模型指南
- 没有实体可以没有主键
- 简短、有意义的命名
设计器会生成大量的类、属性、事件、方法——它们所有的名称都源于你在数据模型中设置的名称基础。所以一个微小的不理想之处会放大一百倍,而且只要程序存在,你就得一直处理它。 - 主键简单命名为 `ID` - *总是*如此 - (更短更有意义是不可能的了 ;) )
- 外键命名为 `ParentTableName + "ID"`
例如 `Order.CustomerID` 或 `Article.CategoryID`。如果这样做,任何人都能立即识别出主键、外键,以及后者指向哪个父表。 - 不要害怕命名冲突
在类型化模型中,在不同表中使用相同的名称不会导致命名冲突,甚至不会间接导致。
不可能把 Category.Name 和 Article.Name 混淆,因为 Category 和 Article 是完全不同且独立的类。 - 在某些情况下,可以随意使用你自己的语言。
对*你*和你的同事来说,简短和有意义可能比满足更抽象的国际标准要求更重要。一个模型可能包含数百个术语,其中一些很难解释——总是找到完全匹配的英文表达是非常困难的。 - 让模型更严格 - 避免使用 AllowNull
默认情况下,设计器配置的属性是允许未设置的。不要这样做!总是尽量强制属性必须被设置,只有在你确实有充分理由时才允许 Null。
代替 AllowNull,通常可以配置一个 `DefaultValue`,例如 `String=""`、`Bool=False`、`Int=0` 等等。这同样可以防止 NotNullAllowed 异常,因为 Null 值不会出现——在不确定的情况下,总会有一个默认值存在。
相信我:这个技巧会显著简化后续针对数据模型的编码工作。 - 让模型更严格 - 尽量使用外键约束和级联删除
通常当父行被删除时,子行会变得无效。为此,只需配置级联删除,你就不需要再考虑这个问题了。 - UpdateRule.Cascade
当访问真实数据库时,这一点变得很重要。但最好从一开始就习惯这样做——它不会有什么坏处。 - 尊重数据类型
我很难过必须包含这一点,但我见过太多次数字存为字符串,日期存为字符串,布尔值存为字符串,*这样*很伤人 :-( - 不要使用前缀
前缀,如果它们有意义的话,其目的是为了区分几种类型。在关系数据模型中,只有表。那么,给每个表加上 "tbl" 前缀有什么用呢?每个人自己都知道 `OrderDataset.Article` 是一个表——它还能是什么呢? - 使用单数命名
如前所述,“实体”既指记录的概念,也指具体的单个记录。是的,一个表也定义了其所含记录的概念,一个表必须被看作是复数。
但在代码中,你将主要处理单个的 DataRow。而如果表名是复数,那么数据集设计器生成的单个 DataRow 也会是复数——这绝对是错误的。
例如,方法 `OrderDts.Articles.AddArticlesRow(...)` 从其命名上看,表示它添加了多个 Articles —— 这并不正确 —— 该方法只添加了*一个* Article。
为此,将 Article 命名为单数,数据集设计器将生成:`OrderDts.Article.AddArticleRow(...)`,这样就名副其实了。
你可能会在我的一些“命名约定”中认出一些东西,当你参考其他命名指南时,例如可以比较一下伊利诺伊州纵向数据系统项目发布的命名指南。
遍历所有示例模型属性
很抱歉,在我们开始拖放操作之前花了这么长时间,但正如所说:模型是应用程序的基础,尽管每个属性的设置都可以在几秒钟内完成,但每个属性的每个细节都至关重要。
所以这里列出了一些值得注意的配置细节
- 所有表的 `ID` 属性配置如下
主键,整数,自动递增,AutoIncrementSeed=-1,AutoIncrementStep=-1
有一个例外:微软,Northwind 数据库的作者,我从中派生了我的数据模型,出于某些原因,他们更喜欢将客户主键设计为字符串 :wtf
结果是,当你想创建一个新客户时,会出现问题,因为数据集无法自动生成一个有效的、唯一的字符串类型主键 :-( - 默认情况下所有表的所有属性
`AllowDBNull=False` 如果相反,一个属性是允许为 Null 的,那是有原因的,我稍后会解释。 - 属性 `Customer.ContactPerson`, `.Position` 和 ` .Telefax:`
术语 `""` 被设置为默认值。这可以防止在未设置值时出现 NotNullAllowed 异常,因为*根本没有*未设置的值,因为默认情况下 `" "` 就是这个值,它非常清楚地告知这是一个未设置的值——没有例外——明白我的意思吗? ;)
所有其他的 `Customer-` 属性都是必需的——一个没有完整联系信息的客户是不可接受的。 - 属性 `Order.DeliveryDate`, `ShipDate`, `ShipCosts`
这些是允许为 Null 的,因为它有特定的含义,例如当发货日期未设置时(一个未处理的订单)。 Category.Image
允许为 Null,因为这样的图片是一个可选的装饰品。Article.Level, .OrderedUnits, .MinimumLevel
这些数量的 DefaultValue=0 是为了防止 NotNullAllowed 异常。对于这些属性来说,0 是一个合理、有效的值——尤其是在新建商品时。
数据模型总结,以及关于 AllowDbNull
你看:每个表的每个属性都经过了深思熟虑的处理,特别是关于是否允许 Null 的问题。可以做出三种不同的决定:
- AllowDbNull=False
标准做法——通过约束异常来防止输入带有不规则 Null 值的记录。 - AllowDbNull=False + DefaultValue
这不会因为缺少值而导致异常,因为 Null 值根本不可能出现——在不确定的情况下,总会有默认值存在。 - AllowDbNull=True
听起来很宽容,但相反,这种配置导致的问题最多。
每当你想通过代码检索这类值时,在此之前你必须使用另一个方法来检查该值是否已设置。
否则,当值未设置时,你会得到一个 `StrongTypingException`。
布局准备
首先我们需要一个窗体,我建议在上面放一个 Menustrip 和一个 TabControl。
然后回顾一下数据模型,我们有 5 个实体/表,关系如下:
Customer => Order => OrderEntry <= Article <= Category
因此,对于第一个视图,称为“原始视图”,准备一个布局,让每个表都能找到自己的位置,并且每个表都有清晰的标签以避免混淆。
你能这样做吗?
TabControl 使用 `Dock.Fill` 停靠,实际上所有(容器)控件都是这样停靠的:在 "RawView" 选项卡上,基本上有一个 SplitContainer。在其左侧的 SplitterPanel 中是 "Customer+Orders" Groupbox。在该 Groupbox 内部,有另一个 `Orientation.Vertical` 的 SplitContainer。其顶部的 SplitterPanel 包含 "Customer" Groupbox,其底部的 SplitterPanel 包含又一个 SplitContainer,这次是水平的。它的 SplitterPanel 包含 "Orders" 和 "OrderEntries" Groupbox。
所以总的来说,`Customer => Order => OrderEntry` 的关系准备在选项卡的左侧,而 `Category => Article` 在右侧。
所有东西都放在 SplitContainer 上,所以当你稍后使用它时,你总能将当前工作区拖动到最佳大小。
我*总是*从一个“原始视图”开始,以便将我的数据模型呈现出来,即使是快速而粗糙的,但要完整。这样我就可以输入测试数据并测试基本行为,并且我可以保存测试数据,以便在后续的测试运行中重新访问它。
调出数据源窗口
打开数据源窗口:“菜单 - 视图 - 其他窗口 - 数据源”
数据源窗口看起来像这样:
这是我们的数据模型——"NorthwindDts",以及它的所有 5 个表——以树形视图呈现。
要开始绑定 `Customer->Order->OrderEntry` 表,只需展开 `Customer`,然后在 Customer 中展开 `Order`——你会在其中找到 `OrderEntry`。
这就是数据源窗口如何表示 `Customer->Order->OrderEntry`(关系)链的方式。
构建“原始视图”
现在将 Customer 从数据源拖到窗体上,拖到 "Customer" Groupbox 中。
这将生成一个配置完整的 DataGridView,包含所有列。首先将其停靠方式设为 Dock.Fill——最简单的方法是使用 DataGridView 智能标签上的“在父容器中停靠”链接。
对 `Order` 和 `OrderEntry` 做同样的操作,然后你可能自己就知道该如何处理 `Category` 和 `Article` 了。 ;)
再次注意:如果要创建一个根据数据关系显示子数据的配置,请从数据源窗口树形图中选择*嵌套*的表。
你的拖放和停靠结果应该看起来像这样:
现在对所有 DataGridView 做一点改进,将它们的 `AutoSizeColumnMode` 设置为 `AllCells`。为此,你可以(按住 Ctrl 并左键单击)同时选中所有的 DGV,然后在属性网格中设置 AutoSizeColumnMode。
信不信由你——我们还需要一点代码才能让它工作起来。
Imports System.IO
Public Class Form1
Private _DataFile As String = Path.GetFullPath("..\..\NorthWindDts.xml")
Private Sub MenuItem_Click(ByVal sender As Object, ByVal e As EventArgs) _
Handles btReload.Click, btSave.Click
Select Case True
Case sender Is btReload
NorthWindDts.Clear()
NorthWindDts.ReadXml(_DataFile)
Case sender Is btSave
NorthWindDts.WriteXml(_DataFile)
End Select
End Sub
End Class
然后你就完成了——按 F5 看看结果。
当然,在图片中它看起来不是很美观。在更宽的屏幕上会好一些。
请注意:该应用程序现在已经是一个可运行的原型,并为每个实体以及每条记录提供了完整的 CRUD 支持。
此外,你已经构建了你的第一个“父子视图”,它实现了一些数据绑定的魔力:当你选择一个客户时,订单网格只显示该客户的订单;当你选择一个订单时,订单条目网格只显示该订单的订单条目 (`Customer => Order => OrderEntry`)。
右边也是一样:选择一个类别,你将只得到该类别的商品。这个机制可以很清楚地看出来——注意:每个显示的商品的 `CategoryID` 都与所选类别的 `ID` 相匹配。
(同样,OrderEntry 的 OrderID 与所选的 Order-ID 匹配)
BindingSources
正如所见,当你将实体拖到窗体上时,设计器会生成配置好的 DataGridView,包括所有列。该配置的另一部分是 BindingSource,它们出现在窗体设计器的组件栏中。
旁注 - 请移除 BindingNavigator
此外,还会生成一个 `NorthwindDts` 和一个 `CategoryBindingNavigator`。后一个组件我建议移除。我总是抱怨它那愚蠢的“点击下一个/上一个”按钮——你能想象一个比这更没用的控件吗?
当你想呈现数据时,就用 DataGridView、ListBox、ComboBox 等你喜欢的控件来呈现它们。但不要提供点击“下一个/上一个”的按钮——当用户可以直接滚动到他想选择的数据记录时,没有人会用这种垃圾。而且,关于一条记录,还有什么信息比它在表中的索引更无关紧要呢?
所以把它删了吧。
回到 BindingSources:一个 BindingSource 是一种来自数据集的“数据查询”,可以作为数据绑定控件的数据源,也可以作为其他 BindingSources 的数据源。
它有两个关键属性:`DataSource` 和 `DataMember`。你可以在窗体设计器的属性网格中设置它们——例如,我们的 `CategoryBindingSource` 配置如下:
它的 DataSource 是数据集,DataMember 是 Category 表。这就是为什么我们数据绑定的 CategoryDataGridView 显示类别的原因——因为它的 DataSource 是 `CategoryBindingSource`。你可以将 `CategoryBindingSource` 看作是从数据集中“查询”类别记录。
现在看看 `ArticleBindingSource` 的配置:
它的数据源*不是*数据集,而是 `CategoryBindingSource`!它的 DataMember 也*不是*一个表,而是数据关系 `Category=>Article`。这就是为什么我们绑定的 ArticleDatagridView 显示商品,而且它不是显示所有商品,*而是只显示*当前在 CategoryGrid 中选中的 CategoryRow 的*子行*。你可以将 `ArticleBindingSource` 视为从数据集中“查询”商品记录,伪代码查询如下:
"Select * From Article Where Article.CategoryID =
`BindingSource.Current` - 这是 BindingSource 的第二个主要关注点:它是一个 CurrencyManager(货币管理器),与绑定的控件进行通信,BindingSource 总是“知道”控件当前的选择索引——无论它是 DataGridView、ComboBox 还是 ListBox。
反之亦然:当一个 BindingSource 改变其 `.Position` 属性时,控件也会改变其选择——它们是*同步*的——这就是“绑定”一词的含义。
BindingSource 命名
随着我们继续,我们将得到更多的 BindingSource,它们将在不同的视图中提供数据。为此,我推荐一个一致的命名约定——否则你会把自己搞糊涂。
- 我建议所有 BindingSource 都使用前缀 `bs`——这通常适用。
- BindingSource 的名称应该说明它提供的是哪个表或关系的数据——在我们的示例中,(到目前为止)它们是:
`"bsCategory"`, `"bsCategoryArticle"`, `"bsCustomer"`, `"bsCustomerOrder"`, `"bsCustomerOrderEntry"` - 稍后一些特定的 BindingSource 将为 ComboBox 和 ComboBox 列提供数据。
为此,使用第二个前缀 `Cmb`,例如 `"bsCmbCategory"` 可以是一个 BS 的名称,它为 DGV-ComboColumn 提供类别数据——我稍后会讲到这一点。
四视图
总的来说,我认为有四种基本的数据呈现方式。所有呈现的数据都可以看作是其中一种或一种特定的组合——我称之为基本视图:
- 父子视图
- 连接视图
- m:n 视图
- 详细视图
这些视图并非数据集所特有——甚至不依赖于*关系*模型。
几乎所有复杂的数据模型都可以通过这四种基本呈现类型的变体和组合来呈现(而且几乎所有实际情况都是如此)。
1 - 父子视图
你已经知道了——上面的“原始视图”以父子方式呈现了我们所有的数据关系:一个选择器控件选择一个父行,另一个多项控件(ListBox、ComboBox 或 DataGridView)呈现所选父行的子行。
这里是一个更简单的例子,展示 `Category => Article`
2 - 连接视图
当一个 DataGridView 包含 ComboBox 列,用于选择引用上级父行的外键时,就存在一个“连接视图”。
看看这个 Article-DataGridView
在 Sql 中,人们会这样查询这个视图:
SELECT Category.Name, Category.Description, Article.Name, Article.DeliverUnit
FROM Category INNER JOIN Article ON Category.ID = Article.CategoryID
因此,我将这种视图命名为“JoiningView”(连接视图),尽管它并非通过 SQL 实现。
注意,一个连接视图可以做到连接*SQL*永远做不到的事情:即更改外键,并将更改保存回去——看看一个 ComboBoxColumn 单元格的实际操作:
我将“Chai”的类别从“Getränketest”更改为“Gewürze”。
在后台,该商品的 CategoryID 获得了一个新值,现在指向了“Gewürze”类别的父行——这只是一个整数的改变。
要实现这种行为,首先将 Article 实体拖到窗体上,生成一个 DGV。然后通过智能标签打开“编辑列”,并将 CategoryID 列的类型从 `DatagridviewTextboxColumn` 更改为 `DatagridviewComboboxColumn`。
然后选择该 ComboBox 列的数据源。
请谨慎选择,即选择 其他数据源-项目数据源-NorthwindDts-Category。这样选择会生成一个新的 BindingSource。
然后选择 DisplayMember —— ComboBox 单元格应该显示哪个属性?
当然,要显示的最重要的是类别的*名称*。
接下来选择 ValueMember —— 当用户选择一个类别时,哪个属性是其“含义”?
它的“含义”是 `ID`:所选 CategoryRow 的主键。这个值将被写入 (Article.)`CategoryID` - (与 DataPropertyName 所表示的相比较)。
这很复杂,你必须很好地理解这4个元素的作用:
- DataPropertyName - `CategoryID`:绑定到此 DGV 列的数据列的名称。
这里,DGV 绑定到 ArticleDataTable,而 `CategoryID` 是指向 CategoryDataTable 的外键。 - DataSource - `Category`:父表,其行是 ArticleRows 的父行。
当组合框下拉列表展开时,这些行就会显示出来。 - DisplayMember - `Name`:表示 *Category.*`Name`
父行中可见的列,当下拉列表展开时会显示(因为组合框只能显示*一*列)。 - ValueMember - `ID`:表示 *Category*.`ID`
当用户选择一个类别时,这个值——`ID`——就会被写入到这个 DataGridViewColumn 的 DataProperty 中,该属性由 `DataPropertyName` 定义。
具体来说:所选类别的 `ID` 会被写入当前商品的 `CategoryID`(意味着该外键被更改)。
并且绑定是双向工作的:ComboBoxCell 既可以将另一个 Category.ID 写入 Article.CategoryID,也可以显示由该外键引用的文章类别的 Category.Name。
你看:与 Sql 相比,这是一个完全不同的概念和思维方式,它实现了连接其他(父)表的属性。
3 - m:n 视图
m:n 视图只是简单地结合了父子视图和连接视图。
看这个订单父表,以及所选订单的订单条目:
你看,在 OrderEntry 中有一个 ComboBox 列,它引用了另一个父表:Article——回想一下数据模型:
Order => OrderEntry <= Article
SQL 会像下面这样表达订单条目:
SELECT Article.Name, OrderEntry.Count
FROM Article INNER JOIN OrderEntry ON Article.ID = OrderEntry.ArticleID
WHERE OrderEntry.OrderID = <bsOrder.Current.ID>
4 - 详细视图
(“DetailView”这个词经常被不加区分地使用,有时指父子视图,有时指连接视图。所以似乎没有人区分细节是来自父级、子级,还是来自当前表本身。)
在我的术语用法中,“DetailView”意味着详细地呈现一条数据记录,也就是说:它的每个属性都在其自己的单项控件中呈现。
选择器控件(DGV、组合框、列表框)选择一条记录,一组单项控件(文本框、标签、数字UpDown、日期时间选择器)呈现所选记录的值——这些项控件不呈现其他相关的表,无论是父表还是子表。
构建详细视图基本上和父子视图一样简单:首先将实体拖到窗体上,以生成作为选择器的 DataGridView。
然后将数据源窗口的生成模式更改为“详细信息”。
然后将同一个实体*再次*拖到窗体上,放到一个空的 Panel 上。
结果
对于每个商品属性,我们都有一个标签和一个文本框——如果是布尔属性,则是一个复选框。
在详细视图中,我建议将显示的 DataGridView 列减少到只显示最重要的属性。
否则 GUI 会被数据展示弄得过于拥挤。
使用 DGV 的智能标签来“编辑列”。
然后移除所有列,直到只剩下 `Name`。在同一个对话框中,你也可以将 `Name` 的 AutoSizeMode 设置为 `Fill`。
结果
现在我建议移除无用的详细信息控件,并给复选框一个有意义的文本。
在运行时
自定义详细信息控件
通常你想要绑定除了数据源窗口生成的控件之外的其他控件。没问题,把你想要的控件放到窗体上,然后绑定它。
例如,我将 `Category.Name` 绑定到了一个 Groupbox 的 Text 属性。
你看到我在 Groupbox 属性网格中展开了 `(DataBindings)` 节点吗?
当然,我把 `Category.Image` 绑定到了一个 Picturebox 的 Image 属性上。
PictureBox 的绑定有点问题——每当数据记录改变时,PictureBox 都会将前一条数据记录标记为“已更改”——尽管 PictureBox 永远无法更改图片。
避免这种情况的变通方法是更详细地配置该绑定,为此请打开高级绑定对话框。
将数据源更新设置为“从不”,这个 Bug 就被绕过了。
自定义列表控件作为“选择器”
如前所述:可绑定的列表控件有:1) DataGridView, 2) Listbox, 3) Combobox。所以你并不一定需要将表绑定到 DataGridViews——例如,我额外绑定了一个 Combobox(为此我使用了 Combobox 的智能标签)。
注意:`DisplayMember=Name` —— 组合框将显示 `Category.Name` 的值。
还要注意,ValueMember 和 SelectedValue 保持未设置(这个组合框作为“选择器”,而不是“连接器”——我稍后会讲到这个)。
那个组合框可以像 DataGridView 一样选择一个类别——实际上,组合框和 DGV 是同步的,因为它们都绑定到同一个 BindingSource。
请看运行时数据绑定的 DGV、ComboBox、GroupBox、PictureBox 和(描述)Label:
![]() |
旁注:DGV 看起来像一个 Listbox,因为我设置了:
你看:DataGridView 的可定制性非常强。 |
自定义列表控件作为“连接器”
就像一个 ComboboxColumn 将父表的值连接到一个 JoiningView 中一样,一个 Combobox 也可以将父值连接到一个 DetailView 中。
回到我们的 Article-DetailView,并添加一个 Combobox,如下所示:
和 DGV-ComboboxColumn 一样,选择 `OtherDatasources.NorthwindDts.Category` 作为数据源,以确保生成一个新的 BindingSource,专门用于这个 Combobox。
否则,当多个控件绑定到同一个 BindingSource 时,可能会出现副作用。
完整的“连接”组合框配置
它与连接型 DGV-ComboColumn 非常相似——同样,4个元素至关重要:
- 数据源 - `CategoryBindingSource` - 提供显示在下拉列表中的数据行。
- DisplayMember - (Category.)`Name` 定义了要显示的所显示数据行的列。
- ValueMember - ID - 定义了当用户选择一个条目时的“含义”。
- Selected Value - `bsArticle-CategoryID` - 这与 DGV-Combos 有点不同。在 Combobox 中,Selected Value 可以是任何 BindingSource 的任何列。并且 Selected Value 会被持久化到那里。
这意味着:从选定的类别中,ValueMember——`ID`——将被写入 `bsArticle.CategoryID`。
这和 DGVComboColumn 中的情况一样:所选的上级主键被写入当前 ArticleRow 的外键中。
在运行时
并且是更改给定商品类别的一种工具。
DataExpression - 计算数据列
让我简要提及一个进一步的 DataColumn 特性,即 DataColumn.Expression 属性(我强烈建议点击链接查看)。
你可以分配一些小的数学公式,DataTable 会一直保持这些值的更新——类似于 Excel 单元格公式。
此外,计算列不需要持久化到数据库,因为这些值总是被重新计算的。
例如,我向 OrderEntry 添加了一个数据列 `Price`,其表达式为:
`Parent(ArticleOrderEntry).Price * Count` 很简单,不是吗?订单条目价格就是商品价格乘以数量。
然后我向 Order 添加了一个列 `OrderPrice`,其表达式为:
`Sum(child(OrderOrderEntry).Price)` 这也不是什么高深的技术——订单总价是所有订单条目子行价格的总和——还能是什么呢?
现在看看我的 m:n 视图:
还要注意 OrderEntry 新增的 `ArticlePrice` 组合框列,其 DisplayMember 是 (Article.)`Price`。在配置中,我将组合框的 DisplayStyle 设置为 Nothing,因为我不希望用户能够通过这个 DGV 组合框列来更改订购的商品。
因此,ArticlePrice 列看起来像一个文本框,但实际上是一个连接型组合框列。
并且我在 DefaultCellStyle 中做了一些设置。
即我选择了非等宽字体 `Courier New`,设置了货币格式,并右中对齐——这似乎不是呈现货币值的最差方式 ;)
示例应用程序
它并非设计上的奇迹,但它能工作,并展示了许多基本的数据绑定行为。
结论
呼——内容真不少,不是吗?
其中没有什么是真正难以理解的,但有些观点需要我们改变看法。
- 首先——在上一篇文章中——是理解关系模型和层次模型的区别。并且在关系模型中,父行*不*包含子行——而是每个子行都引用其父行。以及如何通过主键/外键来实现这一点——它们定义了数据关系。
- 然后需要理解的是,一个关系模型不是一个数据库——恰恰*相反*:没有数据库后端,一个类型化数据集更容易设计、构建和修改。一个底层的数据库总是一个第二位的、冗余的模型,而且将数据库模型正确地映射到客户端应用程序中使用的客户端应用程序模型总是一个挑战。数据库通常可以省略,但客户端应用程序模型永远不可或缺。
- 要明白,Sql 大部分时候是不需要的,因为它属于数据库模型,而不是客户端模型。
- 接下来是忘记代码、控件之类的东西。取而代之的是,思维必须变得非常简单,必须考虑*现实*。数据模型不必满足应用程序的任何需求。它只需要符合现实:关于模型的陈述同时也是关于现实的陈述。
- 然后学习使用数据集设计器,并学会熟练使用它,细心对待每个实体的每个属性:命名、数据类型、是否允许 Null、默认值,甚至可能是表达式。
- 学习并遵循数据模型指南(你可以选择不同于我提供的约定——主要的事情之一是始终如一地遵循一个特定的约定)。
- 然后最后开始使用数据源窗口进行数据绑定。
- 了解 BindingSource 是什么,以及它们的用途。并仔细命名它们,以防混淆。
- 学会了解许多智能标签、属性和配置对话框,以配置 BindingSources、DataGrids、DataGridViewColumns、ComboBoxes、TextBoxes、Labels、DatetimePicker... 甚至学会使用 Tabcontrol、TableLayoutPanel、GroupBox 和 SplitContainer 来布局数据呈现的区域。
仍有缺失的主题
- 我没有提到跨窗体数据绑定的问题,特别是如何构建数据输入对话框(输入掩码)。
WinForms 设计器是一个很棒的工具,可以设计出丰富而可靠的数据呈现界面,但不幸的是,它在每个窗体/对话框上都会生成自己的类型化数据集。结果是对话框没有数据,因为是主窗体填充了它的数据集,而填充对话框的数据集也是个坏主意,因为那最终会导致数据冗余混乱。 - 更深入地探讨 DatasetDesigner 生成的内容
- 针对类型化数据集进行整洁编码,避免代码坏味
大多数程序员,在幸运地构建了类型化数据集之后,会立即退回到非类型化编程——原因无他,只是因为他们不知道类型化数据集所提供的新的强大功能和特性。 - 分部类:可以通过分部类来扩展每个类型化的数据类,并在那里放置任意的业务逻辑。非常强大的功能!
在我看来,1) - 3) 对于开发(小型的)数据绑定驱动的应用程序也是必要的。
因此,这些主题现在已经在这篇的续篇中被涵盖,并且可以看作是进入一个特定“开发世界”的初步完成。