以下转自: http://www.cnblogs.com/lichence/archive/2012/02/17/2356001.html#commentform 原文作者:Qwertie, 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精简框架。
目录:
注意以下内容:
因此,这些对象都代表什么意义?笔者在查阅相关文章的过程中发现对其(译者注:各DataSource属性的异同)描述甚为混乱,因此撰写本文以澄清视听。在现实生活中,您很可能会将一个BindingSource类对象赋值给列表控件或Binding类对象的DataSource属性。如果直接使用来源于数据库的数据信息,那么BindingSource类对象的DataSource属性通常是一个DataSet对象;否则,它将是您当前应用程序中一个自定义类型的实例对象。 开发者似乎可以用多种方式进行数据绑定,但笔者却没有发觉以上不同类型的DataSource的属性在拼写上有任何区别。所以笔者编写了一系列实验程序去深入了解这些异同。 让我们先从经典示例开始:将一个BindingSource类对象赋值给某控件的DataSource属性。可以将该BindingSource类对象想象为“二合一”的数据源,它包含以下两部分:
数据绑定的工作方式随控件的不同而相异:
备注1:在本文中,会经常将数据值绑在TextBox的Text属性上。其他的可绑属性如下:
备注2:在桌面应用程序中,微软鼓励开发者使用DataGrid的升级版本DataGridView。 备注3:ListView和TreeView的内容无法进行数据绑定(仅限于SelectedIndex和Enable这样的属性可以进行绑定)。但CodeProject上的一些文章已给出了解除该限制的方法。
本文中众多代码都是用了基于对象的数据源,定义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列中直接修改。但是,此例旨在演示两个控件之间的动态同步:
着实神奇!两个控件之间是如何通信的?其背后究竟发生了什么?事实上,BindingSource是原因所在。阅读MSDN文献可以了解到,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并遵循以下提示内容(MSDN文献),您可以跟踪调试.NET框架的源代码。这里还介绍一个特殊工具(CodeProject文献),该工具可以帮在其他版本的VS中实现调试.NET框架源码的功能,它会下载.NET框架的所有源码。此外,也可以使用Reflector和FileDisassembler插件查看代码,但是这个方法不允许开发者跟踪调试源代码。 不幸的是,涉及数据绑定的源码量极大且晦涩难懂,经过数小时的研究,笔者找出了一个控件通知BindingSource类对象的途径,即txtModel的Text属性被修改时,是如何通知DataGridView的?下面将对其过程进行描述,但其内容之复杂可能是您从未听过的。 长话短说,其步骤如下:
这个过程结束了。那么,PropertyDescriptor中的Airplane与txtModel的DataBindings集合中的Bindings相关联的情况下,BindingSource是如何将一个事件句柄与PropertyDescriptor中的Airplane相关联的?
这个框架结构很复杂。上述过程很难被探索清楚,因为BCL(基础类库)是被JIT优化的,这意味着一些函数并不会出现在堆栈试图(Debug时,按Ctrl D, C组合键)里,也无法在调试时看到一些变量。此外,智能感知功能(Intellisense)在该过程中无效,众多内部代码没有注释信息。但是笔者至少可以肯定BindingContext(记住这是各控件的BindingContext)类对象拥有针对数据源的特殊代码,这些代码实现了ICurrencyManagerProvider、IList和IListSource接口,因此,可以数据源必须实现这些接口中之一,以列表形式呈现。 对于BindingSource,它创建了一个BindingList<T>类型的List的属性,T是用户期望使用的一种类型,即本例中的Airplane类。BindingSource看起来并没有对DataSet有特殊支持,尽管其中有一些针对某些接口的特殊代码。
可惜整个绑定框架内部联系过于紧密(如类之间拥有众多引用和关系),笔者推荐使用实例程序和文字来描述这些绑定框架,尽管这些关系可能用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)方法: 再次运行程序,Passengers的全部数据均显示在列表中。 译者注:参考MSDN文献对BindingSource.ResetBindings()方法的介绍: 作用:使绑定到BindingSource的控件重新读取列表中的所有项,并刷新这些项的显示值。 参数:true,数据框架已更改;false,只有值发生了更改。
也可以不使用BindingSource进行数据绑定。例如,在Form1上添加一个名为button1的Button,重写Form1_Load()方法并注册button1_Click事件: button1负责两件事:
此例会让人产生疑问,何必还要再用BindingSource?
译者注:运行此例时,出现了如下错误,尚不清楚原因。欢迎反馈。
现在在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才能实现增加行的操作。相同的,需要删除时要相应的将其AllowRemove置为true。只有选中整行,再按[Del]键才能将其删除。
对于更复杂的场景,开发者会要求使用数据库数据,或者DataSet进行数据绑定。下面的示例程序与上节中相似,但是使用DataSet来存储Airplane和Passenger信息。笔者在代码中手工创建了DataSet架构,以避免读者连接数据库并获取数据的麻烦。构造DataSet示例数据的方法如下: DataSetMode_Load()的内容如下(//***标注了与上节例子不同之处): 运行程序和上节示例相同,另外,默认可以排序、新增行、删除行。
通常,如果使用DataSet进行数据绑定,bsP.Current指向一个DataRowView对象,在上节中的示例中则指向一个Passenger。但是,当你创建一个新行时,它并不包含Passengers,bsP.Current则为null。笔者没有发现任何文章讲解绑定架构是如何支持当前元素为空的情况,但至少数据绑定框架知道将txtName的内容清空。但是,会发现仍然可以修改其内容。 可以为bsP注册一个ListChanged事件以处理空Passengers的情况。
在真实的应用环境中,可能需要显示海量数据。如果需要根据用户提供的条件筛选列表,应当如何做呢? BindingSource提供了一个“Filter”属性接收一个布尔表达式,并以此作为筛选数据的依据。但BindingSource并不评估该表达式,而仅仅是将其传递给其List属性(必须实现IBindingListView接口)。在基于对象(即:Form1)的示例代码中,Airplane列表是一个BindingList<Airplane>列表,Passengers列表是一个List<Passenger>列表。这两类泛型集合都没有实现IBindingListView,因此该示例无法使用过滤(可以使用开源类库BindingListView为其添加过滤和排序功能)。 但是,可以对DataTable和DataView进行过滤。DataTable本身并没有实现IBindingListView接口,但它的DefaultView属性通过IListSource.GetList()方法返回的集合数据却实现了该接口。 为演示基于DataSet数据绑定的的过滤功能,在上节DataSetMode窗体中添加2个TextBox,即txtAirplaneFilter和txtPassengerFilter,并为之添加TextChanged事件,如下图所示: DataSet支持SQL风格的过滤表达式。本例中,对于txtAirplaneFilter可以使用Model like ‘*Bo*’或FuelLeftKg < 1000等表达式;txtPassengerFilter同理。如果表达式语法不正确,TextBox的背景色将被置为粉色。将过滤表达式置为空字符串时会清楚过滤器,就如同调用BindingSource.RemoveFilter()方法一样的效果。DateSource.CaseSensitive属性控制是否对过滤字符串大小写敏感。 备注:如果有两个列表绑定在不同的BindingSource上,但每个BindingSource都附加在了相同的DataTable之上,则两个列表将共享过滤字符串。若要对不同列表使用不同过滤字符串,则需要分别对其创建DataView,再附加到DataTable之上(通过DataView.Table属性),并将不同BindingSource的DataSource设置为不同的DataView。
通常,用户不希望输入一长串难以记忆的过滤字符串。因此,您可以只要求用户输入一些关键字以过滤数据。如何实现这一功能?使用一个委托来进行过滤不错,但DataView不支持这一做法。您必须在已经提供的过滤字符串语法的基础上,去尽量匹配用户输入的关键字。所以使用如下代码是明智的做法: 可惜,这么做并无法完全支持用户的过滤需求。比如,用户在文本框中输入含有单引号等特殊符号的字符串。笔者提供了一个转码方法来处理这些特殊字符。 之后就可以修改过滤器赋值语句为:
作者:dotNET程序猿 |
|