程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> 《Programming WPF》翻譯 第4章 3.綁定到數據列表

《Programming WPF》翻譯 第4章 3.綁定到數據列表

編輯:關於.NET

目前為止,你已經看到一些示例將控件綁定到一個單獨的對象。然而,更復 雜的使用是綁定到一個對象列表。例如,想象一下,我們的對象數據源可以創建 一個新類型表示Person對象的列表,正如示例4-19:

示例4-19

using System.Collections.Generic; // List<T>

namespace PersonBinding {
  // XAML doesn't (yet) have a syntax
  // for generic class instantiation
  class People : List<Person> {}
}

我們可以掛起這個新的數據源列表,按照同樣的方式綁定到它,就像綁定到 一個單獨的對象數據源上,如示例4-20。

示例4-20

<!-- Window1.xaml -->
<?Mapping XmlNamespace="local" ClrNamespace="PersonBinding" ? >
<Window  xmlns:local="local">
  <Window.Resources>
    <local:People x:Key="Family">
      <local:Person Name="Tom" Age="9" />
      <local:Person Name="John" Age="11" />
      <local:Person Name="Melissa" Age="36" />
    </local:People>
    <local:AgeToForegroundConverter
      x:Key="AgeToForegroundConverter" />
  </Window.Resources>
  <Grid DataContext="{StaticResource Family}">

    <TextBlock >Name:</TextBlock>
    <TextBox Text="{Binding Path=Name}"  />
    <TextBox
      Text="{Binding Path=Age}"
      Foreground="{Binding Path=Age, Converter=}"  />
    <Button >Birthday</Button>
  </Grid>
</Window>

在示例4-20中,我們創建了一個People集合的示例而且通過三個Person對象 導入它。然而,運行它將會如圖4-6。

4.3.1當前項

盡管文本框屬性每次僅能被綁定到一個單獨的對象上,在可能的被綁定到的 對象列表中,綁定引擎提供了一個名為當前項的概念,正如圖4-6所解釋的。

缺省地,列表的第一項作為當前項的開始。由於我們列表示例的第一項與我 們之前綁定的單獨對象一樣,所以看起來和圖4-11顯示的一樣——Birthday按鈕 除外。

圖4-11

4.3.1.1獲取當前項

回想當前Birthday按鈕的click事件句柄(示例4-21)。

示例4-21

public partial class Window1 : Window {

  void birthdayButton_Click(object sender, RoutedEventArgs e) {
    Person person = (Person)this.FindResource("Tom"));
    ++person.Age;
    MessageBox.Show();
  }
}

我們的Birthday按鈕應該總是產生向當前人士祝賀生日的效果,但是到目前 為止,當前人士卻總是一樣的,因此我們只能簡化事情為直接到達單獨的Person 對象。既然我們已經得到了對象的列表,這個機制就不再使用了(除非你認為一 個包含單詞“InvalidCastException”消息框是可以接受的方式)。進一步而言 ,轉換到People,我們的集合類,不會告訴我們那一個Person對象會在當前UI中 顯示,因為它不知道這些事情(也不需要知道)。由於這一點,我們將要必須建 立“經紀人”在數據綁定的控件和集合項上,這個“經紀人”在這裡被稱為視圖 。

視圖的工作是在數據之上提供服務,包括排序,過濾,以及此刻對於我們的 意圖來說最重要的:控制當前項。視圖是詳細數據的接口實現,在我們這種情形 ,就是ICollectionView接口。我們可以通過BindingOperations類的靜態 GetDefaultView方法訪問這個數據上的視圖,正如示例4-22所示:

示例4-22

public partial class Window1 : Window {

  void birthdayButton_Click(object sender, RoutedEventArgs e) {
    People people = (People)this.FindResource("Family");
    ICollectionView view =
      BindingOperations.GetDefaultView(people);
    Person person = (Person)view.CurrentItem;

    ++person.Age;
    MessageBox.Show();
  }
}

