《Programming WPF》翻译 第4章 数据绑定

 

第四章数据绑定

任何应用程序都存在允许用户操作的数据,无论其来自对象,还是不同等级的或相关的资源。不管数据来自何处或者无论以什么格式,你可以对这些数据做很多事情,包括显示,转换,排序,过滤,关联,时常还会编辑它们。如果没有一种数据绑定的引擎,你将要手动地在UI和数据之间进行大批量的往返转换。适当的使用数据绑定,你只需要少量优雅的代码就可以获取更多的绑定样式——这样做通常是值得的。

4.1不使用数据绑定

考虑一个非常简单的应用程序:遍及一个人的名字和年龄,正如图4-1所示:

4-1

4-1可以实现为一个简单的xaml如示例4-1

示例4-1

在这个简单应用程序中显示的数据,可以被一个简单的类表现,如示例4-2所示。

示例4-2

 

通过这个类,可以自然的实现我们的应用程序行为,如示例4-3所示:

示例4-3

示例4-3的代码创建了一个Person对象,并且用Person对象的属性初始化了文本框。当Birthday按钮按下时,Person对象的Age属性值会增加,同时在一个消息框中显示更新后的Person数据,如图4-2所示。

4-2

我们的简单应用程序实现,事实上,非常的简单。Person对象的Age属性在改变后,显示在消息框中,但是不会显示在主窗体中。一个保持应用程序UI是最新的办法是,编写代码使得无论一个Person对象何时更新,将会同时间手动更新UI,正如示例4-4所示。

示例4-4

仅仅一行代码,我们就“修复”了这个应用程序。这是一个诱人而且流行的方法,然而不能随着应用程序变得复杂而伸缩,并且需要更多这样的“单行”代码。我们需要一个更好的方法,超越于最简单的应用程序之上。

4.1.1对象的改变

对于UI,一个更健壮的跟踪对象改变的方法是,当对象改变的时候为这个对象激发一个事件。从.NET2.0时开始,正确的方法是为这个对象实现INotifyPropertyChanged接口,正如示例4-5

示例4-5

在示例4-5中,当Person对象的任意一个属性改变时(如由Birthday按钮激活引起的实现),就会激活该对象的PropertyChanged事件。我们可以使用这个事件保持UI同步于Person的属性值,正如示例4-6

示例4-6

示例4-6显示了一个单独的Person示例,创建于主窗体第一次开始出现,使用Person值初始化了NameAge的文本框,订阅了随属性改变的事件,用来保持当Person对象改变时文本框仍然是最新的。在这段代码的恰当位置,birthday按钮的click事件句柄不需要手动更新文本框,当Tom的年龄改变的时候;代替的,更新Age属性引起层叠式事件保持年龄的文本框是最新的,随着Person对象的改变,正如图4-3所示。

4-3

步骤如下:

1.用户点击按钮,引起Click的事件被激活

2.Click句柄从Person对象获得年龄:9

3.Click句柄将Person对象的年龄为10

4.PersonAge属性设置器激发了PropertyChanged事件

5. PropertyChanged事件传递到UI代码的事件句柄

6.UI代码更新年龄的文本框,从9改为10

7.按钮的Click事件句柄显示一个消息框,显示新的年龄:10

在消息框显示Tom的新年龄之前,在表单中,年龄的文本框已经更新了,如图4-4所示

4-4

随着处理了InotifyPropertyChanged的事件,当对象的数据改变时,UI将更新以反映这种改变。然而,这仅仅解决了问题的一半;我们仍然需要处理如何将UI中的改变反映到对象中。

4.1.2控件的改变

不考虑跟踪UI改变并将其反映到对象的特定方法,我们可以容易地以一个例子告终(想改变一个人的名字),显示对象(正如点击Birthday按钮时所发的),以及期望改变已经发生,仅仅对图4-5有所失望。

4-5

注意到图4-5,表单中,名字是“Thomsen Federick”,而消息框中是“Tom”,这显示了UI的一部分已经改变,而底层的对象并未改变。为了修复这个问题,我们观察文本框对象的Text属性的改变,相应的更新Person对象,如示例4-7

示例4-7

现在,不论数据如何改变,Person对象和显示Person对象的UI会保持同步。图4-6显示了UI中名字的改变,正确传到了Person对象。

4-6

尽管我们得到了想要的功能,仍需要写相当多的代码使之发生:

  • Window1代码重构,设置控件的初始值
  • Window1代码重构,使用PropertyChanged事件钩子,对Person对象的属性更改进行跟踪。
  • PropertyChanged事件句柄从Person对象获取更新过的数据,将数据转换为适当的字符串
  • Window1代码重构,使用TextBox对象的TextChanged事件钩子,跟踪UI的改变
  • TextChanged事件句柄将更新过的TextBox数据传入Person对象,将数据适当的转换

这段代码允许我们安全的写自己的birthday按钮事件句柄,是所有的改变同步,当我们显示消息框的时候。然而,容易想象到,当对象的数量增加或者对象属性的数量增加时,这段代码很快就会失去控制。加上,这看起来就像一件相当普通的事情,以至于有人一定事先就提供了一种更简单的方法来做这件事。事实上,这种方法被称为“数据绑定”。

