【译⽂】详解DataBinding-通过⼏个简单⽰例深⼊了解WinForm
数据绑定特性
原⽂作者:, Canada
博⽂主页:
原⽂地址:
免责说明:本⽂由CodeProject博⽂翻译⽽来,个⼈学习,仅供参考,欢迎指正,如有侵权,烦请告知删除。
翻译原因:在WPF/Silverlight数据绑定流⾏的时代,很多开发者并没有深⼊研究WinForm所提供的数据绑定机制,以⾄于很多⼈在编写应⽤时,仍在后台代码中操纵数据集合,并不断重新加载到数据控件上。如果您和我⼀样,还在这么做,不妨读⼀下这篇⽂章,改变⼀下编码⽅式。
详解Data Binding
通过⼏个简单⽰例深⼊了解WinForm数据绑定特性
简介:
关于WinForm数据绑定(Data Binding)的⽂章⾮常少。它究竟是如何⼯作的?您可以⽤它来做些什么?诚然,知道如何使⽤数据绑定的⼈不少,但真正了解它的运⾏机制的⼈可能寥寥⽆⼏。就笔者⽽⾔,仅是掌握如何使⽤它就耗时颇多,故深⼊研究以揭开其中奥秘。
“数据绑定”通常可理解为“控件(Controls)与数据表、⾏之间的⾃动同步”。在.NET框架(及.NET Compact Framework,即.NET 2.0精简版)中,您依旧可以这么定义,但它的真实概念已经扩展到了众多场景中,以⾄于您⼏乎可以将任何对象绑定到任何控件的任何属性上。    System.Windows.Forms.BindingSource是.NET 2.0框架中的⼀个新的程序集。微软希望开发者使⽤BindingSource替代其他旧的类型,诸如:CurrencyManager和BindingContext类,因此,本⽂仅帮助您深⼊了解BindingSource类并掌握其应⽤。
数据绑定可以使⽤反射机制,所以其应⽤范围不会局限于ADO.NET的DataSet中的数据表或⾏,⼏乎所有拥有属性(Property,译者注:本⽂中均译为“属性”)的类型都可以⽤于数据绑定。例如,通过数据绑定可以有效地实现⼀个可选对话框,⽽选项信息都存储在⼀个普通的.NET对象中。本⽂不是⼀篇关于ADO.NET、DataSet或DataGridView的教程⽂章,如有此⽅⾯需求,可以参考“相关⽂章”⼀节。
备注:在本⽂中,将假设您是⼀位已熟练掌握C#(包括ADO.NET),但对数据绑定知之甚少的读者,
以展开对数据绑定的讨论。
免责说明:本⽂中没有⼤篇幅的描述.NET精简框架对数据绑定的⽀持,所以不能保证这⾥所提到的所有内容都适⽤于.NET精简框架。
⽬录:
数据绑定API
注意以下内容:
控件的DataBindings集合属性保存被绑定的数据对象,DataBindings集合的每个元素都包含⼀个object类型的名为DataSource的属性。(译者注:DataBindings是ControlBindingsCollection类型,每个元素为Binding类型,DataSource属性包含于Binding类型中)。
常⽤的ListBox、DataGridView等控件的DataSource属性均为object类型。
BindingSource类也有⼀个object类型的名为DataSource的属性。
因此,这些对象都代表什么意义?笔者在查阅相关⽂章的过程中发现对其(译者注:各DataSource属性
的异同)描述甚为混乱,因此撰写本⽂以澄清视听。在现实⽣活中,您很可能会将⼀个BindingSource类对象赋值给列表控件或Binding类对象的DataSource属性。如果直接使⽤来源于数据库的数据信息,那么BindingSource类对象的DataSource属性通常是⼀个DataSet对象;否则,它将是您当前应⽤程序中⼀个⾃定义类型的实例对象。
开发者似乎可以⽤多种⽅式进⾏数据绑定,但笔者却没有发觉以上不同类型的DataSource的属性在拼写上有任何区别。所以笔者编写了⼀系列实验程序去深⼊了解这些异同。
让我们先从经典⽰例开始:将⼀个BindingSource类对象赋值给某控件的DataSource属性。可以将该BindingSource类对象想象为“⼆合⼀”的数据源,它包含以下两部分:
1. ⼀个名为Current的属性(包含⼀个单⼀数据对象)。可以将Current绑定在⼀个控件的某个属性上(译者注:⽐如将Current属性所含
的对象绑定在TextBox的Text属性上)。
2. ⼀个实现IList接⼝的名为List的属性。该列表所包含的众多对象都应与Current对象具有相同类型。List是⼀个只读属性,它将返回⼀
个BindingSource类对象的“内部列表(MSDN:⼀个空ArrayList)”(如果DataMember属性没有被赋值)
,或返回⼀个“外部列表”(如果DataMember属性已被赋值)。Current属性通常是List的⼀员(或为null)。当你将DataSource设置为⼀个单对象(⾮列表对象)时,List仅包含这⼀个元素。
数据绑定的⼯作⽅式随控件的不同⽽相异:
ComboBox和ListBox通过它们的DataSource和DisplayMember属性将数据绑定在List上。通常将⼀个BindingSource类对象赋值给它们的DataSource属性,并将DisplayMember属性设置为想要显⽰的字段名称(该字段即为Current属性所含的对象的某个字段)。
DataGrid和DataGridView通过它们的DataSource属性将数据绑定在List上。它们没有DisplayMember属性,因为他们可以同时显⽰多列值。DataGridView有⼀个附加的DataMember属性,该属性与BindingSource类的DataMember属性类似。不是所有类型的数据都可以设置为DataGridView的DataMember属性的值,除⾮其DataSource属性不是BindingSource类型的值(如果使⽤的
是BindingSource类对象,则需要设置BindingSource类对象的DataMember属性,以起到设置DataGridView的DataMember属性的值的作⽤)。
诸如TextBox、Button、CheckBox这样的“简单”控件,只能将DataBindings数据集合的Current对象绑定在控件上。事实上,列表控件也拥有DataBindings数据集合,但同样也不被使⽤。DataBindings数据集合可以在窗体设计界⾯中进⾏设置。
备注1:在本⽂中,会经常将数据值绑在TextBox的Text属性上。其他的可绑属性如下:
CheckBox和RadioButton的Checked属性。
ComboBox、ListBox和ListView的SelectedIndex属性。
ComboBox、ListBox的SelectedValue属性。
任何控件的Enable和Visible属性。
⼀些控件的Text属性。
备注2:在桌⾯应⽤程序中,微软⿎励开发者使⽤DataGrid的升级版本DataGridView。
备注3:ListView和TreeView的内容⽆法进⾏数据绑定(仅限于SelectedIndex和Enable这样的属性可以进⾏绑定)。但上的⼀些⽂章已给出了解除该限制的⽅法。
Airplane与Passenger⽰例类
本⽂中众多代码都是⽤了基于对象的数据源,定义Airplane和Passenger类如下:
⽰例程序中将使⽤⼀个DataGridView显⽰⼀组Airplane,⼀个TextBox显⽰Model属性,并⽀持对它的修改。
窗体设计中的绑定⽅式
在Visual Studio设计界⾯中建⽴数据绑定,⾸先新建⼀个Windows Forms⼯程,并创建上节中提及的Airplane和Passenger类。随后,在Form1中拖放⼀个名为“grid”的DataGridView和⼀个名为“txtModel”的TextBox。选择DataGridView,点击其右上⾓的⼩箭头弹出配置窗⼝,点击“Choose Data Source”下拉菜单,选择“Add Project Data Source”,弹出“Data Source Configuration Wizard”向导窗⼝。
备注:只有在Airplane和Passenger类被创建,并编译⼯程后,向导中才能看到它们。
选择“Object”,点击“Next”,在树状菜单中选择Airplane。配置向导将在组件托盘中创建⼀个BindingSource类对象,并将
其DataSource属性设置为Airplane类型。
配置向导同样会在DataGridView中创建3列,分别对应于Airplane的三个属性。但其中并没有⼀个列对应于Passengers列表,因为⼀个单⼀Cell中⽆法显⽰复杂的列表对象。(当然,可以在Cell中添加⼀个DropDownList来显⽰列表数据,本⽂中不再赘述。)打开TextBox的属性页,选择“Data Bindings(数据
绑定)”节点,点击“Advanced(⾼级)”的“…”按钮,弹出“Formatting and Advanced Binding(格式化与⾼级绑定)”窗⼝。在左侧“Property(属性)”属性菜单中,选择Text属性,之后点击中间的“Binding”下拉菜单,选择airplaneBindingSource的Model属性。
最后点击“确定”按钮。现在仅需要在airplaneBindingSource中添加数据即可。在Form1的后置代码中,创建Form1_Load()⽅法,并输⼊如下代码:
编译运⾏程序,可以得到如下结果。点击不同⾏,TextBox所显⽰的值也将随之改变。
备注:由于ID等属性是只读的,DataGridView⾃动禁⽌修改其对应值。然⽽其他⼀些控件并没有这么只能。例如,Model属性只读,但在txtModel依然可以对其进⾏修改,所以需要⼿动将txtModel的ReadOnly属性置为true。
这种绑定⽅式未尝不可,但笔者更倾向于在后置代码中进⾏数据绑定。重新开始,在设计窗⼝中删除airplaneBindingSource,使相关控件失去绑定。
后台代码⼿⼯绑定⽅式
修改后置代码如下,//***显⽰了与上节中不同的部分。我们需要⾃⾏创建BindingSource类。运⾏结果与上节⼀致。
DataGridView依然没有创建⼀列以显⽰Airplane的Passengers属性。添加了这些代码,便可不再依赖于设计窗体进⾏数据绑定。
笔者发现⼀件有趣的事,在Form1_Load()的代码中,数据绑定不会介意你编写代码的顺序并可以良好的运⾏。另⼀件有趣的事是,我们⽆需告知BindingSource需要保存哪种类型的数据对象,即可以删除对DataSource的赋值语句,但在第⼀次将数据添加
进BindingSource是,在其内部,会获悉数据的确切类型(如Airplane类),如果继续添加⾮此类型(如⾮Airplane类)时,它将抛
出InvalidOperationException异常。
此外,DataSource是⼀个不可思议的属性。如果直接将⼀个Airplane类对象赋给DataSource,以替代typeof(Airplane)的赋值语句。你能想象下⾯的Add()语句将发⽣什么吗?
运⾏程序会发现在列表中仅有Airbus和Cessna两条信息。这样写法的效果同下:
如果在代码中修改txtModel.Text的值,当前Airplane的Model属性并不会被更新,⾄少不会被⽴即更新(⼀些事件将会以某种⽅式触发更新⾏为)。⼀种更新⽅案是更新Model属性,再调⽤bs.ResetCurrentItem()⽅法以更新UI显⽰。
数据绑定是如何⼯作的?
当然,txtModel并不是必须的,因为可以在DataGridView的Model列中直接修改。但是,此例旨在演⽰两个控件之间的动态同步:如果改变当前选择⾏,txtModel会⾃动显⽰当前⾏的Model值。
如果修改txtModel值并按[Tab]键,其他控件中的Model信息将被刷新。
着实神奇!两个控件之间是如何通信的?其背后究竟发⽣了什么?事实上,BindingSource是原因所在。阅读可以了解
到,BindingSource“通过在Windows窗体控件与数据源之间提供流通管理(Currency Management)、更改通知(Change Notification)和其他服务简化了窗体上的控件与数据的绑定。”
流通管理?当笔者看到该概念时,想“是不是NumberFormatInfo起控制作⽤?”但事实证明“流通管理”与钱币流通⽆关,⽽是微软
对“Currentness”(译者注:暂译为“当前”)⼀词的叫法。换⾔之,BindingSource不断追踪List中那个元素为当前指定元素。在其内
部,BindingSource使⽤⼀个CurrencyMananger类对象,该对象保存了⼀个指向List的引⽤,并不断追踪当前元素。
在本例中,当⽤户编辑Model内容时,控件(txtModel)以某种⽅式修改BindingSource.Current对象,同时,BindingSource类对象触发CurrentItemChanged事件。事实上,单独的修改⾏为会触发多个事件,如果想知道它触发了哪些事件,可以将以下代码添加在
Form1_Load()⽅法中。
但是,控件是如何将其Text属性变更⼀事告知BindingSource类对象的呢?要知道,从控件的⾓度来看,DataSource仅仅是⼀个对象。
再考虑⼀下这些问题,控件是否使⽤⼀些特殊⼿段来⽀持BindingSource,或是它们实现了某种接⼝以使他们可以接受其他类
型?BindingSource时候使⽤⼀些特殊⼿段来⽀持DataSet,或是它是否仅关注某些⽅法或属性?换⾔之,数据绑定是否基于“Duck Typing”(即:鸭⼦类型,动态类型的⼀种风格,在该风格中,⼀个对象有效的语义,不是由继承⾃特定的类或实现特定的接⼝,⽽是由当前⽅法或属性的集合决定。参见),或它是针对某些类型、接⼝的特例?⼀般期望从数据源得到什么?
使⽤VS2008并遵循以下提⽰内容(),您可以跟踪调试.NET框架的源代码。这⾥还介绍⼀个特殊⼯具(),该⼯具可以帮在其他版本的VS中实现调试.NET框架源码的功能,它会下载.NET框架的所有源码。
此外,也可以使⽤和FileDisassembler插件查看代码,但是这个⽅法不允许开发者跟踪调试源代码。
不幸的是,涉及数据绑定的源码量极⼤且晦涩难懂,经过数⼩时的研究,笔者出了⼀个控件通知BindingSource类对象的途径,即txtModel的Text属性被修改时,是如何通知DataGridView的?下⾯将对其过程进⾏描述,但其内容之复杂可能是您从未听过的。
长话短说,其步骤如下:
通过txtModel.DataBindings.Add()⽅法添加的Binding类对象拥有⼀些指向TextBox的TextChanged和Validating事件的句柄。
句柄Binding.Target_Validate通过BindToObject和ReflectPropertyDescriptor等内部类传递新值。ReflectPropertyDescriptor类对象通过反射修改保存在Airplane中的Model的值,并调⽤它的基类PropertyDescriptor的OnValueChanged⽅法。
datagridview数据源类PropertyDescriptor的OnValueChanged调⽤⼀个与当前Airplane相关联的指向BindingSource.ListItem_PropertyChanged的委托。
该句柄触发它的ListChanged事件(事件参数ListChangedEventArgs包含了被修改元素的index值)。
CurrencyManager.List_ListChanged句柄与ListChanged事件相关联。该句柄触发⾃⼰的ItemChanged事
件,BindingSource.CurrencyManager_CurrentItemChanged句柄与之相关联。
该事件句柄(BindingSource.CurrencyManager_CurrentItemChanged)触发BindingSource.CurrentItemChanged事件,此事件将触发的Debug.WriteLine(“CurrentItemChanged”)⽅法。
随后,CurrencyManager.List_ListChanged触发⾃⼰的List_Changed事件。
DataGridView的内部类型拥有⼀个DataGridViewDataConnection.currencyManager_ListChanged句柄,负责刷新被修改的⾏数据。
最后,currencyManager_ListChanged句柄触发DataBindingComplete事件,但没有任何句柄处理该事件(⽤户可以注册该句柄的话)。
这个过程结束了。那么,PropertyDescriptor中的Airplane与txtModel的DataBindings集合中的Bindings相关联的情况
下,BindingSource是如何将⼀个事件句柄与PropertyDescriptor中的Airplane相关联的?
当为DataBindings集合添加新的Binding时,Binding.SetBindableComponent()将其引⽤赋给要绑定的控件,该控件拥有⼀
个BindingContext类对象,它管理了⼀个Bindings集合,该对象与DataBindings类似,但⼜有所区别。(具体区别可以参见
MSDN)。
Binding类对象(以下称之为"b")将它⾃⼰传递给BindingContext.UpdateBinding()⽅法。MSDN中讲到,该⽅法“将Binding类对象与⼀个新的BindingContext相关联。”
UpdateBinding()⽅法调⽤BindingContext.EnsureListManager(),此⽅法会通知Binding类对象的DataSource(注意:这是⼀
个BindingSource类对象)实现ICurrencyManagerProvider接⼝,所以Binding类对象会调
⽤ICurrencyManagerProvider.GetRelatedCurrencyManager(dataMember)⽅法,参数dataMember与当前Binding类对象相关联,此时,dataMember是⼀个空字符串。这是笔者发现的⼀个神奇之处,DataSource并不仅仅是被当作⼀个Object对象对待。可以看到,.NET框架使⽤了特殊的接⼝⽽⾮鸭⼦类型(基于反射)的⽅式来处理DataSource类对象。反射仅在获得Airplane的属性时使⽤。
此时,BindingSource有机会返回它的内部类对象CurrencyManager(以下称之为”c”)。
但是,此时UpdateBinding()⽅法并没有将b添加到BindingContext中,也没有将CurrencyManager赋给b。相反,c通过调⽤
c.Bindings.Add(b)将b添加给了CurrencyManager类对象。
c.Bindings是LsitManagerBindingsCollection类型的(这是⼀个内部类)。c.Bindings.Add(b)会调⽤⽅法b.SetListManager(c)。由
此,BindingSource的CurrencyManager最终与Binding b及b的BindToObject对象相关联。所以,当⽤户更改txtModel的Text属性并按[Tab]键时,b的BindToObject通过存储在b.BindToObject.fieldInfo中的⼀个ReflectPropertyDescriptor类对象以访
问CurrencyManager。在Form1_Load()⽅法中第⼀次调⽤bs.Add()⽅法时,ListBindingHelper.GetListItemProperties()⽅法代
表BindingSource创建了ReflectPropertyDescriptor类对象。该对象包含⼀个从Airplane到BindingSource.ListItem_PropertyChanged的关系映射图,因此,BindingSource可以被告知某Airplane类对象的信息已被修改。
这个框架结构很复杂。上述过程很难被探索清楚,因为BCL(基础类库)是被JIT优化的,这意味着⼀些函数并不会出现在堆栈试图(Debug时,按Ctrl+D, C组合键)⾥,也⽆法在调试时看到⼀些变量。此外,智能感知功能(Intellisense)在该过程中⽆效,众多内部代码没有注释信息。但是笔者⾄少可以肯定BindingContext(记住这是各控件的BindingContext)类对象拥有针对数据源的特殊代码,这些代码实现了ICurrencyManagerProvider、IList和IListSource接⼝,因此,可以数据源必须实现这些接⼝中之⼀,以列表形式呈现。
对于BindingSource,它创建了⼀个BindingList<T>类型的List的属性,T是⽤户期望使⽤的⼀种类型,即本例中
的Airplane类。BindingSource看起来并没有对DataSet有特殊⽀持,尽管其中有⼀些针对某些接⼝的特殊代码。
如果T实现了INotifyPropertyChanged接⼝,BindingList会订阅该接⼝的PropertyChanged事件。
BindingSource与CurrencyManager联系紧密,它拥有IList并维护列表中的当前元素位置。CurrencyManager包含⼀些特殊代码以⽀持列表数据,这些列表都实现了IBindingList、ITypedList或ICancelAddNew接⼝,⽽列表中的元素对象都实现了IEditableObject接⼝。
可惜整个绑定框架内部联系过于紧密(如类之间拥有众多引⽤和关系),笔者推荐使⽤实例程序和⽂字来描述这些绑定框架,尽管这些关系可能⽤UML表⽰更加⼀⽬了然。
数据绑定能做些什么?
⾸先,可以将包含列表的数据对象绑定在DataSource上。试⼀下如下代码:
不同于以前,代码中制定了DataMember属性。grid列表中只显⽰了a中的Passengers列表,为没有显⽰Airplane列表信息。
备注:如果要显⽰⼀个ADO.NET表,仅需⽤DataSet替换Airplane,⽤DataTable的名字替换这⾥的“Passengers”即可。
也可以直接为BindingSource(bs)或a.Passengers添加元素。但直接修改a.Passengers有时并不奏效,⽐如在Form1_Load()⽅法的尾部添加如下代码:
运⾏结果只显⽰了共四⾏数据(实际上Passengers中有6条数据),插⼊值只显⽰了“Oops 1”,当点击gird的不同⾏时,“Oops 2”会出现在第⼆⾏。数据绑定在此时之所以没有奏效,是因为BindingSource没有得到任何变更通知。可以按下图使⽤grid.Refesh()⽅法:运⾏结果显⽰了插⼊的两条数据,但并没有将全部6条数据显⽰出来。原因同上,可以再按下图使⽤bs.RestBindings(false)⽅法:再次运⾏程序,P
assengers的全部数据均显⽰在列表中。
译者注:参考对BindingSource.ResetBindings()⽅法的介绍:
作⽤:使绑定到BindingSource的控件重新读取列表中的所有项,并刷新这些项的显⽰值。
参数:true,数据框架已更改;false,只有值发⽣了更改。
不使⽤BindingSource进⾏绑定
也可以不使⽤BindingSource进⾏数据绑定。例如,在Form1上添加⼀个名为button1的Button,重写Form1_Load()⽅法并注册
button1_Click事件:
button1负责两件事:
当修改grid或txtModel时,底层数据源也同样被修改;
如果在底层数据源中添加⼀⾏数据,必须调⽤grid.ResetBindings()⽅法(但是⽤BindingSource进⾏绑定时,增加⼀⾏则不需要调⽤该⽅法)。
此例会让⼈产⽣疑问,何必还要再⽤BindingSource?
BindingSource可以在多个控件之间⾃动同步并显⽰源数据。该例之所以⼯作良好是因为两个控件的数据是相互独⽴的。(译者注:不太理解“相互独⽴”)
当增加或移除BindingSouce.List中的元素时,BindingSource⾃动刷新绑定的控件。
多个BindingSource可以形成绑定链(将在下节介绍)。
当修改Model时,奇怪的事情发⽣了,grid的CurrentRow.Index变为了0。笔者不知道为什么,但怀疑是在使⽤BindingSource时出发了这种改变。
译者注:运⾏此例时,出现了如下错误,尚不清楚原因。欢迎反馈。
分级式(Hierarchical)数据绑定
现在在Form1中添加⼀个ListBox以显⽰各Airplane对应的Passengers数据。UI和后台代码如下所⽰:
通过ListBox.DisplayMember属性指明显⽰Passengers.Name属性。但笔者不确定是否可以在其他场景下也使⽤类似的点号分隔表⽰法。
如果想修改Passengers的Name属性,可以增加⼀个TextBox,并在后置代码添加如下内容:
这类数据绑定⽅式被称之为“明细绑定(Master-Details)”。为实现此绑定模式,需要两个BindingSource类对象,因为⼀
个BindingSource类对象只有⼀个CurrencyManager,所以只能跟踪⼀个“当前记录”,txtModel绑定当前Airplane,txtName绑定当前所选
的Passenger。
在使⽤BindingSource时,尽管将DataGridView的AllowUserToAddRows属性设为true,它可能也不能新增⼀⾏(笔者对此很迷惑。译者注:将其值为出果然⽆法在UI上新增⾏)。这是因为BindingSource的List拥有⼀个名为AllowNew的属性,必须也将它置为true才能实现增