為了取回聯合了Family集合的視圖,示例4-22對BindingOperations的 GetDefaultView方法進行了一次調用,提供了一個ICollectionView接口的實現 。基於此,我們可以得到當前項,將它從集合中的一項轉換為我們需要的對象( CurrentItem屬性返回一個object對象),以及用它來顯示。

4.3.1.2在數據項中導航

出了獲取當前項外,我們也能改變當前項的位置,通過ICollectionView接口 的MoveCurrentToXX方法,正如示例4-23所示。

示例4-23

public partial class Window1 : Window {

  ICollectionView GetFamilyView(  ) {
    People people = (People)this.FindResource("Family");
    return BindingOperations.GetDefaultView(people);
  }

  void birthdayButton_Click(object sender, RoutedEventArgs e) {
    ICollectionView view = GetFamilyView(  );
    Person person = (Person)view.CurrentItem;

    ++person.Age;
    MessageBox.Show();
  }

  void backButton_Click(object sender, RoutedEventArgs e) {
    ICollectionView view = GetFamilyView(  );
    view.MoveCurrentToPrevious(  );
    if( view.IsCurrentBeforeFirst ) {
      view.MoveCurrentToFirst(  );
    }
  }

  void forwardButton_Click(object sender, RoutedEventArgs e) {
    ICollectionView view = GetFamilyView(  );
    view.MoveCurrentToNext(  );
    if( view.IsCurrentAfterLast ) {
      view.MoveCurrentToLast(  );
    }
  }
}

ICollectionView接口的MoveCurrentToPrevious方法和MoveCurrentToNext方 法,通過在集合中向後和向前的動作改變當前的選中項。如果我們沿著一個方向 移動到列表的盡頭或另一個盡頭,IsCurrentBeforeFirst或IsCurrentAfterLast 屬性將會告訴我們這一點。MoveCurrentToFirst和MoveCurrentToLast方法幫助 我們復原在到達列表的盡頭之後,對於在途4-12中實現Back和Forward按鈕,這 將是很有用的。同樣適用於First和Last兩個按鈕(這將是你的一個機會,將學 到的運用上去)。

圖4-12顯示了從集合中第一個Person元素開始,向前移動的效果,包括基於 Person對象的Age屬性導致的顏色改變(這仍然以同樣的方式工作)。

圖4-12

4.3.2數據列表目標

當然,目前為止,我們僅能做的是把用戶列表數據推出來,而沒有為這些數 據提供一個控件可以准確的一次性顯示多條數據,正如示例4-24中的ListBox控 件。

示例4-24

<!-- Window1.xaml -->
<?Mapping XmlNamespace="local" ClrNamespace="PersonBinding" ? >
<Window  xmlns:local="local">
  <Window.Resources>
    <local:People x:Key="Family"></local:People>
    <local:AgeToForegroundConverter
      x:Key="AgeToForegroundConverter" />
  </Window.Resources>
  <Grid DataContext="{StaticResource Family}">

    <ListBox
      ItemsSource="{Binding}"
      IsSynchronizedWithCurrentItem="True"  />
    <TextBlock >Name:</TextBlock>
    <TextBox Text="{Binding Path=Name}"  />

</Window>

在示例4-24中,ListBox的ItemSource屬性沒有綁定到路徑,等於是說:綁定 到當前整個對象。注意到,這裡也沒有源,因此綁定會從找到的第一個非空的數 據上下文開始工作。在這種情形中,第一個非空的數據上下文來自Grid,就是那 個在name和age的文本框中共享的上下文。我們還設置了 IsSynchronizedWithCurrentItem屬性為true,以確保listbox中的選中項也能發 生改變——這會在視圖中更新當前項;反之亦然。

圖4-13

