【轉】【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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM