1. 程式人生 > >SilverLight 5 資料繫結的高階話題(1)

SilverLight 5 資料繫結的高階話題(1)

至此,您已經掌握如何將UI控制元件與物件進行繫結以使使用者可以檢視和編輯這些物件所暴露的資料。資料不是被推進控制元件的,而是通過XMAL提供的繫結將資料拉進控制元件裡。換言之,繫結是控制元件讀取資料的過程。在此前幾章裡已經介紹瞭如下資訊:

l 將資料物件分配到控制元件的DataContext屬性;

有三種不同的繫結模式:OneTime,OneWayTowWay(見第二章)。如果需要更新繫結源物件的屬性要設定modeTowWay;

當繫結屬性值發生變化時,可以通過實現InotifyPropertyChanged介面作出通知;

當繫結屬性值(或被繫結物件本身)無效導致異常時也可以通過實現

IDataErrorInfoINotifyDataErrorInfo介面作出通知;

ObservableCollection<T>型別可以用於維護集合中的專案,當專案發生增減時可以自動通知繫結的UI控制元件,從而自動進行更新。另外,也可以在集合類中實現INotifyCollectionChanged介面實現同樣的行為;

可以封裝Collection View集合的檢視,使得集合中的資料可以進行操控(即過濾,排序,分組和分頁)而無需要修改其依賴的集合。Collection View還提供了一個當前記錄的指標用於跟蹤集合中的專案,使得多個控制元件可以繫結到同一Collection View以保持同步;

為了有效開發SilverLight業務應用程式,需要清晰理解資料繫結機制。本章,我們來看看一些高階資料繫結特徵,在此通過一些話題的展開,充分領略SliverLight資料繫結引擎的強大之處。

分配繫結源

在第2章提到資料繫結時,已經指出資料繫結即要有source(下稱源)又要有target(下稱目標),繫結源來自於被繫結控制元件的DataContext屬性。分配給DataContext屬性的物件向下繼承直到物件的每個層次,因此分配給Grid控制元件的DataContext屬性,完全可以應用於該Grid控制元件包含的所有控制元件。

但是,如果你想要一個控制元件的屬性繫結到控制元件的DataContext屬性以外的屬性時,例如資源,或另一個控制元件的某個屬性,那麼該怎樣做呢?下面來看看。

使用繫結的Source屬性

     設定繫結的Source屬性,需要使用物件作為繫結的source而不是將物件分配給控制元件的DataContext屬性。如果在XAML中實現,一般採取使用StaticResource標記擴充套件
繫結到資源這種方式

     假定有一個Products物件被定義為資源,使用productResource作為其key。(“定義和例項化一個類作為資源用於繫結”在本章後面的“繫結到資源”一節中介紹)。繫結到這種資源的標準方法是使用StaticResource標記擴充套件將這種資源分配到目標控制元件的DataContext屬性中。例如,可以將TextBox控制元件Text屬性繫結到Products物件資源 Name屬性,使用如下的XMAL

<TextBox DataContext="{StaticResource productResource}" Text="{Binding Name}"/>

但是,有時如果想要將控制元件的屬性繫結到一個給定的源時,沒有給其子控制元件修改繼承的資料上下文(此處的資料上下文已經在某處設定到更高階的層次結構中)或者沒有為其他屬性修改繫結源。你可以通過使用bindingSource屬性將TextBox控制元件直接繫結到資源而無需使用TextBox控制元件的DataContext屬性繫結到資源的方式。如下的程式碼示例展示了此種繫結方法:

<TextBox Text="{Binding Name, Source={StaticResource productResource}}"/>

注:另外,如果在程式碼中建立資料繫結,可以分配任何物件到此屬性中充當繫結的源。

ElementName繫結

56章,我們看到了如何通過過使用ElementName 繫結將各種控制元件的ItemsSource屬性繫結到DomainDataSource控制元件的Data屬性上,但是尚未對此進行深入探討。DomainDataSource控制元件從伺服器獲取資料並通過其Data屬性公開暴露資料。然後我們就通過將ListBoxDataGridDataForm控制元件的ItemsSource屬性繫結到DomainDataSource控制元件的Data屬性以利用了這些公開暴露的資料,實際上也就是一個控制元件的屬性繫結到另一個控制元件的屬性上。如果要繫結一個控制元件的屬性到另一個在檢視中的控制元件(已命名)的屬性上,我們需要使用特殊的繫結方法稱之為ElemnetName繫結。使用bingding ElementName屬性,需要提供在同一名稱空間裡可以充作繫結源的控制元件的名稱(而不是分配給控制元件的DataContext屬性的物件)。下面一些例子展示了這種繫結。

一個簡單的例子就是將兩個TextBox控制元件的Text屬性時行連線。當修改一個TextBox裡的文字時,第二個TextBox的文字也會自動進行更新:

<StackPanel>

    <TextBox Name="FirstTextBox"/>
    <TextBox Name="SecondTextBox" Text="{Binding Text, ElementName=FirstTextBox}"/>
</StackPanel>

注:如果將第二個TextBox的繫結模式設定為TowWay,修改第二個TextBox也會更新第一個TextBox。第一個TextBox只有在第二個TextBox失去焦點時才會更新。

類似地,可以將TextBlock控制元件的Text屬性繫結到Slider控制元件的Value屬性上,從而可以在TextBlock上顯示Slider控制元件的當前值。

 <Slider Name="sourceSlider"/>

<TextBlock Text="{Binding ElementName=sourceSlider, Path=Value}"/>

下面示例中的XMAL展示了ListBox控制元件的ItemsSource屬性繫結到DomainDataSource控制元件(名為productSummaryDDS)的Data屬性:

<ListBox ItemsSource=”{Binding ElementName=productSummaryDDS,Path=Data}”/>
 

下面這個例子展示了BusyIndicator控制元件的IsBusy屬性繫結到DomainDataSource控制元件(名為)的IsBusy屬性。當DomainDataSource控制元件的IsBusy屬性值為ture時會顯示BusyIndicatior控制元件:

 <controlsTollkit:BusyIndicator IsBusy=”{Binding ElementName=productSummaryDDS, Path=IsBusy}” />

最後一個例子,將Lable控制元件的Target屬性繫結到TextBox上去。注意,沒有指定繫結的路徑,而是將Target屬性繫結到了TextBox自身:

<sdk:Label Content=”Name:” Target=”{Binding EmementName=ProductNameTextBox}” /> 

RelativeSource繫結

在繫結的標記擴充套件中有一個RelativeSource屬性,使得我們可以繫結到相對於目標的源上。前面介紹了的ElementName繫結是將一個控制元件的屬性繫結到另一個控制元件的屬性上。但是,只有當源控制元件進行了命名時ElementName繫結才可以使用。而RelativeSource則可以繫結到相對於目標控制元件的未命名源控制元件上。

通過使用RelativeSource標記擴充套件可以取得一個對源控制元件的引用,返回的值可以分配給繫結的RelativeSource屬性。RelativeSource標記擴充套件有三種模式:SelfTemplateParentFindAncestor。下面依次看看。

Self 模式

Slef模式返回目標控制元件本身,用於繫結同一控制元件的兩個屬性。如果想要將控制元件自身的屬性與控制元件的附加屬性進行繫結時,這一模式就能發揮作用。例如,如果想要讓使用者在TextBox上停靠顯示相關工具提示時,需要在提示中顯示文字框內的所有文字。在這裡,我們需要使用ToolTipService.ToopTip附加屬性(見第2章)然後將其繫結到TextBoxText屬性,就需要使用RelativeSource標記擴充套件的Self模式:

<TextBox Text=”{Binding Path=CurrentItem}” ToolTipService.ToolTIp=”{Binding Text, RelativeSource={RelativeSource Self}}” /> 

TemplatedParent 模式

TemplatedParent模式僅應用於控制元件內包含控制元件模板或資料模板的控制元件。這種模式返回模板專案物件並且可以使模板專案的有關屬性繫結到目標屬性上。當在控制元件內使用資料模板時,例如在ListBox控制元件的Item資料模板,TemplatedParent模式就會返回相應模板項的顯示內容。注意這一模式並不會返回ListBoxItem控制元件;資料模板實際上是應用於專案的表現器(Content Presenter),因此返回的是內容表現器;如果控制元件模板是針對ListBoxITem構建的,則TemplatedParent模式就可以返回LIstBoxItem本身。

例如,如下的資料模板繫結表示式將會獲取內容表現器的實際高度:

"{Binding RelativeSource={RelativeSource TemplatedParent}, Path=ActualHeight}" 

這種繫結的另一個有用場景是在需要獲得模板項的資料上下文的情形。控制元件本身資料上下文當然可以向下覆蓋到資料模板層,但是如果在控制元件中的資料模板分配一個不同的資料上下文,如Grid,你應該使用這種方法再次獲取和繫結原始資料上下文到專案中。

 注:如果使用控制元件模板,繫結表示式:“{Binding RelativeSource={RelativeSource TemplatedParnet}}”等價於“{TemplateBinding}”標記擴充套件。與TemplateBinding標記擴充套件只支援單向繫結不同,RelativeSource標記擴充套件可以用於實現雙向繫結,這在很多場景下都很有用,特別是在建立定製控制元件的控制元件模板時。在12章我們會深入討論。

FindAncestors 模式

SilverLight 5 引入了一種新的RelativeSource 繫結模式—FindAncestor模式。這種模式允許你在XMAL層級結構中向上查詢給定型別的控制元件。假設有如下的XMAL

複製程式碼 <Grid Background="Green" Width="200" Height="200"> 

    <Grid Background="Red" Margin="20">
        <Grid Background="Blue" Margin="20">
              <Grid Margin="20"/>
        </Grid>
    </Grid>
</Grid>

複製程式碼

正如所見,這個XMAL程式碼中包含了四個巢狀的Grid控制元件。如果現在想要將最裡面的Grid控制元件的背景顏色設定為與上面各層級Grid控制元件之一的相同,就可以使用RelativeSource繫結的FindAncesotr模式來進行。RelativeSource標記擴充套件有兩個屬性:AncestorsTypeAncestorLevel。前者用於指定供搜尋的控制元件型別,後者用於指定在控制元件被選定前有多少次指定控制元件型別應該在控制元件層級中暴露。如下XMAL展示了BackGround屬性的繫結(最內側的Grid控制元件與最外側的Grid控制元件相應屬性進行繫結),一共跳過兩個Grid例項,因此共有3次暴露:

複製程式碼 <Grid Background="Green" Width="200" Height="200">
    <Grid Background="Red" Margin="20">
        <Grid Background="Blue" Margin="20">
            <Grid Background="{Binding Background, 
                                       RelativeSource={RelativeSource FindAncestor,
                                                                      AncestorType=Grid,
                                                                      AncestorLevel=3}}
" 
                  Margin
="20"/>
        </Grid>
    </Grid>
</Grid> 複製程式碼

如果將AncestorLever屬性調整為12,會看到最內側Grid控制元件的背景會與相應Grid控制元件的屬性進行變化。

注:RelativeSourceFindAncestors模式還有很多潛在用途。比如,如果想要分配一個ViewModel物件到檢視的DataContext屬性(即在PageUserControl控制元件),一個層級低於控制元件層次結構的控制元件其DataContext沒有設定到ViewModel物件如果控制元件包含在ListBox項裡,仍需要繫結到ViewModel物件 的一個屬性上。RelativeSource繫結的FindAncestor模式就能夠很容易獲得對PageUserControl或其他頂級控制元件的引用,可以直接獲得ViewModel物件的DataContext屬性的引用。另一個應用是能夠使用ListBox內部的控制元件專案資料模板可以獲得ListBoxtItem控制元件的引用(因為包含在其中)。這就使得控制可以訪問ListBoxItemIsSelected屬性,使其可以根據ListBox專案是否被選定而改變其狀態。 

直接繫結控制元件屬性到其Data Context

你可以直接將控制元件的屬性與分配給其的DataContext屬性物件進行繫結,可以直接分配到控制元件的屬性上,也可以分配到向下的物件層級裡,只需要簡單設定其值到“{Binding}”。這一場景適用於繫結大量控制元件到同一資料上下文的情況。例如,一個Grid 控制元件的資料上下文繫結到了一個集合,在該Grid內的多個控制元件都可以繼承這個資料上下文將其作為繫結源。因此,為了將Grid控制元件內的ListBox控制元件的ItemsSource屬性繫結到這個集合,可以直接設定繫結表示式為“{Binding}”。

作為示例,如下XAMLTextBox控制元件的Text屬性被繫結到TextBox控制元件的DataContext屬性,結果在TextBox中的Text就會顯示“Hello”字樣:

<TextBox DataContext="Hello" Text="{Binding}"/>

注:“{Binding}”繫結表示式等價於{Binding Path=.},也等價於{Binding Path=} 

檢測DataContext的值何時變更

假如有一個檢視用於處理由ViewModel物件(分配給檢視的DataContext屬性)引發的事件,如果一個新的ViewModel物件被分配到ViewDataContext屬性上,檢視需要取消訂閱前面的事件並處理新ViewModel物件的事件。在這種場景裡,檢視需要知道DataContext屬性是否發生變化以便作出相應的處理。

在早期版本的SilverLight裡,沒有方便的方法來確定控制元件DataContext值是否發生了變化 ,使得這種場景處理起來相當棘手。而SilverLight5 引入了DataContextChanged事件,只有當DataContext屬性變更時才會觸發。你可以處理這種事件並作出相應響應。

現在來看一個例子。如下的XMAL包含了一個Button控制元件和一個TextBox控制元件 TextBox控制元件 Text屬性繫結到其DataContext,而TextBox控制元件的DataContextChaged事件在後置程式碼中進行處理。Button控制元件的Click事件也在後置程式碼進行處理,因為我們將使用這一事件來改變 TextBox控制元件的DataContext屬性:

<StackPanel>
    <Button Name="ChangeContextButton" Content="Change Context" 
            Height
="33" Width="143" Click="ChangeContextButton_Click"/>
    <TextBox Name="MyTextBox" Text="{Binding}"
             DataContextChanged
="MyTextBox_DataContextChanged"/>
</StackPanel>

       在後置程式碼裡,我們現在需要處理Button控制元件的Click事件和TextBox控制元件的DataContextChanged事件。在Button控制元件的Click事件處理方法中,將TextBox控制元件的DataContext屬性值進行變更。如下程式碼為其分配了一個新的GUID

private void ChangeContextButton_Click(object sender, RoutedEventArgs e)
{
    MyTextBox.DataContext = Guid.NewGuid();
}

TextBox控制元件的DataContextChanged事件處理程式碼中,我們簡單地顯示了一個資訊框指出事件已經引發:

private void MyTextBox_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
    MessageBox.Show("My data context has changed!");
}

執行程式碼,你會發現每次點選按鈕都會彈出一個資訊框。作為練習,分配GUID值到檢視的DataContext屬性而不是TextBox控制元件的:

private void ChangeContextButton_Click(object sender, RoutedEventArgs e)
{
    this.DataContext = Guid.NewGuid();
}

TextBox控制元件會繼承這一資料上下文,這樣在檢視的資料上下文屬性發生變化TextBoxDatContext屬性也會發生變化。因此,DataContextChanged事件仍然會引發。

View的後置程式碼中繫結到屬性

前面介紹了屬性可以繫結到物件,資源或檢視中的其他控制元件,但是如果你要繫結的屬性源實際上是檢視的後置程式碼類怎麼辦?見如下的XAML

複製程式碼 <UserControl x:Class="Chapter11Workshop.MainPage"
    xmlns
="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x
="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d
="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc
="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable
="d"
    d:DesignHeight
="300" d:DesignWidth="400">
    
    <TextBlock Width="100" Height="20"/>
</UserControl> 複製程式碼

檢視的後置程式碼定義了一個屬性名為UserName,如下:

複製程式碼 using System.Windows.Controls;
namespace Chapter11Workshop
{
    public partial class MainPage : UserControl
    {
        public MainPage()
        {
            InitializeComponent();
        }
 
        public string UserName
        {
            get { return "Chris Anderson"; }
        }
    }

複製程式碼

TextBlock控制元件可以使用RelativeSource繫結的FindAncestor模式來找到檢視的根元素並將其作為繫結源,如下所示:

<TextBlock Width="100" Height="20" Text="{Binding UserName, RelativeSource={RelativeSource FindAncestor, AncestorType=UserControl}}"/> 

另外,也可以使用ElementName繫結獲得同樣的效果。這必須給XAML檔案的根元素用Name屬性進行命名。此例中將其命名為Root。這樣就可以使用ElementName綁定了:

<TextBlock Width=”100” Height=”20” Text=”{Binding UserName , ElementName=Root}” />

注:通常,任何檢視需要繫結的屬性都應在ViewModle類中進行定義。(為MVVM設計模式的一部分)。屬性通常在後置程式碼中定義;模型提供資料;XAML可以直接通過繫結獲取這些資料而無需後置程式碼的互動(除了屬性值的getset訪問器的設定以外)。對於MVVM的詳細分析見第12章的討論。 

XAML中例項化類

可以在XAML中例項化類,將其定義為資源或者作為控制元件屬性的值。下面來看看。

建立一個可繫結的類

我們先在XAML中建立類的例項。在你的專案中建立一個ViewModels資料夾。增加一個新的類,命名為ProductViewModel。新增一些屬性(Name,ProductNumber等) 以便我們進行繫結。在類的構造器中為這些屬性設定一些預設值。

複製程式碼 public class ProductViewModel
{
    public string Name { getset; }
    public string ProductNumber { getset; } 
 
    public ProductViewModel()
{
Name = "Helmet";
        ProductNumber = "H01";
    }
}
  複製程式碼

注:為了在XMAL中例項化類,必須有一個預設構造器(無參構造器)。如果尚未定義任何構造器,將會為你自動建立預設構造器,否則必須向類中明確新增。 

XAML中例項化類並作為資源然後進行繫結

在XAM例項化類的第一步是需要 宣告 一個名稱 空間在XAML檔案中:

   Xmlns:vm=”clr-namespace:AdventureWorks.ViewMOdels”

下一步是定義 類的例項作為資源,記住要給出一個key:

<UserControl.Resources>
    <vm:ProductViewModel x:Key="productResource"/>
</UserControl.Resources>

甚至可以分配一個值給類中的屬性(這將會覆蓋類構造器的預設值):

<vm:ProductViewModel x:Key="productResource" Name="Bike" ProductNumber="B001"/> 

注:在XAML中只能給少數幾種資料型別賦值,如string,Boolean,IntDouble。其他複雜型別如DecimalDateTime等,需要型別轉換以便能夠適應待處理的語句。建立和使用型別轉換器見第12章。

類被例項化並定義為資源後,就可以作為物件資源繫結到控制元件上了上了。方法是使用StaticResource標記擴充套件:

<TextBox Text="{Binding Name, Source={StaticResource productResource}}"/>

注:任何在類中硬編碼的資料或作為資源的屬性都會在設計時顯示在被繫結的控制元件上。

例項化類並作為控制元件屬性的值

可以直接在XAML中例項化類並將物件分配到控制元件的屬性上去,無需定義物件為資源。例如,可能想要分配一個ProductViewModel類的例項到檢視的DataContext屬性上。就可以採用如下程式碼實現:

this.DataContext = new ProductViewModel();

XAML中,採用如下元素語法操作:

<UserControl.DataContext>
    <vm:ProductViewModel />
</UserControl.DataContext>

這是宣告viewViewModel的連線方式比較好的選擇,在使用MVVM設計模式時更是這樣。

在後置程式碼中定義資源

現在你已經看到可以在XMAL中定義資源,但有時使用程式碼定義資源也是有用的。例如,可以有一個factory類需要進行例項化,但是該類沒有無參建構函式(這是XAML例項化類所必須的)。在這種情況下就需要在程式碼中定義資源。

在本書貫穿始終的SilverLight業務應用程式專案:AdventureWoks就有這樣的例子。如果開啟AdventureWorks專案的App.xaml.cs檔案就會發現Application_Startup事件處理函式,並可以找到如下程式碼:

This.Resources.Add(“WebContext”,WebContext.Current);

這行程式碼添加了一個WebContext物件作為應用程式範圍內的資源。如你所見,其簡單地獲取了一個對資源字典的引用(在本例中是Application物件資源字典)並呼叫Add方法指定了資源keyWebContext)和一個作為資源的物件(WebContext.Current)。可以在XAML中像其他資源一樣進行繫結和使用。

<Buttton Content=”Click Me!”
           IsEnabled
=”{Binding Path=User.IsAuthenticated,
Source={StaticResource WebContext} }” />
 

繫結到巢狀屬性

    通常在建立繫結時,需要繫結到源物件的單個屬性。例如TextBox控制元件的DataContext屬性可能被分配了一個Personal物件,而你可能想要繫結該屬性的FirstName屬性,因為FirstName屬性返回了一個字串:

<TextBox Text=”{Binding FirstName , Source={StaticResource personResource}}” />

但是,如果Persona物件並沒有FirstName屬性而只有Name屬性,該屬性返回PersonName物件,而該物件有FirstName屬性,如何處理這種巢狀屬性呢?這可以使用與C#程式碼相同風格的點式語法訪問下級物件:

<TextBox Text=”{Binding Name.FirstName, Source={StaticResource personResource} }” />