正如你可能看到的,圖4-13中的每件事物都很完美。所發生的是,當你綁定 一個完整對象時,數據綁定盡其所能顯示每一個Person對象。無需特殊的指令, 它會使用一個類型轉換器來得到一個字符串表示。對於name和age,都是內嵌類 型,具有內嵌轉換,這將工作良好;但是也有不能很好工作的時候,對於一個不 具備可視化生成的自定義類型,正如Person類型這種情形。

4.3.3數據模板

正確解決這個問題的做法是使用數據模板。數據模板是一棵元素樹,可以在 特定的上下文擴展。例如,對於每一個Person對象,我們希望能夠像以下方式將 name和age連接在一起:

Tom(age: 9)

我們可以把它想象成一個合乎邏輯的模板,如下:

    Name(age: Age)

為了在listbox中為數據項定義模板,我們創建了一個DataElement元素,正 如示例4-25。

示例4-25

<ListBox  ItemsSource="{Binding}">
  <ListBox.ItemTemplate>
    <DataTemplate>
      <StackPanel Orientation="Horizontal">
        <TextBlock TextContent="{Binding Path=Name}" />
        <TextBlock TextContent=" (age: " />
        <TextBlock
          TextContent="{Binding Path=Age}"
          Foreground="
            {Binding
              Path=Age,
              Converter={StaticResource AgeToForegroundConverter}}" />
        <TextBlock TextContent=")" />
      </StackPanel>
    </DataTemplate>
  </ListBox.ItemTemplate>
</ListBox>

在這種情形中, ListBox控件有一個ItemTemplate屬性,它接受一個 DataTemplate對象示例。DataTemplate允許我們詳細指出一個單獨的子元素,用 於綁定重復顯示在ListBox控件的每一個數據項。在我們的例子中,使用了 StackPanel將四個TextBlock控件放在一行中:2個綁定到每個Person對象的屬性 ,兩個是常量文本。注意到,我們使用AgeToForegroundConverter已經將 Foreground綁定到Age屬性,為了Age屬性顯示為黑色或紅色,為了列表框和age 文本框是一致的。

通過使用數據模板,我們經歷了從圖4-13到圖4-14。

圖4-14

注意到,列表框顯示了集合中所有的條目,而且保持了視圖同步於當前條目 ,當選擇向前或向後的按鈕按下時(實際上,你並不會從圖4-14的部分截圖真正 “注意到”,但是相信我,確實是發生了)。此外,當Person對象的數據改變的 時候,列表框以及文本框會保持同步,還包括Age的顏色。

4.3.1類型化數據模板

在示例4-25中,我們顯示地為ListBox列表設置了數據模板。然而,如果一個 Person對象顯示在一個按鈕或是其它什麼元素中,我們最好分別詳細指出那些 Person對象的數據模板。另一方面,如果你想要Person對象有一個特殊的模板而 不論其顯示在哪裡,你可以通過類型化的數據模板來實現。

示例4-26

<Window.Resources>
  <local:AgeToForegroundConverter
    x:Key="AgeToForegroundConverter" />
  <local:People x:Key="Family"></local:People>
  <DataTemplate DataType="{x:Type local:Person}">
    <StackPanel Orientation="Horizontal">
      <TextBlock TextContent="{Binding Path=Name}" />
      <TextBlock TextContent=" (age: " />
      <TextBlock TextContent="{Binding Path=Age}"  />
      <TextBlock TextContent=")" />
    </StackPanel>
  </DataTemplate>
</Window.Resources>

<!-- no need for an ItemTemplate setting -->
<ListBox ItemsSource="{Binding}" >

在示例4-26中,我們將數據模板的定義提升到資源模塊,並且使用標簽的 DataType屬性標志這個數據模板是類型化的。現在,除非另外通知,每當WPF看 到Person對象的一個實例,就會應用相應的數據模板。這是一條便利之路,保證 數據以一致的方式顯示,遍及於你的應用程序,而不用擔心顯示的位置。

4.3.4列表的改變

