WPF - 绑定及惯用法(一) 转载自loveis715
简介
一个绑定常常由四部分组成:绑定源、路径、绑定目标及目标属性,同时转换器也是一个非常重要的组成。源用来标示源属性所存在的类型实例,路径用来标示需要绑定到的处于绑定源之上的源属性,绑定目标标示将接受相应更改的属性所在的实例,目标属性则用来标示接受绑定运行值的目标,而转换器则在源属性和目标属性不能直接赋值时执行转化工作。
这四部分组成之间的联动方式为:绑定源发出属性更新的通知,从而驱动绑定执行。其中源属性将作为绑定的输入,而绑定的输出则被赋予目标属性。如果绑定声明中标明了转换器,那么转换器将被执行,从而将源属性转化为合适 的目标属性。除了这些组成之外,绑定还常常使用转换器参数,绑定模式等各种信息影响绑定的运行。
声明绑定的方法
- Markup Extension形式:
<Button Content="{Binding Source=BindingSource, Path=BindingPath}"/>
- 也可以使用XML元素的形式:
<Button> <Binding Source="BindingSource" Path="BindingPath"/> </Button>
- C#代码中的形式:
1 Binding binding = new Binding("BindingPath"); 2 binding.Source = BindingSource; 3 mButton.SetBinding(Button.ContentProperty, binding);
绑定源和路径
容易混淆的地方:绑定源的声明在程序中是code部分对下面各种源类型的定义,而在xaml中就是各种源的<Tag>,而Binding里的Source或者ElementName是使用源的属性。
首先来看看绑定中的组成绑定源。
在WPF中,绑定源是最纷繁多变的绑定组成。软件开发人员可以将绑定源指定为特定的属性,也可以指定为集合,更可以指定为 ObjectDataProvider以及XmlDataProvider等更为复杂的结构。同时在介绍绑定源时,绑定路径也常常用来辅助标明绑定所实际 需要作为输入的属性,因此本节将同时介绍绑定源和绑定路径以及它们之间的配合使用。
DependencyObject类
首先要介绍的绑定源就是DependencyObject。该类默认提供了对绑定的支持。软件开发人员可以在该类型的派生类中使用 DependencyProperty.Register()等众多函数实现可绑定属性。其步骤主要分为两步:(有关如何创建 DependencyProperty以及创建它们所需要遵守的常见规则,请见“属性系统”一文)
- 在类型的静态初始化过程(如静态构造函数)中使用Register()等函数注册一个static readonly的DependencyProperty。(属性系统:访问权限)
- 在类型中声明一个属性。而在属性的访问符中使用GetValue()以及SetValue()完成对步骤1所声明的DependencyProperty的设置。(属性系统:访问符实现)
下面就是一段在DependencyObject类的派生类上实现DependencyProperty的示例:
public class CustomBindingSource : DependencyObject //若在要xaml中使用Cusotm control,则这里DO类可以继承自ContentControl { public string Source { get { return (string)GetValue(SourceProperty); } set { SetValue(SourceProperty, value); } } public static readonly DependencyProperty SourceProperty = DependencyProperty.Register("Source", typeof(string), typeof(CustomBindingSource), new FrameworkPropertyMetadata(string.Empty)); }
在完成了这些工作以后,软件开发人员就可以使用这个新创建的类型以及属性参与绑定了:
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>
//看xaml里的源首先指定的应该是类的实例:这里是别名为MainWindow的Window实例
//其次指定Path应该是具体DO对象的DP属性:这里是后台代码的DO对象BindingSource的Source属性
在WPF中,软件开发人员所常接触的大部分类型都派生自DependencyObject,因此派生自DependencyObject的类型非常适合在 UI层中作为绑定源。但由于并不是所有的WPF类型都派生自DependencyObject,而且DependencyObject上的所有属性并不全 是DependencyProperty,因此在使用一个类型及属性之前,软件开发人员需要检查该类型是否派生自DependencyObject,而该 属性是否在该类型中拥有相应的DependencyProperty。
INotifyPropertyChanged类
WPF只是一个UI 类库,而如果软件需要绑定使用非UI层的属性,那么从DependencyObject类派生并不是一个好的选择。正确的做法则是从INotifyPropertyChanged接口派生。实现了该接口后,类型实例可作为绑定源使用。
实现并使用该接口的步骤为:
- 声明PropertyChanged事件。绑定将侦听该事件并在事件发出后执行。
- 由于基类中的事件只能用来添加及删除侦听函数,因此软件开发人员需要提供一个派生类可访问的包装函数,以允许派生类发出PropertyChanged事件。该包装函数一般被命名为NotifyPropertyChanged(),并常常接受一个string类型的参数。
- 实现相应属性。在属性的访问符实现中,软件开发人员需要在属性的实际值发生更改后调用NotifyPropertyChanged()。
该接口实现的示例如下所示:
public class DataSource : INotifyPropertyChanged { protected void NotifyPropertyChanged(string property) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(property)); } public event PropertyChangedEventHandler PropertyChanged; public string Source { get { return mSource; } set { if (mSource == value) return; mSource = value; NotifyPropertyChanged("Source"); } } private string mSource = string.Empty; }
通常情况下,一个实现了INotifyPropertyChanged接口的可绑定类型的基类也常常需要是可绑定的。在这种情况下,软件开发人员可 以考虑将INotifyPropertyChanged接口实现为一个基类BindableBase,并令那些可绑定类型派生于该类(Wilma Project)。但是在选择该做法之 前,软件开发人员需要仔细考虑编程语言的单继承特性。这不仅仅决定于当前的类型继承关系,更会影响到程序的扩展性。
集合ObservableCollection类
如果软件开发人员希望一个集合参与绑定,并且在集合发生变化,如插入、移动数据等操作时使绑定工作,那么他需要使用实现 INotifyCollectionChanged接口的集合类型。WPF为软件开发人员提供了一个较为有用的实现该接口的集合类型:ObservableCollection。同时该类还实现了INotifyPropertyChanged接口,因此该集合类型暴露的各个属性也是 可绑定的。
需要注意的是,如果需要绑定ObservableCollection中的某项数据的属性(如Wilma里的某个集合ObservableCollection下的<Tool>的ToolName属性我们会设置为继承自INotifyPropertyChanged),如将ListBoxItem的Header属性绑定为数据的Name属性,那么该数据仍然需要是可绑定的。这是因为在该绑定中,绑定源是该数据,而不再是ObservableCollection,因此 ObservableCollection对INotifyPropertyChanged接口的实现并不会起作用。
CollectionViewSource类
以上所介绍的是最常用的绑定数据源。除此之外,软件开发人员还可以使用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类的方法如下所示:
<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类
另一种重要的数据源则是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接口。在这种情况下,清晰地了解这些解决方案的特征和优缺点才能更为合理地使用它们。虽然在前面对各个绑定 源的介绍中已经将这些内容贯穿于其中,但我仍然觉得需要在这里给出一个总结
顾名思义,ObjectDataProvider 就是把对象作为数据源提供给 Binding。前面提到的 XmlDataProvider,也就是把 XML 数据作为数据源提供给 Binding。这两个类的父类都是 DataSourceProvider 抽象类。之所以使用 ObjectDataProvider 来包装作为 Binding 源的数据对象,因为很难保证一个类的所有数据都能使用属性暴露出来,比如,我们需要的数据可能是方法的返回值。
添加以下 XAML 代码:
<StackPanel Background="LightBlue"> <TextBox x:Name="textBoxArg1" Margin="5"/> <TextBox x:Name="textBoxArg2" Margin="5"/> <TextBox x:Name="textBoxResult" Margin="5"/> </StackPanel>
接着在 C# 代码中添加一个类,用来计算前两个 TextBox 中的值,并在第三个 TextBox 中显示。代码如下:
class Calculator { //加法 public string Add(string arg1, string arg2) { double x = 0; double y = 0; double z = 0; if (double.TryParse(arg1,out x)&&double.TryParse(arg2,out y)) { z = x + y; return z.ToString(); } return "Input Error"; } //其它算法 }
然后添加一个用于绑定的方法,在构造函数中调用此方法:
private void SetBinding() { //创建并配置 ObjectDataProvider 对象 ObjectDataProvider odp = new ObjectDataProvider(); odp.ObjectInstance = new Calculator(); odp.MethodName = "Add"; odp.MethodParameters.Add("0"); odp.MethodParameters.Add("0"); //以 ObjectDataProvider 对象为 Source 创建 Binding Binding bindingToArg1 = new Binding("MethodParameters[0]") { Source=odp, BindsDirectlyToSource=true, UpdateSourceTrigger=UpdateSourceTrigger.PropertyChanged }; Binding bindingToArg2 = new Binding("MethodParameters[1]") { Source=odp, BindsDirectlyToSource=true, UpdateSourceTrigger=UpdateSourceTrigger.PropertyChanged }; Binding bindingToResult = new Binding(".") { Source = odp }; //将 Binding 关联到UI元素上 this.textBoxArg1.SetBinding(TextBox.TextProperty,bindingToArg1); this.textBoxArg2.SetBinding(TextBox.TextProperty,bindingToArg2); this.textBoxResult.SetBinding(TextBox.TextProperty, bindingToResult); }
运行程序后效果图:
解析以上代码:先是创建了一个 ObjectDataProvider 对象,然后用一个 Calculator 对象为其 ObjectInstance 属性赋值--这就把一个 Calculator 对象包装在了 ObjectDataProvider 对象里。接着,使用 MethodName 属性指定将要调用 Calculator 对象中名为 Add 的方法。
如果 Calculator 类里有多个重载的 Add 方法该怎么区分呢 ?由于重载方法的区别在于参数列表,紧接着的两句代码向 MethodParameters 属性中加入两个 string 类型的对象,这就相当于告诉 ObjectDataProvider 对象去调用 Calculator 对象中具有两个 string 类型参数的 Add 方法,也就是说,MethodParameters 属性是类型敏感的。数据源准备好之后,我们开始创建 Binding 。第一个 Binding 的 Source 是 ObjectDataProvider 对象、Path 是 ObjectDataProvider 对象 MethodParameters 属性所引用的集合中的第一个元素。BindingToSource=True;这句代码的含义是告诉 Binding 对象只负责把 UI 元素收集到的数据写入其直接 Source (即 ObjectDataProvider 对象)而不是被 ObjectDataProvider 对象包装着的 Calculator 对象。同时,UpdateSourceTrigger 属性被设置为一有更新就立即将值传回 Source。第二个Binding 对象是第一个的翻版,只是把 Path 指向了第二个参数。第三个 Binding 对象仍然使用 ObjectDataProvider 对象作为 Source,但使用 "."作为Path--前面说过,当数据源本身就代表数据的时候就使用 “.”作为 Path,并且“.”在XAML 代码中可以省略。
总结上面提到过的各种绑定源
可以看到,各个绑定源所提供的很多功能都是彼此重复的,如WPF在提供了DependencyObject类的情况下又提供了 INotifyPropertyChanged接口。在这种情况下,清晰地了解这些解决方案的特征和优缺点才能更为合理地使用它们。虽然在前面对各个绑定 源的介绍中已经将这些内容贯穿于其中,但我仍然觉得需要在这里给出一个总结。
首先是DependencyObject和INotifyPropertyChanged接口之间的区别。实际上,这两种绑定源分别对应着UI 层和数据层中的绑定源:DependencyObject是继承自DispatcherObject类的,拥有强制线程安全功能。由于WPF中的各个界面 组成具有单线程关联特性,因此DependencyObject类及DependencyProperty类更适合使用在界面代码中。反观 INotifyPropertyChanged接口则没有任何有关线程的约束,可以用来在Model层,具有更好的运行性能,更不会将WPF中的类型引入到数据层中。
相反地,ObservableCollection和INotifyCollectionChanged则不存在这种UI层和数据层之间的区 别。ObservableCollection类引入的原因非常简单:INotifyCollectionChanged接口的实现较为困难,软件开发人 员可能无法轻易地提供一个正确处理所有集合操作的集合类型。它们的使用不受软件层次的影响:无论在UI层还是数据层中,您都可以使用它。
下一个需要讨论的则是CollectionViewSource类。相信您从前面的介绍中已经看出,CollectionViewSource实际上是一个UI组成。其提供的是基于数据层之上执行筛选,排序等操作后的结果。筛选和排序等功能都是与UI相关的操作。
CompositeCollection则是常常声明于XAML中的UI组成。其作用也十分简单:将多个数据源合并在一起。在决定是否使用该类 的时候,软件开发人员常常面临的抉择就是,是否应该将其所使用的众多数据源合并在一起并提供一个新的数据源。而做出决定的常常是这些子数据源是否被其它代码 使用以及合并后的数据源是否有清晰的语义特征。
而DataSourceProvider则常常用来为较复杂的数据源提供一个包装,XmlDataProvider以及ObjectDataProvider等。
ItemSource和DataContext
这两个不是绑定源的直接类型,但是所有UI都会有DataContext属性,用来binding在不知道源的情况下指定制高点源。所以我们补充这两个属性的用法。
DataContext is a general (dependency) property of all descendants of FrameworkElement. Is is inherited through the logical tree from parent to children and can be used as an implicit source for DataBinding. It does not do anything by itself, you must basically databind to it.
ItemsSource
is the property identifying the source for templated generation of
items in an ItemsControl derived control (or HierarchicalDataTemplate).
When
you set the value of ItemsSource (either through DataBinding or by
code), the control will generate the templated items internally.
Setting DataContext on an ItemsControl will NOT achieve this.
Usually,
DataContext is set explicitly at the top of the hierarchy, and
sometimes overriden (re-bound explicitly) lower in the hierarchy.
Most
of the time, ItemsSource will be bound to a member of DataContext
(either direct or indirect, as a sub-property of a property of the
DataContext).
To use the canonical example of an Order with a few
properties, one of them a collection of OrderLine objects (oh how I
hate that example...), You could have a GUI with a first part detailing
the information pertaining to the order.
You Window or UserControl
DataContext would be data-bound to the Order Object, with a fex
TextBlocks/TextBoxes databound to the primary properties of the object.
Somewhere
down the line, you would have an itemsControl, (or ListBox, Listview,
whatever). Its DataContext would be the Order object because it is
inherited. Its ItemsSource property however would need to be data-bound
to the "OrderLines" property to display each OrderLine as an item in the
list.
For example, assuming a class looking like that (pseudo VB style, sorry)
Public Class Order Property OrderDate as Date Property Customer as Customer Property OrderLines as List(of OrderLine) End Class Public Class Customer Property Name as String End Class Public Class OrderLine Property Product as Product Property Quantity as Integer End Class Public Class Product Property Name as String End Class
You could have some XAML looking like that, assuming you set the DataContext on the top level object (a window or a UserControl for example):
<StackPanel> <StackPanel Orientation="Horizontal"> <TextBLock Text="Customer:" /> <TextBLock Text="{Binding Customer.Name}" /> </StackPanel> <StackPanel Orientation="Horizontal"> <TextBLock Text="Date:" /> <TextBLock Text="{Binding Date}" /> </StackPanel> <ListBox ItemsSource="{Binding OrderLines}"> <ListBox.ItemTemplate> <DataTemplate TargetType="{x:Type local:OrderLine}"> <StackPanel Orientation="Horizontal"> <TextBlock Text="{Binding Product.Name}" /> <TextBlock Text=" Qty= " /> <TextBlock Text="{Binding Quantity}" /> </StackPanel> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </StackPanel>
Disclaimer; I just typed that in the browser from memory, and I am using Silverlight and not WPF these days, so there might be some errors in there, but the basic idea is here.
So to come back to your
original question, using DataContext is not enough to bind an
ItemsControl to a list. You need to bind ItemsSource to achieve
auto-generation of items in the list.