WPF - 绑定及惯用法(一)

2023-07-29,,

写在前面:这仍然是一些没有经过严格审阅的文字。虽然我的确执行了初稿、复稿以及审阅等一系列用以保证文章质量的方法,但是仍然担心其中是否有错误。希望您能帮助指出,以在下一次我在版本更新时进行修正。所有的错误,包括别字、概念不清(表述错误等)、边缘情况没有覆盖等,您认为有必要提及的各个方面,都可以是我们深入讨论的话题。

  绑定是WPF程序中所不可或缺的一项技术,同时其其强大的功能及灵活性也导致软件开发人员在编写绑定的过程中产生这样或那样的疑问。本文将会把精力主要集中在绑定中的重要惯用法以及如何(how)、为什么(why)以及何时(when)这样使用。

一.简介

  为了后面行文顺利,在进入正文之前,我们首先对本文所涉及到的绑定知识进行简单地介绍。该节包含绑定的基本组成以及构建方式。

  WPF中的绑定完成了绑定源和绑定目标的联动。一个绑定常常由四部分组成:绑定源、路径、绑定目标及目标属性,同时转换器也是一个非常重要的组成。绑定源用来标示源属性所存在的类型实例,路径用来标示需要绑定到的处于绑定源之上的源属性,绑定目标标示将接受相应更改的属性所在的实例,目标属性则用来标示接受绑定运行值的目标,而转换器则在源属性和目标属性不能直接赋值时执行转化工作。这四部分组成之间的联动方式为:绑定源发出属性更新的通知,从而驱动绑定执行。其中源属性将作为绑定的输入,而绑定的输出则被赋予目标属性。如果绑定声明中标明了转换器,那么转换器将被执行,从而将源属性转化为合适的目标属性。

  除了这些组成之外,绑定还常常使用转换器参数,绑定模式等各种信息影响绑定的运行。

  在XAML中声明绑定的方法有几种。首先是使用Markup Extension:

1 <Button Content="{Binding Source=BindingSource, Path=BindingPath}"/>

  接下来,还可以使用XML元素的形式:

1 <Button>
2 <Binding Source="BindingSource" Path="BindingPath"/>
3 </Button>

  最后,还可以在C#代码中创建绑定:

1 Binding binding = new Binding("BindingPath");
2 binding.Source = BindingSource;
3 mButton.SetBinding(Button.ContentProperty, binding);

  这三种形式都会在后面的内容中被使用,因此希望您能了解这些使用方法。

二.绑定详解

  首先来看看绑定中的组成绑定源。

  在WPF中,绑定源是最纷繁多变的绑定组成。软件开发人员可以将绑定源指定为特定的属性,也可以指定为集合,更可以指定为ObjectDataProvider以及XmlDataProvider等更为复杂的结构。同时在介绍绑定源时,绑定路径也常常用来辅助标明绑定所实际需要作为输入的属性,因此本节将同时介绍绑定源和绑定路径以及它们之间的配合使用。

  首先要介绍的绑定源就是DependencyObject。该类默认提供了对绑定的支持。软件开发人员可以在该类型的派生类中使用DependencyProperty.Register()等众多函数实现可绑定属性。其步骤主要分为两步:(有关如何创建DependencyProperty以及创建它们所需要遵守的常见规则,请见“属性系统”一文)

    在类型的静态初始化过程(如静态构造函数)中使用Register()等函数注册一个static readonly的DependencyProperty。(属性系统:访问权限)
    在类型中声明一个属性。而在属性的访问符中使用GetValue()以及SetValue()完成对步骤1所声明的DependencyProperty的设置。(属性系统:访问符实现)

  下面就是一段在DependencyObject类的派生类上实现DependencyProperty的示例:

 1 public class CustomBindingSource : DependencyObject
2 {
3 public string Source
4 {
5 get { return (string)GetValue(SourceProperty); }
6 set { SetValue(SourceProperty, value); }
7 }
8
9 public static readonly DependencyProperty SourceProperty =
10 DependencyProperty.Register("Source", typeof(string),
11 typeof(CustomBindingSource),
12 new FrameworkPropertyMetadata(string.Empty));
13 }

  在完成了这些工作以后,软件开发人员就可以使用这个新创建的类型以及属性参与绑定了:

1 public partial class Window1 : Window
2 {
3 public CustomBindingSource BindingSource…
4 }
1 <Window x:Class="BindingSource.Window1"
2 … x:Name="MainWindow">
3 …
4 <TextBlock Grid.Row="1" Grid.Column="0" Grid.RowSpan="2"
5 Text="{Binding ElementName=MainWindow, Path=BindingSource.Source}"/>
6 </Window>

  在WPF中,软件开发人员所常接触的大部分类型都派生自DependencyObject,因此派生自DependencyObject的类型非常适合在UI层中作为绑定源。但由于并不是所有的WPF类型都派生自DependencyObject,而且DependencyObject上的所有属性并不全是DependencyProperty,因此在使用一个类型及属性之前,软件开发人员需要检查该类型是否派生自DependencyObject,而该属性是否在该类型中拥有相应的DependencyProperty。

  WPF只是一个UI 类库,而如果软件需要绑定使用非UI层的属性,那么从DependencyObject类派生并不是一个好的选择。正确的做法则是从INotifyPropertyChanged接口派生。实现了该接口后,类型实例可作为绑定源使用。

  实现并使用该接口的步骤为:

    声明PropertyChanged事件。绑定将侦听该事件并在事件发出后执行。
    由于基类中的事件只能用来添加及删除侦听函数,因此软件开发人员需要提供一个派生类可访问的包装函数,以允许派生类发出PropertyChanged事件。该包装函数一般被命名为NotifyPropertyChanged(),并常常接受一个string类型的参数。
    实现相应属性。在属性的访问符实现中,软件开发人员需要在属性的实际值发生更改后调用NotifyPropertyChanged()。

  该接口实现的示例如下所示:

 1 public class DataSource : INotifyPropertyChanged
2 {
3 protected void NotifyPropertyChanged(string property)
4 {
5 if (PropertyChanged != null)
6 PropertyChanged(this, new PropertyChangedEventArgs(property));
7 }
8
9 public event PropertyChangedEventHandler PropertyChanged;
10
11 public string Source
12 {
13 get { return mSource; }
14 set
15 {
16 if (mSource == value)
17 return;
18
19 mSource = value;
20 NotifyPropertyChanged("Source");
21 }
22 }
23
24 private string mSource = string.Empty;
25 }

  通常情况下,一个实现了INotifyPropertyChanged接口的可绑定类型的基类也常常需要是可绑定的。在这种情况下,软件开发人员可以考虑将INotifyPropertyChanged接口实现为一个基类BindableBase,并令那些可绑定类型派生于该类。但是在选择该做法之前,软件开发人员需要仔细考虑编程语言的单继承特性。这不仅仅决定于当前的类型继承关系,更会影响到程序的扩展性。

  如果软件开发人员希望一个集合参与绑定,并且在集合发生变化,如插入、移动数据等操作时使绑定工作,那么他需要使用实现INotifyCollectionChanged接口的集合类型。WPF为软件开发人员提供了一个较为有用的实现该接口的集合类型:ObservableCollection。同时该类还实现了INotifyPropertyChanged接口,因此该集合类型暴露的各个属性也是可绑定的。

  需要注意的是,如果需要绑定ObservableCollection中的某项数据的属性,如将ListBoxItem的Header属性绑定为数据的Name属性,那么该数据仍然需要是可绑定的。这是因为在该绑定中,绑定源是该数据,而不再是ObservableCollection,因此ObservableCollection对INotifyPropertyChanged接口的实现并不会起作用。

  以上所介绍的是最常用的绑定数据源。除此之外,软件开发人员还可以使用CollectionViewSource、CompositeCollection等作为数据源。这些数据源在特定条件下会提供非常精美的解决方案。

  首先来看看CollectionViewSource类。该类提供了对数据进行筛选、排序以及分组等众多功能的支持。或许我们在了解该类的使用之前应首先了解我们为什么需要它。考虑下面一系列问题:如果我们需要为程序所显示的列表提供筛选功能,那么界面下所对应的数据结构是否应该是一个经过筛选的集合?如果原始集合中的数据项发生了改变,如添加或删除条目,条目的位置发生了移动等,那么这个经过筛选的集合如何进行同步处理?其实这是一个较为复杂的逻辑。编写出处理该事务的完全正确的逻辑实际上并不是一件容易的事情。而WPF通过集合视图所提供的功能让我们避免了该问题。

  WPF中,集合视图是出于绑定源集合之上的一个层次。如果绑定的源集合发生了变化,那么这些变化将会通过特定接口,如INotifyCollectionChanged,将消息传递到集合视图中。

  在绑定到一个集合的时候,绑定实际操作的是与该集合所关联的集合视图,而不是直接操作集合。在执行筛选等功能的时候,集合视图中的数据将会被筛选,以促使绑定正确地显示所有被操作过的条目。由于视图并不会更改源集合,因此每个源集合可能包含多个关联的视图,从而允许在一个界面中使用不同的控件显示集合内容,如一个控件显示排序后的数据,而另一个控件显示分组的任务。软件开发人员仅仅需要直接实例化视图对象并将其用作绑定源即可。

  在WPF中,表示集合视图的类型为CollectionView。WPF中的所有的集合都有一个与之关联的默认集合视图,如CollectionView是所有实现IEnumerable接口的类型的默认集合视图、ListCollectionView是实现IList的默认集合视图、BindingListCollectionView则是可绑定接口IBindingList的集合视图类。其中ListCollectionView以及BindingListCollectionView都是CollectionView的派生类。CollectionView的另一个派生类为ItemCollection,其主要与ItemsControl关联。而在ItemsControl类的内部实现中,ItemsControl.ItemsSource属性则通过CollectionView向ItemsControl隐藏各接口的区别。

  软件开发人员可以使用GetDefaultView函数得到默认集合视图。同时CollectionView类还和DataProvider一样提供了DeferRefresh函数。该函数可以用来提高WPF的运行性能。

  如果要在XAML中使用CollectionView,那么软件开发人员需要使用CollectionViewSource。与之对应地,CollectionView则没有可以在XAML中使用的构造函数。CollectionViewSource可直接使用在XAML中并隐藏了众多的CollectionView所提供的不必要功能。该类型所提供的最主要属性就是表示数据源的Source属性,而View属性用来表示与之关联的CollectionView实例。最常用的一种做法则是将CollectionViewSource定义为一个资源,并在为集合属性赋值时使用绑定与其关联。

  在XAML中使用CollectionViewSource类的方法如下所示:

1 <Window x:Class="BindingSource.Window1"
2 … x:Name="MainWindow">
3 <Window.Resources>
4 <CollectionViewSource x:Key="history" Filter="OnFilterItem"
5 Source="{Binding History, ElementName=MainWindow}"/>
6 </Window.Resources>
7 …
8 <ListBox ItemsSource="{Binding Source={StaticResource history}}"/>
9 </Window>

  接下来的问题则是:如果绑定需要使用来自于两个集合的数据,那应该怎么做?答案则是CompositeCollection。该类用来将多个集合以及单一数据项合并为一个可绑定数据项。在XAML中,该类的使用方法如下所示:

<Window …
xmlns:sys="clr-namespace:System;assembly=mscorlib" x:Name="MainWindow">
<Window.Resources>
<CollectionViewSource x:Key="history" …/>
<CompositeCollection x:Key="allHistory">
<sys:String>Predefined item 1</sys:String>
<sys:String>Predefined item 2</sys:String>
<CollectionContainer Collection="{Binding Source={StaticResource history}}"/>
</CompositeCollection>
</Window.Resources>

<ListBox ItemsSource="{Binding Source={StaticResource allHistory}}"/>
</Window>

  CompositeCollection实现了INotifyCollectionChanged接口,因此可以作为绑定的源。其可以将多个数据集合以及数据项混合。在向其中添加数据集合的时候,软件开发人员需要使用CollectionContainer类包装该集合。

  CompositeCollection内部记录的是两种类型的数据:数据项及CollectionContainer。在加入一个CollectionContainer的时候,CompositeCollection会添加对CollectionContainer所发出事件的侦听,因此CompositeCollection会响应CollectionContainer的数据变化。

  另一种重要的数据源则是DataSourceProvider以及它的派生类。WPF提供该类的目的则是为了允许软件开发人员将原有数据源作为绑定源使用,如ADO。我将使用单独的一篇文章讲解该类型的具体使用方法。而在本文中,我们将仅仅讨论其与绑定相关的各个方面。

  首先要提到的则是Data属性。如果一个DataSourceProvider类作为绑定的源,那么它的Data属性将记录生成的绑定源对象。在IsInitialLoadEnabled属性的值为true的情况下,绑定将在首次运行时查询DataSourceProvider类并设置其Data属性。该属性的设置有时会对程序的执行效率拥有较大影响,因此软件开发人员应谨慎设置该属性的值。

  Binding.BindsDirectlyToSource属性则是一个专门针对DataSourceProvider的属性,软件开发人员可以通过将该属性设置为true绑定到实际的数据,如MethodParameters属性中的数据,并使该实际数据随绑定目标而变化。此时绑定可以通过目标属性的更改,如TextBox的Text属性,完成对实际数据的更新,从而导致包装的结果也随之更新。

  XmlDataProvider以及ObjectDataProvider则是该类的两个派生类。XmlDataProvider允许用户访问XML数据。ObjectDataProvider则能够在XAML中以如下方式创建绑定源对象:使用MethodName和MethodParameters属性执行函数调用;使用ObjectType指定类型并通过ConstructorParameters属性将参数传递给对象的构造函数;直接为ObjectInstance属性赋值指定需要用作绑定源的对象。

  可以看到,各个绑定源所提供的很多功能都是彼此重复的,如WPF在提供了DependencyObject类的情况下又提供了INotifyPropertyChanged接口。在这种情况下,清晰地了解这些解决方案的特征和优缺点才能更为合理地使用它们。虽然在前面对各个绑定源的介绍中已经将这些内容贯穿于其中,但我仍然觉得需要在这里给出一个总结。

  首先是DependencyObject和INotifyPropertyChanged接口之间的区别。实际上,这两种绑定源分别对应着UI层和数据层中的绑定源:DependencyObject是继承自DispatcherObject类的,拥有强制线程安全功能。由于WPF中的各个界面组成具有单线程关联特性,因此DependencyObject类及DependencyProperty类更适合使用在界面代码中。反观INotifyPropertyChanged接口则没有任何有关线程的约束,具有更好的运行性能,更不会将WPF中的类型引入到数据层中。

  相反地,ObservableCollection和INotifyCollectionChanged则不存在这种UI层和数据层之间的区别。ObservableCollection类引入的原因非常简单:INotifyCollectionChanged接口的实现较为困难,软件开发人员可能无法轻易地提供一个正确处理所有集合操作的集合类型。它们的使用不受软件层次的影响:无论在UI层还是数据层中,您都可以使用它。

  下一个需要讨论的则是CollectionViewSource类。相信您从前面的介绍中已经看出,CollectionViewSource实际上是一个UI组成。其提供的是基于数据层之上执行筛选,排序等操作后的结果。筛选和排序等功能都是与UI相关的操作。

  CompositeCollection则是常常声明于XAML中的UI组成。其作用也十分简单:将多个数据源合并在一起。在决定是否使用该类的时候,软件开发人员常常面临的抉择就是是否应该将其所使用的众多数据源合并在一起并提供一个新的数据源。而做出决定的常常是这些子数据源是否被其它代码使用以及合并后的数据源是否有清晰的语义特征。

  而DataSourceProvider则常常用来为较复杂的数据源提供一个包装,如XmlDataProvider以及ObjectDataProvider等。
  (未完待续)

http://www.cnblogs.com/loveis715/archive/2011/12/11/2284063.html

WPF - 绑定及惯用法(一)的相关教程结束。

《WPF - 绑定及惯用法(一).doc》

下载本文的Word格式文档,以方便收藏与打印。