迄今,我們已經得到一個對象的列表,我們可以適當的進行編輯,以及在其 中建立導航,甚至輕而易舉地高亮顯示某些數據,以及提供了一個自動搜索,表 現那些沒有裝載的來自廠商的數據。考慮到我們已經到達的程度,你可能懷疑提 供一個Add按鈕是一件輕而易舉的事情,正如示例4-27所示。

示例4-27

public partial class Window1 : Window {

  void addButton_Click(object sender, RoutedEventArgs e) {
    People people = (People)this.FindResource("Family");
    people.Add(new Person("Chris", 35));
  }
}

這個實現的問題在於,盡管視圖可以判斷出新條目的存在當你移動到這裡的 時候,而列表框本身卻並不知道新增加的集合中的條目,正如圖4-15。

圖4-15

為了與圖4-15顯示的應用程序狀態交互,我運行了這個程序,點擊了Add按鈕 並使用Forward按鈕導航到圖中所示。然而,即使新人顯示在文本框中,列表框 仍然不知道添加了什麼事物。同樣地,如果有對象被刪除,它也不會知道。就像 數據綁定需要事先INotifyPropertyChanged接口,使用數據綁定的列表需要實現 INotifyPropertyChanged這個接口,正如示例4-28。

示例4-28

namespace System.Collections.Specialized {
  public interface INotifyCollectionChanged {
    event NotifyCollectionChangedEventHandler CollectionChanged;
  }
}

INotifyCollectionChanged接口用於通知數據綁定控件,有條目在綁定列表 中添加或刪除。盡管在你的自定義類型中實現INotifyPropertyChanged,從而支 持兩種方式的數據綁定在你的類型化屬性上——這很普通;不普通的是實現你自 己的集合類,這些類給你很少的機會實現INotifyCollectionChanged接口。取代 之,你更加更能依賴於集合類的一項在.NET 框架類庫中,用來實現 INotifyCollectionChanged。這樣的類數量很少,而且不幸的是,我們使用的保 持著Person對象的集合類,並不在其中。當你受歡迎的度過你的夜晚和周末實現 了INotifyPropertyChanged,WPF提供了ObservableCollection<T>類,用 於我們那些緊迫的職責,如示例4-29所示。

示例4-29

namespace System.Windows.Data {
  public class ObservableCollection<T> :
    Collection<T>, INotifyCollectionChanged, INotifyPropertyChanged {

  }
}

既然ObservableCollection<T>派生於Collection<T>,而且實 現了INotifyCollectionChanged接口,我們可以使用它代替List<T>作為 我們的Person集合,正如示例4-30。

示例4-30

namespace PersonBinding {
  class Person : INotifyPropertyChanged {}
  class People : ObservableCollection<Person> {}
}

現在,當一個條目添加到或刪除自Person集合,這些變化將要在數據綁定列 表中反映出來,正如圖4-6所示。

圖4-16

4.3.5排序

一旦我們適當地使數據目標每次顯示多於一個事物,一個年輕人的愛好變得 更多,當然,是喜歡的事物,正如對數據視圖排序或者過濾。回憶視圖經常位於 數據綁定目標和數據源之間。這意味著可以越過我們不要顯示的數據(被稱為過 濾,而且可以被直接覆蓋),而且可以改變數據顯示的順序,又名排序。最簡單 的排序方法是通過操作視圖的Sort屬性,正如示例4-31所示。

示例4-31

public partial class Window1 : Window {

  ICollectionView GetFamilyView(  ) {
    People people = (People)this.FindResource("Family");
    return BindingOperations.GetDefaultView(people);
  }

  void sortButton_Click(object sender, RoutedEventArgs e) {
    ICollectionView view = GetFamilyView(  );
    if( view.Sort.Count == 0 ) {
      view.Sort.Add(
        new SortDescription("Name", ListSortDirection.Ascending));
      view.Sort.Add(
        new SortDescription("Age", ListSortDirection.Descending));
    }
    else {
      view.Sort.Clear(  );
    }
  }
}

這裡我們通過檢測SortDescriptionCollection暴露在外的 ICollectionView.Sort屬性,將排序視圖和未排序視圖拴在一起。如果沒有排序 方式的描述,我們首先對Name屬性按上升方式排序,然後對Age屬性按下降方式 排序。如果有排序方式的描述,我們將其清除,重新排序——無論之前是如何排 序的。雖然排序描述在適當的位置,任意新添加到集合中的對象將被添加到它們 已經排好序的適當位置,正如4-17所示。

一個SortDescription對象集合應該覆蓋大多數的情形,但是如果你需要更多 一點的控件,你可以提供自定義排序對象的視圖,通過實現IComparer接口,正 如示例4-32。

圖4-17

示例4- 32

class PersonSorter : IComparer {
public int Compare(object x, object y) {
Person lhs = (Person)x;
Person rhs = (Person)y;
// Sort Name ascending and Age descending
int nameCompare = lhs.Name.CompareTo(rhs.Name);
if( nameCompare != 0 ) return nameCompare;
return rhs.Age - lhs.Age;
}
}

public partial class Window1 : Window {
ICollectionView GetFamilyView( ) {
People people = (People)this.FindResource("Family");
return BindingOperations.GetDefaultView(people);
}
void sortButton_Click(object sender, RoutedEventArgs e) {
ListCollectionView view = (ListCollectionView)GetFamilyView( );
if( view.CustomSort == null ) {
view.CustomSort = new PersonSorter( );
}
else {
view.CustomSort = null;
}
}
}

在設置了自定義排序的情況,我們必須做一個假設——詳細明確地實現了 ICollectionView,這裡使用的是ListCollectionView,是WPF包裝在IList的實 現(由ObserverableCollection提供),來提供視圖的功能性。此外還有其它沒 有提供自定義排序的ICollectionView接口實現,因此你要在想*使用這段代碼前 先測試一下。

希望你在使用前也測試一下其它代碼,但是指出這些事情並沒有什麼危害。

盡管我肯定,當我們使用WPF1.0時,這將變得更好。從現在開始,視圖實現 了聯合詳細數據特征,正如在ListCollectionView和IList間進行匹配並沒有文 本化(至少現在我這麼說)。這看起來有點有趣,CustomSort是視圖實現類的一 部分,並不是ICollectionView接口的一部分,因此讓我們為之祈禱:Microsoft 發布新的WPF版本改變這一點。

4.3.6過濾

正因為所有的對象按順序顯示使你快樂,這並不意味著你想要顯示所有的對 象。對於這些沒用的出現在數據中的對象,卻不屬於這個視圖,我們需要提供這 個實現了CollectionFilterCallback委托*的視圖,需要一個單獨的對象作為參 數並返回一個Boolean值表明這個對象是否應該被顯示,正如示例4-33。

排序使用一個單方法的接口實現,是由於歷史原因;而過濾使用一個委托, 是因為在C#2.0中另外使用匿名委托機制,這是一個很流行的機制。

示例4-33

public partial class Window1 : Window {
...
ICollectionView GetFamilyView( ) {
People people = (People)this.FindResource("Family");
return BindingOperations.GetDefaultView(people);
}
void filterButton_Click(object sender, RoutedEventArgs e) {
ICollectionView view = GetFamilyView( );
if( view.Filter == null ) {
view.Filter = delegate(object item) {
return ((Person)item).Age >= 18;
};
}
else {
view.Filter = null;
}
}
}

正如排序,通過使用一個恰當的過濾器,新條目被適當的過濾掉了,正如圖 4-18所示。

圖4-18

圖4-18中最 上面的窗體顯示了沒有過濾器,中間的窗體顯示了過濾了初始的列表,底部的窗 體顯示了添加一個成年人,過濾器仍然在恰當的位置。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved