【转】【WPF】WPF绑定用法

一.简介

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

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

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

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

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

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

<Button>
     <Binding Source="BindingSource" Path="BindingPath"/>
 </Button>

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

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

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

二.绑定详解

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

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

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

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

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

public class CustomBindingSource : DependencyObject
{
    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));
}

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

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

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

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

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

  1. 声明PropertyChanged事件。绑定将侦听该事件并在事件发出后执行。
  2. 由于基类中的事件只能用来添加及删除侦听函数,因此软件开发人员需要提供一个派生类可访问的包装函数,以允许派生类发出PropertyChanged事件。该包装函数一般被命名为NotifyPropertyChanged(),并常常接受一个string类型的参数。
  3. 实现相应属性。在属性的访问符实现中,软件开发人员需要在属性的实际值发生更改后调用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,并令那些可绑定类型派生于该类。但是在选择该做法之前,软件开发人员需要仔细考虑编程语言的单继承特性。这不仅仅决定于当前的类型继承关系,更会影响到程序的扩展性。

  如果软件开发人员希望一个集合参与绑定,并且在集合发生变化,如插入、移动数据等操作时使绑定工作,那么他需要使用实现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类的方法如下所示:

<Window x:Class="BindingSource.Window1"
  … x:Name="MainWindow">
  <Window.Resources>
    <CollectionViewSource x:Key="history" Filter="OnFilterItem"
      Source="{Binding History, ElementName=MainWindow}"/>
  </Window.Resources><ListBox ItemsSource="{Binding Source={StaticResource history}}"/>
</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等。

在前面对绑定数据源进行介绍的过程中,本文都是使用Binding类的Source属性指定数据源的。使用该属性访问绑定源具有一些限制:软件开发人员无法引用XAML中定义的元素或依某种规律查找与绑定源相关的元素。因此除了Source属性之外,WPF还提供了ElementName、RelativeSource等方法以辅助完成对绑定源的指定。

  ElementName用来引用(甚至是后向引用)在同一NameScope中显式指定名称的界面组成。绑定将沿其所在的元素沿逻辑树向上查找,直到遇到第一个具有NameScope的元素并尝试在该NameScope中查找该名称。(并不精确,却会是您所接触的大多数情况)

  ElementName属性与Source属性互斥,即在同时设置这两个属性的时候,程序会在运行时抛出异常。在正确引用了所需要指定的界面元素以后,当前绑定的绑定源将会是该界面元素,而具体需要绑定的属性则由Path属性所指定。

  RelativeSource则用来指定与当前界面元素相关的绑定源。使用它进行查找的方式分为几种。查找自身时,使用Self模式:

{Binding RelativeSource={RelativeSource Self}}

 查找祖先的特定类型时,使用FindAncestor模式:

 {Binding Path=ActualWidth, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ScrollContentPresenter}}

  绑定应用模板的数据项时,使用TemplatedParent模式:

 {Binding RelativeSource={RelativeSource Mode=TemplatedParent}}

  绑定到数据项集合中当前项之前的数据项时,使用PreviousData模式。在网络上使用PreviousData绑定的示例较少,因此在这里,我给出一个较为完整的示例:

<Window …
   xmlns:local="clr-namespace:Binding_PreviousData"
    x:Name="MainWindow">
    <Window.Resources>
        <local:HistoryConverter x:Key="historyConverter"/>
        
        <DataTemplate x:Key="historyTemplate">
            <TextBlock>
                <TextBlock.Text>
                    <MultiBinding Converter="{StaticResource historyConverter}">
                        <Binding RelativeSource="{RelativeSource PreviousData}"/>
                        <Binding/>
                    </MultiBinding>
                </TextBlock.Text>
            </TextBlock>
        </DataTemplate>
    </Window.Resources>
    <StackPanel>
        <DockPanel>
            <Button DockPanel.Dock="Right" Content="Submit" Click="OnButtonClicked"/>
            <TextBox x:Name="mInput"/>
        </DockPanel>
        <ListBox ItemTemplate="{StaticResource historyTemplate}" ItemsSource="{Binding ElementName=MainWindow, Path=History}"/>
    </StackPanel>
</Window>

通过Source、ElementName、RelativeSource等属性提供绑定源的方法彼此拥有一定的互补性。一般说,Source用来引用数据层中的数据,ElementName引用的是UI逻辑树中的组成,而RelativeSource则常常用来从视觉树中查找绑定源,甚至可以穿越当前XAML文件根元素。因此在决定绑定源时,软件开发人员需要根据实际查找方式决定需要使用的方式。

  在绑定中,指定了绑定的源并不足够。请考虑指定绑定源的各种方法:ElementName用来引用逻辑树中所给出的元素;RelativeSource则用来指定与当前元素相关联的其它元素;而Source则较为通用,只是其所接受的各种表达式,如StaticResource,常常用来引用具有特定性质的实例。而绑定所操作的,常常是这些源所具有的各个属性,甚至是子属性。软件开发人员需要一种方法指定参与绑定的绑定源的属性。这也便是绑定提供Path属性及XPath属性的原因。

  一般情况下,绑定的Path所指定的各级属性都需要提供属性更改通知的支持,至少软件开发人员应能保证在需要绑定执行时属性更改通知能及时发出。例如在INotifyPropertyChanged接口的实现中,软件开发人员需要显式地发送PropertyChanged事件。而就DependencyProperty而言,其更改通知的发送则由WPF属性系统辅助解决。另外,绑定的Path属性提供了对结构化数据的支持,如对于Point类型的属性Location,软件开发人员可以设置Path为Location.X。这样做的好处在于,提供结构化数据的支持可以拥有更好的语义特征。另一个优点则在于,更新该结构化数据可以避免其内的各个属性在更新时拥有先后顺序,从而使众多依赖于这些数据的绑定在错误状态中执行绑定逻辑。

  另一个常见的疑惑则是有关绑定的更新:如果在绑定中指明路径为X.Y,并且X发出Y更改的消息,那么绑定是否会执行。答案是会。

  如果绑定的源是XML数据而不是CLR对象,那么软件开发人员需要使用XPath属性指定要使用的绑定源,而不是Path。

  在讲解完绑定的数据源后,请读者来看看绑定的目标。WPF规定绑定的目标属性必须是依赖项属性,而不能是字段等其它组成。您可能心中有疑问:为什么绑定的目标属性必须是依赖项属性?原因很简单:除了对绑定的支持之外,依赖项属性的更改还可能导致布局等功能的变化。如TextBlock的Text属性变化可能会导致TextBlock所需要的空间变大。此时属性更改不仅仅要参与绑定功能的执行,更需要以非常高效的方式参与布局系统以及绘制系统等等各WPF子系统。该高效参与各子系统的方式就是依赖项属性。

  由于一般绑定都使用在XAML中,因此对绑定目标的使用也常常是水到渠成的事情:绑定的目标属性常常是DependencyProperty,而被绑定的属性所处于的实例便自然是DependencyObject。

  如果仅仅提供从源属性到目标属性的联动,那么绑定的作用可能并不那么大。考虑这样一个情况:如果软件开发人员希望将源属性绑定到TextBox的Text属性,那么在用户更改TextBox所显示的文字时,源属性将与目标属性不再匹配。这样的例子有很多,而WPF所提供的解决方案就是绑定的Mode属性。其主要分为四种模式:Twoway、Oneway、OnewayToSource以及OneTime。Twoway模式所提供的功能最为强大。一旦绑定源中参与绑定的属性发生变化,或是绑定的目标属性发生变化,那么绑定都将执行。该模式下的绑定不仅仅会将绑定源属性的变化传递到目标属性,更可以在目标属性发生变化时将变化传递到源属性。这种绑定常常使用在绑定目标属性可以从用户界面更改的情况下,如TextBox的Text属性。另一种模式Oneway可以说是最常用的绑定模式。如果绑定源中参与绑定的属性是一个只读属性,或者绑定的目标属性不会由于其它外界因素所更改,那么Oneway绑定是绑定的最佳选择。与Oneway模式类似的是OnewayToSource模式。该模式可以用来绕过绑定对绑定目标属性的限制:绑定要求其目标属性必须是DependencyProperty,而在某些情况下,软件开发人员需要一个不是DependencyProperty的属性根据一个DependencyProperty属性的变化而变化。最后,正如其名称所表现的一样,OneTime模式仅仅运行一次。

  从性能上来讲,OneWay模式绑定的开销较TwoWay模式的开销小。而OneTime则是较OneWay模式更为轻量级的绑定模式。

  与绑定模式相关的一个知识点则是:在创建一个DependencyProperty作为绑定源时,依赖项属性的元数据中的BindsTwoWayByDefault属性用来控制一个依赖项属性在绑定时是否默认为双向绑定。

  接下来要讲解的则是绑定更新通知。绑定提供了两个附加事件SourceUpdated、TargetUpdated。如果需要激活这两个附加事件,软件开发人员需要将相应的属性NotifyOnSource(Target)Updated设置为True。通过该附加事件,软件开发人员可以将事件处理逻辑与其它界面元素进行互动,从而扩展了绑定执行逻辑的灵活性。另外,一个属性的更新常常与其所处于的更新模式相关。软件开发人员可以通过UpdateSourceTrigger属性确定触发源更新的原因,如TextBox的更新条件。

  除此之外,WPF中的绑定还拥有另外一个模式:异步模式。与该模式相关的属性则为IsAsync。在将该属性的值设置为True的情况下,Binding将会从线程池中取出一个线程处理该绑定,以避免在绑定求值时阻塞UI。在属性的访问符没有返回的时候,绑定会暂时使用FallbackValue作为绑定的值。在没有设置FallbackValue的时候,绑定结果将为绑定目标属性的默认值。该绑定模式在源属性值需要较长时间才能获得的情况下非常有用。

  XmlDataProvider.IsAsynchronous以及ObjectDataProvider.IsAsynchronous属性同样提供了该功能。

  另外一个与绑定相关的重要概念就是主从模式:如果在绑定的实例中将属性Selector.IsSynchronizedWithCurrentItem设置为true,那么当前选定项将与之相关的CollectionView的CurrentItem属性同步。那么其它不接受集合类型数据,只接受单一数据的属性直接绑定到该集合数据项属性的时候实际上是绑定到了该关联CollectionView的CurrentItem属性上。

  另一个问题是:当绑定的目标与绑定源中的属性不一致时该怎么处理?实际上,这就是绑定的转换器所提供的功能。在通过Converter属性标示了绑定所使用的转换器后,绑定的每次运行都会调用该转换器,以将源属性的值转换为所需要的目标属性的值。在使用转换器的时候,软件开发人员还可以通过ConverterParameter属性为转换器标明参数,以允许转换器在转换目标属性的过程中使用该参数。

  实现一个绑定的转换器非常简单:软件开发人员只需要实现IValueConverter或IMultiValueConverter即可:

internal class IntToVisibilityConverter : IValueConverter
{
    public Object Convert(object value, Type typeTarget, object param, CultureInfo culture)
    {
        return (int)value == 0 ? Visibility.Collapsed : Visibility.Visible;
    }

    public Object ConvertBack(object value, Type typeTarget, object param, CultureInfo culture)
    {
        throw new NotSupportedException();
    }
}

   在实现一个转换器时,软件开发人员最好使用ValueConversion特性修饰此实现,以向开发工具指示转换所涉及的数据类型。

  既然提到了IMultiValueConverter,那么就不得不提到另一种绑定:MultiBinding。该绑定以多个绑定作为子元素,并在每个子绑定的绑定源发生变化后被执行。每个绑定运行的结果都会通过IMultiValueConverter所实现的转化逻辑将源数据转化为目标属性所需要的值。也正是由于这种一个绑定发生变化时MultiBinding就会被执行的特性,软件开发人员需要在使用MultiBinding时不得不考虑一些特殊情况。一个较为严重的情况则是两个相互关联数据所参与的MultiBinding。在其中一个数据发生改变的时候,MultiBinding将被执行以反映数据的更新。但是此时与该数据相关联的其它数据并非处于正确的状态,从而导致错误的结果,更严重地,程序崩溃。其中一个不适合使用MultiBinding的情况就是求子串。如果使用MultiBinding传入需要操作的字符串以及子串的起始位置,那么在任意一个数据源发生改变的时候另一个数据可能是非法的,如在字符串发生变化的时候,子串的起始位置将可能大于字符串的长度。解决该问题的方法则是为这些相关联的信息提供一个结构化的数据,并以其作为绑定的源属性。每次字符串发生变化的时候,软件开发人员创建一个新的该结构化数据,并使其正确记录字符串以及子串的起始位置。在对该结构化数据进行更新时,其内部所记录的数据将是统一的,从而保证绑定的正确执行。

  另一个需要提及的知识点则是子绑定对MultiBinding类属性的继承。MultiBinding类的Mode属性以及UpdateSourceTrigger属性的值将被其各个子绑定继承,除非其中的子绑定重写该属性。

  MultiBinding当前只支持Binding类型的对象,而不支持MultiBinding或PriorityBinding类型的对象。这是因为Converter本身已经支持对这两种对象的模拟。

  除了MultiBinding外,另一个较为特殊的绑定则是PriorityBinding。其同样接受一组Binding作为子元素,并为这些子绑定依次赋予由高到低的优先级。在运行时,PriorityBinding将返回当前成功执行的具有最高优先级的绑定的值。而在所有Binding都没有成功执行或没有返回的情况下,FallbackValue所标示的值将被使用。一般情况下,该绑定用来处理绑定源属性需要较长时间才能成功返回的情况。其与异步模式绑定具有一定的相似性。但不同的是,首先,异步模式绑定所标示的FallbackValue只能在XAML中标明,如果软件开发人员希望FallbackValue会根据数据层状态而改变,那么他需要选择PriorityBinding,并为该PriorityBinding赋予一个具有最低优先级的绑定。同时具有最低优先级的绑定常常是一个同步绑定,以在PriorityBinding中作为FallbackValue使用。其次,异步模式绑定并不支持多个绑定。在需要执行大量耗时操作,却希望给用户一个粗略计算结果的情况下,在PriorityBinding中使用多个Binding并同时执行这两种计算常常是一个较为合适的解决方案。最后,PriorityBinding所提供的异步特性实际上是通过其各个子绑定的异步特性所提供的。

  综上所述,PriorityBinding的这种运行方式决定了各子绑定的运行方式:除了最后一个子绑定外,其它的各个绑定的IsAsync属性需要被设置为True,以令这些耗时功能的执行以异步方式完成,并在完成后对PriorityBinding的运行结果进行适当更新。而对于最后一个子绑定而言,将IsAsync设置为True则不再是一个强制要求。如果IsAsync属性并没有被设置为True,那么最后一个绑定将作为一个更灵活的FallbackValue;如果IsAsync属性并没有被设置为True,那么所有的绑定都将异步执行,绑定所最初显示的则是由PriorityBinding的FallbackValue所指定的数据。

  另外,对PriorityBinding中的各个异步子绑定提供FallbackValue会影响到PriorityBinding的执行。如果您有兴趣,可以自行试验一下。就个人经验而谈,软件开发人员不应设置异步子绑定的FallbackValue。

  现在,您脑中可能有这样一个疑问:看起来,在提供了合适的转换器的情况下,MultiBinding同样可以达成PriorityBinding的效果。对于这个问题,我想我的答案是肯定的。下面就是我为这个问题所提供的简单实现:

<Window …
    xmlns:local="clr-namespace:SimulatedPriorityBinding">
    <Window.Resources>
        <local:PrioritizedConverter x:Key="prioritizedConverter"/>
    </Window.Resources>
……
    <MultiBinding Converter="{StaticResource prioritizedConverter}">
        <Binding … Path="MoreSlowProperty" IsAsync="True"/>
        <Binding … Path="SlowProperty" IsAsync="True"/>
        <Binding … Path="QuickProperty"/>
    </MultiBinding>
</Window>
public class PrioritizedConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        foreach (object value in values)
            if (value != DependencyProperty.UnsetValue)
                return value;
        return DependencyProperty.UnsetValue;
    }

    public object[] ConvertBack…
}

      其实这种功能的重叠,甚至PriorityBinding只提供MultiBinding所提供功能的子集这一行为并不足以为奇。WPF常常会为一些常用的语法结构提供了简化版本并可能借此提高性能,像TemplateBinding之于使用TemplatedParent模式的Binding。这和C#语言所具有的严格特征略有不同(即C#的前几个版本的设计原则为:一件事情,尽量不提供两种方法去做。举例来说,静态成员函数的调用只能通过类型进行,而C++既可通过类型又可以通过类型实例完成)。

  另一类比较特殊的绑定则是TemplateBinding。其实际上类似于使用了TemplatedParent的Binding,但较Binding所提供的功能更少,也因为更轻量而具有更高的效率。这是因为在TemplateBindingExpression类中,GetValue的实现仅仅是获得特定属性的值,而不是繁琐的计算。由于其固定地绑定到使用该模板的数据实例上,因此该类仅仅提供了Property属性以指定需要绑定到的属性。