4.2.2 数据绑定

我们手动编写代码保证UI和数据同步。有效将两组属性隐式的绑定在一起,一组来自Person对象,另一组来自显示Person对象的控件。数据绑定用于显式的将属性从一个对象绑定到另一个,保持它们的同步,并转换为适当的类型,正如图4-7所示。

4-7

4.2.1 绑定

取代以在代码中手动设置TextBox对象的Text属性并保证它们是最新的,数据绑定允许我们使用Binding对象的实例来设置Text属性,正如示例4-8所示。

示例4-8

在示例4-8中,我们已经使用了在第一章介绍的属性元素语法,创建了一个Binding类的实例,初始化它的Path属性为“Age”,而且将Binding对象设置为TextBox对象的Text属性值。使用绑定的标签(也在第一章介绍过),我们可以示例4-8简写为示例4-9

示例4-9

作为一个更短的删节版,你可以一起省略指定Path,而且Binding也可以知道这是什么意思,正如示例4-10

示例4-10

我更喜欢显示的语法声明,因此我不会使用示例4-10的语法,但我不做评价——如果你喜欢用的话

Binding类提供了各种各样有趣的工具,用来管理属性间的绑定,但是我们更关心的是Path属性。大多数场合,你可能认为Path作为一个对象的属性名,是作为数据源。因此,示例4-10binding语句建立了一个TextBoxText属性和某个对象的Name属性之间的绑定,正如图4-8所示。

4-8

在这个绑定中,TextBox控件是绑定目标,它扮演一个消费者的角色——当绑定源——提供数据的对象有改变时。绑定目标可以是一个WPF元素,但是只允许绑定到元素的依赖属性。(在第9章介绍)

另一方面,你可以将任意公有的CLR属性绑定到绑定源,绑定源在这个示例中没有具体命名,因此我们有一些自由度——关于这个对象在运行期来自哪里,因此也易于将多个控件绑定到同一个对象上(像nameage这两个文本框,都绑定到同一个Person对象)。

普遍地,绑定源数据来自数据上下文

4.2.2 隐式的数据上下文

数据上下文是绑定机制寻找数据源的地方如果没有任何额外的特殊指令(我们随后要讨论这些指令)。在WPF中,每个FrameworkElementFrameworkContentElement都有一个DataContext属性,这个属性是Object类型的,所以可以将任何对象放入其中,例如stringPersonList<Person>等等。当寻找一个使用绑定源的对象时,绑定对象从它所定义的位置向上遍历控件树,寻找一个非空的DataContext属性。

这种遍历是便捷的,因为这意味着在同一个父控件中,任意两个控件都可以绑定到同一个数据源。例如,我们的两个文本框控件都是grid的子控件,而且每一个都会在同一个数据上下文中搜索,如图4-9所示。

4-9

工作步骤如下:

l        绑定机制在TextBox自身搜索非空DataContext属性

l        绑定机制在Grid搜索非空DataContext属性

l        绑定机制在Window搜索非空DataContext属性

为这两个文本框控件都提供一个非空DataContext,在Window1类的构造函数中,设置共用的Person对象作为gridDataContext属性,正如示例4-11

示例4-11

因此,为了我们的应用程序的功能性如图4-9所示,数据同步的代码减少到,为每一个显示数据的xaml属性设置一个绑定对象,以及使用数据上下文为Binding搜索数据。没有必要初始化UI代码或者事件句柄,来复制和转换数据(注意示例4-11中椭圆的不足)

清楚起见,实现INotifyPropertyChanged的用途绝非偶然。这是WPF数据绑定引擎保持UI同步于对象属性改变的接口。没有这个接口,UI的改变仍然可以传达到对象,但是绑定引擎没有办法知道什么时候改变UI

#一个没有实现INotifyPropertyChanged接口的对象发生改变,绑定引擎没有办法知道什么时候改变UI——这种说法不是完全正确。一种可以知道的方法是,如果一个对象实现了PropertyNameChanged事件——正如.NET 1.x中规定的数据绑定(如SizeChangedTextChanged等等)——而.NET是向后兼容的。另一种方法是,手动调用BindingExpression对象的UpdateTarget方法,联合赈灾讨论的绑定机制。

#然而,可靠的说,实现INotifyPropertyChanged是一种可取的方式,来支持属性改变通知,在WPF数据绑定中。

4.2.3可声明的数据

当我们的程序还在尝试去模仿更复杂的应用程序时,可能是从持久化形式加载“私人数据”,以及将其保存在应用程序的Session中,不难想象这样的场景,在编译期就知道某些数据的位置。可能是示例数据(如前面示例中的Tom);或者是众所周知的数据,而且在Session中不会改变,如

应用程序默认设置或错误信息。很多应用程序有独立于UI工作的字符串资源,但是仍然包装在这个应用程序中——这样做使之易于维护和本地化,将其脱离与UI逻辑本身,降低了数据与UI的耦合。到目前为止,在我们的示例中,我们已经将这些众所周知的数据保存在代码中,但是xaml是一个更好的选择,不仅因为易于在xaml中维护数据,还有xaml对本地化的支持(在第6章介绍)。

正如在第1章讨论到的,xaml是一种描述对象图表的语言,因此实际上任何带有默认构造函数的类型都可以在xaml中初始化,回忆示例4-2Person类有一个默认的构造函数,因此我们可以创建一个Person实例在我们的xaml应用程序中,如示例4-12

示例4-12

我们在window标签中的资源元素中创建了一些“数据岛”,使用xaml的映射语法(在第一章介绍),引进了Person类型。

通过在xaml中使用指定的Person标签,我们能够声明性的设置gridDataContext属性,而不是在后台代码文件中以编程方式设置,如示例4-13

示例4-13

现在,我们将Person对象的创建转移到了xaml中,我们已经更新了Birthday按钮的Click事件句柄,从使用一个成员变量到使用定义在资源中的数据,正如示例4-14所示。

示例4-14

在示例4-14中,我们使用了FindResource方法(在第一章介绍过,会在第6章详细介绍),用来从主窗体的资源中拖出Person对象。通过最小的改动,结果

是再次呈现图4-6所示的两个窗体。唯一的不同是不必接触后台代码文件就可以在编译期维护或本地化已知数据。(第6章讨论了本地化xaml资源)

4.2.4显示的数据源

一旦你已经得到一个命名的资源,你可以在xaml中显示的绑定对象源,而不是依赖于隐式的绑定到控件树上某处的一个非空的DataContext属性。显示的数据源方式是有用的,如果你有多个数据源,例如,两个Person对象。设置显示的数据源给绑定中的Source属性,如示例4-15所示。

示例4-15

在示例4-14中,,我们绑定了两个文本框到两个Person对象,使用Binding对象的Source属性,显示的绑定到每个Person对象。

#隐式绑定和显示绑定的对比

通常说,当我们在多个控件中共享数据时,我发现隐式的绑定方式最有用,因为所有需要的是一点代码,用来在一个单独的父对象上设置DataContext属性。另一方面,如果我已经得到多个数据源,我真的喜欢使用Source属性在我们Binding对象上,从而使数据来源更加清晰。

4.2.5数值转换

目前为止,我们的应用程序示例展示了绑定数据到文本框的文字。然而,这并没有完全阻止你绑定到控件的其他属性,例如ForegroundFontWeightHeight等等。举例来说,我们可能判定任何大于25岁的都是无所顾虑的,因此应该在UI中标记为红色(或者,它们是濒临灭绝的。无论哪一个都使你更加可能推荐这本书给你的朋友)。当某人的年龄随着点击Birthday按钮而增长,我们想要保持UI是最新的,这意味着我们已经得到了数据绑定的完美候选者。想象下面示例4-16的能力。

示例4-16

在示例4-16中,我们将age文本框的Text属性绑定到Person对象的Age属性,正如我们已经看到的,但是我们也已经将文本框的Foreground属性绑定到了Person对象的同一个Age属性。随着Tom年龄的改变,我们想要更新文本框的前景色。然而,由于AgeInt32类型的而ForegroundBrush类型的,这就需要一个从Int32Brush的映射,从而应用数据帮定从AgeForeground。这就是值转换器的工作。

一个值转换器(或者简称为“转换器”),是IValueConverter接口的一种实现,其中有两个方法:ConvertConvertBackConvert方法用于将源数据转换为UI目标数据,如从Int32BrushConvertBack方法用于将UI数据转换回源数据。在这两种情形中,dangqian值和希望转换的目标类型都需要传入方法中。

为了将一个Age属性的Int32类型转换为Foreground属性的Brush,我们可以在Convert方法中,按照我们满意的方式,实现无论哪一种映射。如图4-17

示例4-17

在示例4-17中,我们已经实现了Convert方法,以再次确认我们寻找到一个Foreground笔刷,并以不同的年龄分发适当的笔刷。既然我们没有提供任何工具来改变用来显示年龄的Foreground笔刷,就没有理由实现ConvertBack方法了。

#我选择AgeToForegroundConverter作为类的名称,是因为我有特殊的意图:我在我的转换器中生成了Int32装换为Brush的简单转换。即使这个转换器可以被嵌入到任何地方用来将Int32装换为Brush,我也可能有非常不一样的需求对于HeightToBackgroundConverter而言,仅仅作为一个示例。

一旦你得到一个转换器类,这将易于在xaml中创建一个实例,就像我们刚刚对Person对象所做的,如示例4-18所示。

示例4-18

在示例4-18中,一旦我们在xaml中有了一个命名的转换器对象,我们确定它作为Age属性和Foreground笔刷的装换器,通过设置绑定对象的Converter属性。图4-10显示了这一转换的结果。

4-10

在图4-10中,注意到随着Tom的年龄从开端增长,转换器转换笔刷的前景色从黑到红。当数据改变时这个改变迅速发生,并没有显示的代码强迫使之执行,正如使用其他种类的数据绑定一样。

4.3绑定到数据列表

目前为止,你已经看到一些示例将控件绑定到一个单独的对象。然而,更复杂的使用是绑定到一个对象列表。例如,想象一下,我们的对象数据源可以创建一个新类型表示Person对象的列表,正如示例4-19

示例4-19

我们可以挂起这个新的数据源列表,按照同样的方式绑定到它,就像绑定到一个单独的对象数据源上,如示例4-20

示例4-20

在示例4-20中,我们创建了一个People集合的示例而且通过三个Person对象导入它。然而,运行它将会如图4-6

4.3.1当前项

尽管文本框属性每次仅能被绑定到一个单独的对象上,在可能的被绑定到的对象列表中,绑定引擎提供了一个名为当前项的概念,正如图4-6所解释的。

缺省地,列表的第一项作为当前项的开始。由于我们列表示例的第一项与我们之前绑定的单独对象一样,所以看起来和图4-11显示的一样——Birthday按钮除外。

4-11

4.3.1.1获取当前项

回想当前Birthday按钮的click事件句柄(示例4-21)。

示例4-21

我们的Birthday按钮应该总是产生向当前人士祝贺生日的效果,但是到目前为止,当前人士却总是一样的,因此我们只能简化事情为直接到达单独的Person对象。既然我们已经得到了对象的列表,这个机制就不再使用了(除非你认为一个包含单词“InvalidCastException”消息框是可以接受的方式)。进一步而言,转换到People,我们的集合类,不会告诉我们那一个Person对象会在当前UI中显示,因为它不知道这些事情(也不需要知道)。由于这一点,我们将要必须建立“经纪人”在数据绑定的控件和集合项上,这个“经纪人”在这里被称为视图。

视图的工作是在数据之上提供服务,包括排序,过滤,以及此刻对于我们的意图来说最重要的:控制当前项。视图是详细数据的接口实现,在我们这种情形,就是ICollectionView接口。我们可以通过BindingOperations类的静态GetDefaultView方法访问这个数据上的视图,正如示例4-22所示:

示例4-22

为了取回联合了Family集合的视图,示例4-22BindingOperationsGetDefaultView方法进行了一次调用,提供了一个ICollectionView接口的实现。基于此,我们可以得到当前项,将它从集合中的一项转换为我们需要的对象(CurrentItem属性返回一个object对象),以及用它来显示。

4.3.1.2在数据项中导航

出了获取当前项外,我们也能改变当前项的位置,通过ICollectionView接口的MoveCurrentToXX方法,正如示例4-23所示。

示例4-23

ICollectionView接口的MoveCurrentToPrevious方法和MoveCurrentToNext方法,通过在集合中向后和向前的动作改变当前的选中项。如果我们沿着一个方向移动到列表的尽头或另一个尽头,IsCurrentBeforeFirstIsCurrentAfterLast属性将会告诉我们这一点。MoveCurrentToFirstMoveCurrentToLast方法帮助我们复原在到达列表的尽头之后,对于在途4-12中实现BackForward按钮,这将是很有用的。同样适用于FirstLast两个按钮(这将是你的一个机会,将学到的运用上去)。

4-12显示了从集合中第一个Person元素开始,向前移动的效果,包括基于Person对象的Age属性导致的颜色改变(这仍然以同样的方式工作)。

4-12

4.3.2数据列表目标

当然,目前为止,我们仅能做的是把用户列表数据推出来,而没有为这些数据提供一个控件可以准确的一次性显示多条数据,正如示例4-24中的ListBox控件。

示例4-24

在示例4-24中,ListBoxItemSource属性没有绑定到路径,等于是说:绑定到当前整个对象。注意到,这里也没有源,因此绑定会从找到的第一个非空的数据上下文开始工作。在这种情形中,第一个非空的数据上下文来自Grid,就是那个在nameage的文本框中共享的上下文。我们还设置了IsSynchronizedWithCurrentItem属性为true,以确保listbox中的选中项也能发生改变——这会在视图中更新当前项;反之亦然。

4-13

正如你可能看到的,图4-13中的每件事物都很完美。所发生的是,当你绑定一个完整对象时,数据绑定尽其所能显示每一个Person对象。无需特殊的指令,它会使用一个类型转换器来得到一个字符串表示。对于nameage,都是内嵌类型,具有内嵌转换,这将工作良好;但是也有不能很好工作的时候,对于一个不具备可视化生成的自定义类型,正如Person类型这种情形。

4.3.3数据模板

正确解决这个问题的做法是使用数据模板。数据模板是一棵元素树,可以在特定的上下文扩展。例如,对于每一个Person对象,我们希望能够像以下方式将nameage连接在一起:

Tom(age: 9)

我们可以把它想象成一个合乎逻辑的模板,如下:

Name(age: Age)

为了在listbox中为数据项定义模板,我们创建了一个DataElement元素,正如示例4-25

示例4-25

在这种情形中, ListBox控件有一个ItemTemplate属性,它接受一个DataTemplate对象示例。DataTemplate允许我们详细指出一个单独的子元素,用于绑定重复显示在ListBox控件的每一个数据项。在我们的例子中,使用了StackPanel将四个TextBlock控件放在一行中:2个绑定到每个Person对象的属性,两个是常量文本。注意到,我们使用AgeToForegroundConverter已经将Foreground绑定到Age属性,为了Age属性显示为黑色或红色,为了列表框和age文本框是一致的。

通过使用数据模板,我们经历了从图4-13到图4-14

4-14

注意到,列表框显示了集合中所有的条目,而且保持了视图同步于当前条目,当选择向前或向后的按钮按下时(实际上,你并不会从图4-14的部分截图真正“注意到”,但是相信我,确实是发生了)。此外,当Person对象的数据改变的时候,列表框以及文本框会保持同步,还包括Age的颜色。

4.3.1类型化数据模板

在示例4-25中,我们显示地为ListBox列表设置了数据模板。然而,如果一个Person对象显示在一个按钮或是其它什么元素中,我们最好分别详细指出那些Person对象的数据模板。另一方面,如果你想要Person对象有一个特殊的模板而不论其显示在哪里,你可以通过类型化的数据模板来实现。

示例4-26

在示例4-26中,我们将数据模板的定义提升到资源模块,并且使用标签的DataType属性标志这个数据模板是类型化的。现在,除非另外通知,每当WPF看到Person对象的一个实例,就会应用相应的数据模板。这是一条便利之路,保证数据以一致的方式显示,遍及于你的应用程序,而不用担心显示的位置。

4.3.4列表的改变

迄今,我们已经得到一个对象的列表,我们可以适当的进行编辑,以及在其中建立导航,甚至轻而易举地高亮显示某些数据,以及提供了一个自动搜索,表现那些没有装载的来自厂商的数据。考虑到我们已经到达的程度,你可能怀疑提供一个Add按钮是一件轻而易举的事情,正如示例4-27所示。

示例4-27

这个实现的问题在于,尽管视图可以判断出新条目的存在当你移动到这里的时候,而列表框本身却并不知道新增加的集合中的条目,正如图4-15

4-15

为了与图4-15显示的应用程序状态交互,我运行了这个程序,点击了Add按钮并使用Forward按钮导航到图中所示。然而,即使新人显示在文本框中,列表框仍然不知道添加了什么事物。同样地,如果有对象被删除,它也不会知道。就像数据绑定需要事先INotifyPropertyChanged接口,使用数据绑定的列表需要实现INotifyPropertyChanged这个接口,正如示例4-28

示例4-28

INotifyCollectionChanged接口用于通知数据绑定控件,有条目在绑定列表中添加或删除。尽管在你的自定义类型中实现INotifyPropertyChanged,从而支持两种方式的数据绑定在你的类型化属性上——这很普通;不普通的是实现你自己的集合类,这些类给你很少的机会实现INotifyCollectionChanged接口。取代之,你更加更能依赖于集合类的一项在.NET 框架类库中,用来实现INotifyCollectionChanged。这样的类数量很少,而且不幸的是,我们使用的保持着Person对象的集合类,并不在其中。当你受欢迎的度过你的夜晚和周末实现了INotifyPropertyChangedWPF提供了ObservableCollection<T>类,用于我们那些紧迫的职责,如示例4-29所示。

示例4-29

既然ObservableCollection<T>派生于Collection<T>,而且实现了INotifyCollectionChanged接口,我们可以使用它代替List<T>作为我们的Person集合,正如示例4-30

示例4-30

现在,当一个条目添加到或删除自Person集合,这些变化将要在数据绑定列表中反映出来,正如图4-6所示。

4-16

4.3.5排序

一旦我们适当地使数据目标每次显示多于一个事物,一个年轻人的爱好变得更多,当然,是喜欢的事物,正如对数据视图排序或者过滤。回忆视图经常位于数据绑定目标和数据源之间。这意味着可以越过我们不要显示的数据(被称为过滤,而且可以被直接覆盖),而且可以改变数据显示的顺序,又名排序。最简单的排序方法是通过操作视图的Sort属性,正如示例4-31所示。

示例4-31

这里我们通过检测SortDescriptionCollection暴露在外的ICollectionView.Sort属性,将排序视图和未排序视图拴在一起。如果没有排序方式的描述,我们首先对Name属性按上升方式排序,然后对Age属性按下降方式排序。如果有排序方式的描述,我们将其清除,重新排序——无论之前是如何排序的。虽然排序描述在适当的位置,任意新添加到集合中的对象将被添加到它们已经排好序的适当位置,正如4-17所示。

一个SortDescription对象集合应该覆盖大多数的情形,但是如果你需要更多一点的控件,你可以提供自定义排序对象的视图,通过实现IComparer接口,正如示例4-32

4-17

示例4-32

在设置了自定义排序的情况,我们必须做一个假设——详细明确地实现了ICollectionView,这里使用的是ListCollectionView,是WPF包装在IList的实现(由ObserverableCollection提供),来提供视图的功能性。此外还有其它没有提供自定义排序的ICollectionView接口实现,因此你要在想*使用这段代码前先测试一下。

*希望你在使用前也测试一下其它代码,但是指出这些事情并没有什么危害。

#尽管我肯定,当我们使用WPF1.0时,这将变得更好。从现在开始,视图实现了联合详细数据特征,正如在ListCollectionViewIList间进行匹配并没有文本化(至少现在我这么说)。这看起来有点有趣,CustomSort是视图实现类的一部分,并不是ICollectionView接口的一部分,因此让我们为之祈祷:Microsoft发布新的WPF版本改变这一点。

4.3.6过滤

正因为所有的对象按顺序显示使你快乐,这并不意味着你想要显示所有的对象。对于这些没用的出现在数据中的对象,却不属于这个视图,我们需要提供这个实现了CollectionFilterCallback委托*的视图,需要一个单独的对象作为参数并返回一个Boolean值表明这个对象是否应该被显示,正如示例4-33

*排序使用一个单方法的接口实现,是由于历史原因;而过滤使用一个委托,是因为在C#2.0中另外使用匿名委托机制,这是一个很流行的机制。

示例4-33

正如排序,通过使用一个恰当的过滤器,新条目被适当的过滤掉了,正如图4-18所示。

4-18

4-18中最上面的窗体显示了没有过滤器,中间的窗体显示了过滤了初始的列表,底部的窗体显示了添加一个成年人,过滤器仍然在恰当的位置。

4.4 数据源

目前为止,我们已经简单的处理了对象。然而,这并不是数据的唯一来源;XML和突然想到的相关数据库,都是流行的选择。更进一步地,由于XML

相关数据库并不能存储数据为.NET对象,某些转换可能需要支持数据绑定,正如你会想到的,需要数据源对象上的.NET属性。而且即使我们可以直接在xaml中声明对象,仍然希望有一个层间接地从其他源中拉数据,甚至于将这个工作交给一个工作线程,如果说取回是一个呆板的操作。

简而言之,为了对象的转换和加载,我们希望间接的而不是直接的声明方式。对于这个间接方式,我们必须致力于IDataSource接口的实现,其中一种就是数据对象源。

4.4.1数据对象源

一种对IDataSource接口的实现是,为所有的操作提供一个间接的层,这些操作用于生成要绑定到的对象。例如,如果我们想要在Web上加载一组Person对象,我们需增强一些代码中的逻辑,如示例4-34

示例4-34

在示例4-34中,RemotePeopleLoader类从People集合类中派生,在构造器中检索数据,因为对象数据源希望它创建的对象是一个集合,正如示例4-35

示例4-35

ObjectDataSource元素通常位于资源块中,按名称在xaml的其他位置中使用。TypeName属性引用了集合类的完整的限定名称。

#WPF中的大部分具有type参数的类,如DataTemplate元素的DataType属性,在设置中带上type扩展标记,这包括类,命名空间和使用mapping语法的编译集信息。

。。。若干行代码

然而,ObjectDataSource以自己的方式设置type的信息。

。。。若干行代码

愿望是美好的,现实是残酷的。在RTM版本之前,两种技术都是合理的。

伴随着对象数据源担当数据和绑定之间的中介者,我们需要更新代码,当我们遍历People集合时(现在是一个基本类RemotePeopleLoader,但是仍然是Person对象的容器),正如示例4-36所示。

示例4-36

由于Family资源现在是一个ObjectDataSource,本身就是IDataSource接口的实现,在示例4-26中,当我们需要People集合的时候,我们将Family中的资源转换为IdataSource,并从Data属性中拉出这个集合。

#即使数据源对象通过Data属性暴露他的数据,这并不意味着你必须绑定它。如果你注意到示例4-35,我们仍然像从前一样绑定了列表框。

<!--do not bind to Path=Data -->

<ListBox ItemSource=”{Binding}” …>

这样做的原因是WPFIDataSource提供内建的支持,因此没有必要间接地这样做。

4.4.1.1异步数据遍历

在示例4-35中,我们应用了Asynchronous属性,这是最有趣的一块功能:数据源对象提供给我们所欠缺的——当我们直接在xaml中声明对象图的时候。

Asynchronous属性设置为true时(默认为false),通过TypeName创建详细对象的任务就交给工作线程处理,当遍历过数据,仅仅在UI线程表现绑定。这与绑定到数据并不一样——数据是在网络流中遍历到的,但是这总比当一个长时间的遍历发生时阻塞了UI线程要好。

4.4.1.2传递参数

Asynchronous属性外,数据源对象还提供了Parameters属性,这是一个逗号分隔的字符串列表,作为一个字符串传递到由数据源对象创建的类型中。例如,如果我们要传递一组URL参数,用来尝试遍历其中的数据,我们可以使用Parameters参数如示例4-37

示例4-37

在示例4-37中,我们已经添加了一个包含两个URL的列表,这将被转换为调用RemotePeopleLoader有两个参数的构造函数,如示例4-38

示例4-38

不幸的是,如果我们把其他的数据类型放入由数据源对象的Property属性支持的参数列表,如整型,这将不会被转换,即使构造函数拥有适当的类型是有效的;数据源对象只支持创建带有无参或有参构造函数的对象。如果每一个数据都必须转换,你就不得不这么做了。

4.4.2 XMLDataSource

正如我提及的,对象是仅由数据绑定支持的,但数据究竟不仅仅存为对象。实际上,大部分数据并不存储为对象。一种日益流行的方法是把数据存储到XML。例如,示例4-39显示了我们的家庭数据,表示以XML的形式。

示例4-39

这个文件作为可执行应用程序,在同样的文件夹中是有效的,我们能够使用XmlDataSource绑定到它,正如示例4-40所示。

示例4-40

注意到,XmlDataSource的使用,带着一个相对的URL指向family.xml文件,这个Xpath表达式在Family根元素下推出Person元素。在XAML文件中唯一改变的是,使用ObjectDataSource绑定NameAgeTextBox控件,而我们使用Xpath表达式代替了Path表达式

#Xpath语法的说明草果了本书的范围,一个好的参考书目是,Essential XML Quick Reference by Aaron Skonnard and Martin Gudgin (Addison Wesley)

4.4.2.1 XML数据岛

如果你恰好在编译期知道你的数据,XML数据源也以同样的方式支持“数据岛”:由XAML直接创建对象,如示例4-41所示。

示例4-41

在示例4-41中,我们将XmlDataSource元素下的内容复制到了family.xml中,去掉Source属性而保留了Xpath表达式。

尽管如此,既然我们使用XML替代了对象数据,我们示例中的操作需要改动,如访问和改变当前项(正如我们对Birthday按钮的实现),添加新项,排序和过滤。简而言之,任何我们使用Person对象集合的地方,都需要改动。另一方面,在数据项之间移动的一系列方法ICollectionView.MovingCurrentToXxx()继续工作的很好,我们的AgeToForegroundValueConverter也是这样。

#IValueConverter.Convert的实现可以继续工作,因为我们对对象的字符串值进行语法解析,而不是直接将其转换为Int32。在Person对象的情形中使用转换是首选的,因为AgeInt32类型的,对其进行语法分析是不必要的。尽管如此,在XML以及我们的应用程序缺少XSD的情形,Age是一个String类型,因此解析它就是必要的了。

4.4.2.2 XML数据源和访问数据项

为了访问和操作XML数据源,取代你的自定义类型实例,你可以使用位于System.Xml命名空间的XMLElement实例,正如示例4-42所示。

示例4-42

在示例4-42中,首先要注意的是GetFamilyView的实现,我们不再直接寻找People集合,而是实现由Xml数据源提供的IEnumerable接口。IEnumerable.NET中你能拥有的最简单接口,仍然有一个集合——是GetdefaultView方法所需要的。

还要注意示例4-42中集合视图的CurrentItem属性,是一个XmlElement实例。为了增加age,我们访问元素的Age属性,取出它的值,将其解析为一个整型,增加它的值,再将这个整型转换为String类型,设置为当前元素的新的Age属性值。显示每一个属性不过是对成对属性的访问。

4.4.2.3XML数据源以及添加数据项

当添加(或移除)一个数据项时,最好访问XmlDataSource自身,从而可以访问Document属性来创建和添加新元素,正如示例4-43

示例4-43

这里,我们使用了XmlDataSource来获取XmlDocument,以及使用XmlDodument来创建一个叫做Person的新元素(使之符合其余Person元素),设置NameAge属性,以及在Family根元素下添加这个元素(在顶级Document对象上ChildNodes[0]是有效的)。

4.4.2.4 XML数据源以及排序

Xml数据源的条目进行排序,大概会想起我们要使用XmlElements进行处理,正如示例4-44

示例4-44

在示例4-44中,我们进行了排序,正如先前一样,但是我们从NameAge属性中拉出数据并适当的进行转换。

4.4.2.5 XML数据源以及过滤

XML的过滤机制非常像对象的过滤,只是我们使用XmlElements进行处理,正如示例4-45

示例4-45

这里我们的过滤器使用了匿名委托,将每一个数据项转换为一个XmlElement元素来进行过滤。

4.4.3相关数据源

目前的版本,WPF没有直接支持绑定到相关的数据库,而且间接的支持范围并不是很广。作为WPF一个关于当前状态的绑定到相关数据的示例,我建议WinFX SDK示例提名为“Binding with Data in an ADO DataSet Sample

4.4.4自定义数据源

如果你愿意利用为遍历对象提供的间接数据源,但是没有一个内嵌数据源会使你满意,一个自定义的IDataSource实现应该会获得成功。例如,代替创建RemotePersonLoader集合来加载或移除家庭数据(在集合的构造函数中添加集合项,无论如何都有点做作),我们将要创建一个自定义的IDataSource实现,来达到这一点,如示例4-46

示例4-45

在示例4-46中,通过创建一个People集合的实例,我们已经实现了IDataSource接口,而且,在构造函数中,在一个神秘的数据遍历过程之后,我们激发了一个事件,让数据绑定知道我们已经得到了数据,还有再次检查Data属性。这个协议特别有用——一旦你进行异步的数据遍历(像对象数据源那样)。

如果你的数据源通过自定义属性,像Asynchronous,一个或更多属性可以在运行期被改变。如果你已经得到了多个影响数据遍历的属性,你可能不想开始搜索新数据直到Refresh方法被调用,你可能开始于一个属性的改变,但是在客户端有机会改变其他的属性之前。

4.5主从复合(Master-Detail绑定

我们已经看到绑定一个单独的对象,还看到绑定一个单独的对象列表。另一种非常流行的方式是绑定多个对象列表,尤其是相关的列表。例如,如果你向用户显示一个客户列表,当他们选中其中一个客户,就会显示客户的相关订单,这时,你就会使用主从复合绑定。

主从复合绑定是一种过滤的形式,在主页面的列表部分,客户452设置了过滤器作为联合到从页面数据的参数,例如,客户452的相关订单。

到我们目前的讨论为止,我们并没有客户和订单这样的模型,但是我们已经有了家庭和人的定义,于是可以进一步形式化这些,如示例4-47

示例4-47

在示例4-47中,我们得到了熟悉的具有NameAge属性的Person类,聚合在熟悉的People集合中。进一步,我们有Family这样的类,具有FamilyName属性和People类型的Members属性。最后,我们有一个Families集合,聚合了Family类型的对象。换句话说,families包含着members,后者由带有年龄和名称的people组成。。

你可以想象到FamiliesFamilyPeople以及Person的实例如图4-19所示。

在图4-19中,Families集合构成了主页面的数据,保存着Family类的实例,每一个Family实例中持有一个People类型的Members属性,People类型中保存着Person这样的数据。你可以导入这种数据结构的实例,正如示例4-48所示。

4-19

示例4-48

在顶级绑定到这些数据,例如,显示这些家庭的姓名,如示例4-49所示。

示例4-49

在示例4-49中,我们在Families列(第0行)做了两件事。第一件将标题设置为常量“Families”字符串;第二件是形成了body主体,这是一个Family对象的清单,位于Families集合中,现时每一个家庭的FamilyName属性,正如图4-20所示。

4-20

4-20并不是一个主从复合结构,因为选中一个主页面的家庭并不会显示这个家庭联合到的详细信息。为了这么做,我们需要绑定到下一级,如示例4-50所示。

示例4-50

Members列(第1列),我们也设置了标题和主体,但是这次标题绑定到当前选中的Family对象的FamilyName

同样,回忆在Families列,我们的列表框的源条目通过不带Path属性的Binding语句,绑定到了整个集合。然而,在从页面中,我们想告诉数据绑定引擎,我们想要绑定到当前选中的Family对象的Members属性,这是一个Person对象的列表。图4-21显示了主从绑定的效果。

4-21

但是,稍等:有点过了。主从绑定并没有在这两个级别停下来,根本没有。你可以走得尽可能深,每一个从页面都是主页面的下一级。为了看到这个效果,让我们多增加一级从页面到我们的数据类,如示例4-51所示。

示例4-51

现在,不由仅家庭有了家庭名和成员——带有姓名和年龄的人,而每一个人都有一组特性,每一个都有其独自的描述。张开我们的xaml一小块,包括了这些特性,如示例4-52所示。

示例4-52

我们可以绑定第3级从页面,如示例4-53所示。

示例4-53

Families的列标题中,回想我们没有任何绑定;这个文本是硬编码的:

<TextBlock …>Families:</ TextBlock>

Members的列标题中,我们绑定了当先选中的Family对象FamilyName如下:

<TextBlock … TextContent=”{Binding Path=FamilyName}” />

逻辑上讲,你可以认为是对如下进行了扩展:

<TextBlock … TextContent=”{Binding Path=Family.FamilyName}” />

这里family是当前选中的Family对象。

取得这一级的更深一级,在具有特性的列标题中,我们将当前选中FamilyMembers属性绑定到当前选中PersonName属性,绑定如下:

<TextBlock …

TextContent=”{Binding Path=Members/Name}” />

再次,从逻辑上,你可以把它当作这样的扩展:

<TextBlock …

TextContent=”{Binding Path=family.Members.person.Name}” />

这里family是当前选中的Family对象,person是当前选中的Person对象。绑定表达式“/”担当了对象间的分隔符,每一级的对象假定为“当前选中的”。

列表框的源条目的绑定以同样的方式工作,除非我们想要的是当前选中PersonTRaits集合,而不是Name。我们的多级主从绑定的示例如图4-22所示。

4-22

4.6我们进行到哪里了?

根本上讲,数据绑定是关于在一个地方保持数据,例如,在另一个地方,一个控件的属性,在数据上同步一个对象的属性。在这一章,我们从深度和广度上遍历了数据绑定引擎这一基础概念,以及大量牵连到的,包括基于对象和XML数据格式的条目和列表,基于数据目标的条目和列表,管理当前项,值的转换,排序,过滤,数据模板,甚至主从页面的关系。

WPF的每一层对数据绑定的彻底支持,使之成为优秀的样式,在某种程度上数据绑定并不像以往那样。你会发现它已经渗透到WPF程序中的方方面面,包括样式,这将是下一章的主题。

posted @ 2008-03-17 13:05  包建强  Views(1115)  Comments(0Edit  收藏  举报