三.其它问题

  在绑定中,软件开发人员可以通过Source、ElementName以及RelativeSource等属性标明绑定源。实际上,对这些属性的使用实际上都是完成了对DataContext属性所记录的默认绑定源的重写。DataContext属性用来记录用户界面元素在参与数据绑定时所使用的绑定源。在为一个元素设置了DataContext属性之后,其各个子元素将继承该DataContext值,除非该子元素显式地为DataContext属性赋予了新值。在需要绑定到DataContext上的时候,软件开发人员可以在XAML中直接使用Binding而不再显式地对绑定源进行设置。

  而在XAML中,软件开发人员常常可以通过绑定为DataContext赋值。为了能让该DataContext被更多元素使用,对DataContext的赋值常发生在较高的层次上,如XAML的根元素。

  但是在通过绑定设置了DataContext的情况下,软件开发人员需要注意在何时DataContext才能作为有效值。这是因为绑定的运行并不是在根元素的构造函数调用完成后即已经执行完毕。因此在根元素的InitializeComponent()函数调用完毕以后,DataContext可能并不可用。

  在知道了DataContext的含义之后,我们就可以开始讲解如何调试绑定了。很多人会在编写绑定时不知如何对其进行调试。实际上,绑定的错误常常是绑定源在XAML中标示错误。而就这方面而言,绑定的调试十分简单,只需要您使用一些小技巧。

  在进入该议题之前,本文先来强调一下绑定成功执行的条件:

  1. 指定正确的绑定源。
  2. 转换器能正确地执行源属性的转化。
  3. 经过转换器转换的数值能被赋予目标属性。

  首先,软件开发人员需要在绑定中添加一个转换器。该转换器不做任何事情,只需要它返回符合绑定目标的特定值,如绑定目标为int类型时,它就返回1。该转换器允许软件开发人员在绑定运行过程中设置断点,查看绑定所传入的绑定源。

  无论是使用Source、RelativeSource、ElementName,对这些属性进行指定的XAML语法都较为简单。因此软件开发人员可以通过转换器逐渐调整该绑定源,直至其正确为止。在正确地标示了绑定源之后,软件开发人员可以逐渐修改绑定的Path属性,直到转换器的输入为预定的源属性为止。

  在成功地标示了绑定源之后,软件开发人员需要添加代表实际转换逻辑的转换器。在该转换器中,软件开发人员需要保证所有的返回值都是目标属性所具有的类型。

  另外一种方法则是使用类型System.Diagnostics.PresentationTraceSources类所提供的TraceLevel附加属性。在绑定中标明了该附加属性之后,该绑定在解析和运行时所产生的众多信息都会显示在屏幕上。

  如果在转换器运行过程中发生了异常情况,软件开发人员可以通过Binding.DoNothing以及DependencyProperty.UnsetValue控制绑定的运行。如果绑定的源属性或者转换器返回了Binding.DoNothing,那么绑定引擎将不会执行任何后续操作,如指示绑定引擎不要将值传输到绑定目标、不要移动到PriorityBinding中的下一个Binding,或者不要使用FallBackValue或默认值。即类似取消本次绑定计算的功能。与之对应的是DependencyProperty.UnsetValue。如果绑定执行的三个步骤:绑定源路径解析成功、值转换器能够转换结果并且结果对绑定目标有效都成功执行,那么绑定成功执行。任何一步所返回的DependencyProperty.UnsetValue都表示绑定运行发生了异常且绑定将返回FallbackValue所记录的值。如果没有设置FallbackValue,那么将会使用目标属性的默认值。

  另一个棘手的问题则是绑定的失效。有时候,您会发现在程序开始时还能正常运行的绑定失效了。就个人经验而言,绑定的失效主要分为两种情况:对于One-way绑定而言,如果软件开发人员绕过绑定直接更改了目标属性,那么绑定将会失效。而对于Two-way绑定而言,如果软件开发人员没有通过绑定直接更改了目标属性,而目标属性对源属性的更新由于抛出异常等原因失败,那么绑定也将失效。

  最后一个与DataContext相关的知识点则是DataTemplate对DataContext的更改。DataTemplate会将DataContext更改为使用DataTemplate的数据项。

  接下来要讨论的则是BindingExpression。也许您会奇怪为什么会在提供了Binding类的情况下再提供BindingExpression类。这两个类型之间的不同在于:BindingExpression类用来表示绑定功能的实例,而Binding类则用来记录绑定功能的公有信息。也就是说,每个绑定实例都对应着同一个BindingExpression类,以保持绑定源与绑定目标之间的连接,记录着绑定的运行状态,如Status、HasError以及ValidationError属性。而相同的绑定则共享着同一个Binding类实例。

  和绑定拥有不同的形式一样,BindingExpression类也有相似的组织形式,如PriorityBindingExpression以及MultiBindingExpression。PriorityBindingExpression类的成员属性ActiveBindingExpression可以用来获取当前活动的BindingExpression对象。TemplateBinding同样拥有一个对应的TemplateBindingExpression。

 

四.常用方法

  本节中,本文将讲解一些有关绑定的常用方法。

  首先要讲解的则是延迟绑定。产生该解决方案的原因是为了提高程序的启动性能。请想象下面一种情况:在一个程序的XAML中声明的绑定会在程序启动时加载,并请求绑定源属性的值。对该源属性值的求解将会导致其它功能被加载。试想一下,如果Ribbon所罗列的所有功能都会在程序启动时被加载,那么程序的启动性能将变得非常差。

  这也就是延迟绑定所需要解决的问题。只有在程序界面变为可见时,绑定才会被添加到界面元素中并对其进行求解。

  您可能第一反应是创建一个自定义绑定以解决该问题。的确,BindingBase类提供了虚函数CreateBindingExpressionOverride()以供自定义绑定实现者提供自定义功能。但是本文不采用该方法,其原因有二:该函数所提供的灵活性较差;该函数具有较强的语义特征,并不适用于延迟绑定的实现。

  因此,使LazyBinding派生自MarkupExtension并重写它的ProvideValue()函数可能是一个更好的选择。下面就是实现LazyBinding的代码:

[MarkupExtensionReturnType(typeof(object))]
public class LazyBindingExtension : MarkupExtension
{
    public LazyBindingExtension()
    { }

    public LazyBindingExtension(string path)
    {
        Path = new PropertyPath(path);
    }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        IProvideValueTarget service = serviceProvider.GetService
            (typeof(IProvideValueTarget)) as IProvideValueTarget;
        if (service == null)
            return null;

        mTarget = service.TargetObject as FrameworkElement;
        mProperty = service.TargetProperty as DependencyProperty;
        if (mTarget != null && mProperty != null)
        {
            mTarget.IsVisibleChanged += OnIsVisibleChanged;
            return null;
        }
        else
        {
            Binding binding = CreateBinding();
            return binding.ProvideValue(serviceProvider);
        }
    }

    private void OnIsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
        Binding binding = CreateBinding();
        BindingOperations.SetBinding(mTarget, mProperty, binding);
    }

    private Binding CreateBinding()
    {
        Binding binding = new Binding(Path.Path);
        if (Source != null)
            binding.Source = Source;
        if (RelativeSource != null)
            binding.RelativeSource = RelativeSource;
        if (ElementName != null)
            binding.ElementName = ElementName;
        binding.Converter = Converter;
        binding.ConverterParameter = ConverterParameter;
        return binding;
    }

    #region Fields
    private FrameworkElement mTarget = null;
    private DependencyProperty mProperty = null;
    #endregion

    #region Properties
    public object Source…
    public RelativeSource RelativeSource…
    public string ElementName…
    public PropertyPath Path…
    public IValueConverter Converter…
    public object ConverterParameter…
    #endregion
}

     在这里,本文仅仅探测IsVisibileChanged事件,以在UI元素显示时动态添加绑定。在该类的真正实现中,以何种方式完成延迟功能则是您需要根据需求决定。

  在XAML中,软件开发人员可以像普通绑定一样使用它。但需要注意的一个问题就是MarkupExtension的嵌套使用。如果您按照下面的方法使用LazyBinding:

 <TextBlock Text="{local:LazyBinding ElementName=mMainWindow, Path=Source, Converter={StaticResource testConverter}}"/>

  那么编译器会在编译时报错。从网络上的讨论来看,这是一个Bug,但是无论在VS2008还是VS2010中,其都没有得到修正。如果我是错误的,请通知我。

  作为一个变通的方法,我们可以在程序中通过XML元素的方法完成对LazyBinding的使用:

<TextBlock>
     <TextBlock.Text>
         <local:LazyBinding ElementName="mMainWindow" Path="Source" Converter="{StaticResource testConverter}"/>
     </TextBlock.Text>
 </TextBlock>

  在一个大型程序中,软件开发人员可能需要为绑定编写众多的Converter,而这些Converter中可能存在着部分重复的执行逻辑。为了能够重用这些执行逻辑,软件开发人员可以通过一个Converter将多个子Converter组合起来:

 <local:CompositeConverter x:Key="compositeConverter">
     <local:StringToBooleanConverter/>
     <BooleanToVisibilityConverter/>
</local:CompositeConverter>

 CompositeConverter的实现如下:

[ContentProperty("Converters")]
public class CompositeConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        foreach (IValueConverter converter in Converters)
        {
            value = converter.Convert(value, targetType, parameter, culture);
            if (value == DependencyProperty.UnsetValue 
                || value == Binding.DoNothing)
                break;
        }
        return value;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        for (int index = Converters.Count - 1; index >= 0; index--)
        {
            value = Converters[index].ConvertBack(value, targetType, 
                parameter, culture);
            if (value == DependencyProperty.UnsetValue 
                || value == Binding.DoNothing)
                break;
        }
        return value;
    }

    public List<IValueConverter> Converters
    {
        get { return mConverters; }
    }

    private List<IValueConverter> mConverters = new List<IValueConverter>();
}

 接下来要解决的问题则是使用转换器时的繁琐:在需要使用一个转换器的时候,软件开发人员常常需要在资源中声明转换器实例,并在需要使用该转换器的时候通过StaticResource标记扩展等方法引用它。如果该转换器在不同的文件中使用,那么软件开发人员需要再次在资源中声明转换器,或在公共资源文件中添加该转换器(但注意,公共资源文件将不会被编译为baml,其加载速度较baml慢)。

  相反地,如果能在程序中通过一个标记扩展就能完成对特定转换器的创建及引用,那么软件开发人员所需要做的事情就非常简单了。而下面就是针对该方法给出的解决方案:

public class ConverterFactoryExtension : MarkupExtension
{
    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        Assembly assembly = Assembly.GetExecutingAssembly();
        Type type = assembly.GetType(ConverterName);
        if (type != null)
        {
            ConstructorInfo defCons = GetDefaultConstructor(type);
            if (defCons != null)
                return defCons.Invoke(new object[] {});
        }
        return null;
    }

    private ConstructorInfo GetDefaultConstructor(Type type)
    {
        ConstructorInfo[] infos = type.GetConstructors();
        foreach (ConstructorInfo info in infos)
        {
            ParameterInfo[] paramInfos = info.GetParameters();
            if (paramInfos.Length == 0)
                return info;
        }
        return null;
    }

    public string ConverterName…
}

 在XAML使用该标记扩展的方法为:

<TextBlock Text="{Binding … Converter={local:ConverterFactory ConverterName=Converter_Factory.TestConverter}}"/>

 其实,这也是针对上面所提到的不能使用内嵌标记扩展的解决方案。软件开发人员可以使用这种方法在自定义绑定中提供对转换器的支持。当然,该解决方案还有很多情况需要考虑:没有提供对在其它程序集中定义的转换器类型的支持等等。

 

原文地址:http://www.cnblogs.com/loveis715/archive/2011/12/16/2289641.html

posted on 2017-09-28 10:21  梦琪小生  阅读(9956)  评论(0编辑  收藏  举报

导航