21 WPF數據視圖

視圖對象

當你綁定集合到ItemsControl,在幕後數據視圖被安靜地創造。視圖位於數據源和綁定控件之間。數據視圖是通往數據源的一個窗口。它跟蹤當前項目,它支持諸如排序,過濾,和分組特徵。這些特徵獨立於數據對象自己,意味着你能以不一樣的方式、在窗口的不一樣部分(或應用的不一樣部分)綁定相同的數據。例如,你能綁定相同的產品集合到兩個不一樣的列表可是過濾他們顯示不一樣的記錄。html

視圖對象依賴於數據對象的類型。全部的視圖派生自CollectionView,可是兩個特殊的實現派生自CollectionView:ListCollectionView和BindingListCollectionView。這是它如何工做:數據庫

  • 若是數據源實現IBindingList,一個BindingListCollectionView被創造。當你綁定一個ADO.NET DataTable時發生。
  • 若是數據源沒有實現IBindingList可是它實現IList,一個ListCollectionView被創造。當你綁定一個ObservableCollection,如同產品的列表。
  • 若是你的數據源沒有實現IBindingList或IList可是它實現IEnumerable,你得到一個基本CollectionView。

取回一個視圖對象

爲得到一個目前使用的視圖對象,你使用System.Windows.Data.CollectionViewSource類的GetDefaultView()靜態方法。當你調用GetDefaultView(),並傳遞數據源,就是你正使用的集合。這是一個例子,得到綁定到列表的產品集合的視圖:api

ICollectionView view = CollectionViewSource.GetDefaultView(lstProducts.ItemsSource);

GetDefaultView()方法總返回一個ICollectionView引用。你須要根據數據源轉換視圖對象到合適的類,多是ListCollectionView或BindingListCollectionView。this

var view = (ListCollectionView)
    CollectionViewSource.GetDefaultView(lstProducts.ItemsSource);

 用視圖導航

視圖對象決定列表項目的數目(Count屬性)和得到當前的數據對象一個引用(CurrentItem)或當前的位置索引(CurrentPosition)。也能使用幾個方法從一記錄移動到另外一個,諸如MoveCurrentToFirst(),MoveCurrentToLast(),MoveCurrentToNext(),MoveCurrentToPrevious(),和MoveCurrentToPosition()。編碼

顯示綁定產品數據的綁定文本框保持不變。他們只須要指明合適的屬性,以下所示:spa

<TextBlock Margin="7">Model Number:</TextBlock>
<TextBox Margin="5" Grid.Column="1" Text="{Binding Path=ModelNumber}"></TextBox>

可是,這例子沒有包括任何列表控件,因此你要控制導航。爲簡化生活,你能在你的窗口類添加一個成員變量,存儲指向視圖的一個引用:code

private ListCollectionView view;

在這種狀況下,代碼轉換視圖到合適的視圖類型(ListCollectionView)而不是使用ICollectionView接口。ICollectionView接口提供了大多數功能,可是它缺少Count屬性。component

當窗口第一次加載,你能得到數據,放置它到窗口的DataContext,和存儲一個引用指向視圖:xml

var products = App.StoreDB.GetProducts();
this.DataContext = products;

view = (ListCollectionView)
    CollectionViewSource.GetDefaultView(this.DataContext);
view.CurrentChanged += new EventHandler(view_CurrentChanged);

第二行在DataContext中放置產品對象的完整集合。綁定控件將沿元素樹向上搜索,直到他們發現這個對象。固然,你但願綁定表達式綁定到集合的當前項目,而不是綁定到集合自己,可是WPF足夠聰明能自動地推算。它自動地提供他們當前項目,因此你不須要額外的代碼的一個縫合。htm

前一個例子有一附加的代碼語句。它鏈接一個事件處理器到視圖的CurrentChanged事件。當事件發生,你能執行幾個有用的行爲,諸如前一個和下一個按鈕依賴於當前位置可用或不可用,和在窗口底部的TextBlock顯示當前位置。

private void view_CurrentChanged(object sender, EventArgs e)
{
    lblPosition.Text = "Record " + (view.CurrentPosition + 1).ToString() +
      " of " + view.Count.ToString();
    cmdPrev.IsEnabled = view.CurrentPosition > 0;
    cmdNext.IsEnabled = view.CurrentPosition < view.Count - 1;
}

最後一步是寫前一個和下一個按鈕的邏輯。由於當這些按鈕不能應用時,自動地不可用。你不須要考慮可能會移動到第一個項目以前或最後一個項目以後。

private void cmdNext_Click(object sender, RoutedEventArgs e)
{
    view.MoveCurrentToNext();
}
private void cmdPrev_Click(object sender, RoutedEventArgs e)
{
    view.MoveCurrentToPrevious();
}

你能添加一個組合框到窗口,用於直接跳到某一記錄。

<ComboBox Name="lstProducts" DisplayMemberPath="ModelName"
 Text="{Binding Path=ModelName}"
 SelectionChanged="lstProducts_SelectionChanged"></ComboBox>

指定數據源:

lstProducts.ItemsSource = products;

默認狀況下,ItemsControl的當前項目不與視圖的當前項目同步。幸運地,有兩個容易的方法解決問題。

第一個用傳統的代碼方式強制同步:

private void lstProducts_SelectionChanged(object sender, RoutedEventArgs e)
{
    view.MoveCurrentTo(lstProducts.SelectedItem);
}

一個更簡單解決方案是設置ItemsControl.IsSynchronizedWithCurrentItem爲真。那樣,目前選擇項目自動地同步匹配視圖的當前位置。

使用查詢表幫助編輯

組合框能方便地編輯記錄值。

例如,你可能有數據庫一個字段接受幾個預置值之一。在這種狀況下,使用一個組合框,綁定它到合適的字段,在Text屬性上使用一個綁定表達式。可是,填充組合框用允許的值,依靠設置它的ItemsSource屬性指向你定義列表。而且若是你但願顯示列表值一方式(例如,爲文本)可是存儲他們另外一個方式(爲數字編碼),只要添加一個值轉換器到你的Text屬性綁定。

另外一個狀況是相關表。例如,你可能但願容許用戶拾一個產品目錄使用定義全部的目錄列表。基本方法是相同的:設置Text屬性綁定合適的字段,和用ItemsSource屬性填充選項列表。若是你須要轉換低層的IDs到更有意義的名字,使用一個值轉換器。

用聲明方式創造一個視圖

你能在XAML標記以聲明方式構造一個CollectionViewSource,和而後綁定CollectionViewSource到你的控件(諸如列表)。

從技術上,CollectionViewSource不是一個視圖。它是一個幫助者類,容許你取回一個視圖(使用GetDefaultView()方法)和一個工廠,能創造一個視圖。

CollectionViewSource類的二最重要的屬性是View,包裹視圖對象,和Source,包裹數據源。CollectionViewSource也添加SortDescriptions和GroupDescriptions屬性,這鏡像同一地命名視圖屬性。當CollectionViewSource創造一個視圖,它簡單地傳遞這些屬性的值到視圖。

CollectionViewSource也包含一個Filter事件,你能處理執行過濾。這過濾工做方式等同於視圖對象提供的過濾回調,除了它被定義爲一個事件,因此你能容易地在XAML中掛鉤上你的事件處理器。

例如,考慮前一個例子,使用價格範圍這對產品分組。這是你如何以聲明方式定義轉換器和CollectionViewSource:

<local:PriceRangeProductGrouper x:Key="Price50Grouper" GroupInterval="50"/>
<CollectionViewSource x:Key="GroupByRangeView">
  <CollectionViewSource.SortDescriptions>
    <component:SortDescription PropertyName="UnitCost" Direction="Ascending"/>
  </CollectionViewSource.SortDescriptions>
  <CollectionViewSource.GroupDescriptions>
    <PropertyGroupDescription PropertyName="UnitCost"
       Converter="{StaticResource Price50Grouper}"/>
  </CollectionViewSource.GroupDescriptions>
</CollectionViewSource>

注意,SortDescription類不是WPF名字空間。爲了使用它,你須要填加下面的名字空間別名:

xmlns:component="clr-namespace:System.ComponentModel;assembly=WindowsBase"

一旦你創建CollectionViewSource,你能綁定它到你的列表:

<ListBox ItemsSource="{Binding Source={StaticResource GroupByRangeView}}" ... >

彷佛列表框控件綁定到CollectionViewSource,而不是CollectionViewSource暴露的視圖(這被存儲在CollectionViewSource.View屬性)。可是,WPF數據綁定對於CollectionViewSource一個特殊的例外。當你使用它在一個綁定表達式,WPF請求CollectionViewSource創造它的視圖,而後綁定視圖到合適的元素。

聲明式的方法沒有真正地節省你任何工做。你仍然須要在運行時用代碼取回數據。不一樣的是如今你的代碼必須傳遞數據沿着到CollectionViewSource而不是直接提供它到列表:

var products = App.StoreDB.GetProducts();
var viewSource = (CollectionViewSource)
    this.FindResource("GroupByRangeView");
viewSource.Source = products;

可選地,你能使用XAML標記創造產品集合做爲一個資源。而後你能以聲明方式綁定CollectionViewSource到你的產品集合。可是,你仍然須要使用代碼填充你的產品集合。

過濾、排序、和分組

視圖跟蹤數據對象集合的當前位置。這是一個重要的任務,和發現(或改變)當前項目是使用視圖的最廣泛緣由。

視圖也提供若干可選的特徵那容許你管理項目的全體集合。在下幾節中,你將會看到你能如何使用一個視圖過濾你的數據項目(暫時地隱藏那些你不但願看見),你能如何使用它應用排序(改變數據項目順序),和你能如何使用它應用分組(創造能被獨立地導航子集合)。

過濾集合

過濾容許你顯示知足特定條件的一個子集。當帶有一個集合做爲數據源工做時,你使用視圖對象的Filter屬性設置過濾。

Filter屬性的實現有點笨拙。它接受一個Predicate委託指向一個自定義過濾方法(你創造)。這是一個例子,你能如何鏈接視圖到方法FilterProduct():

var view = (ListCollectionView) 
    CollectionViewSource.GetDefaultView(lstProducts.ItemsSource);
view.Filter = new Predicate<object>(FilterProduct);

笨拙之處在於你只能使用Predicate<object>類型,而不是Predicate<Product>類型。

這是一個簡單的過濾器方法,只容許單價高於100的產品:

public bool FilterProduct(Object item)
{
    var product = (Product) item;
    return (product.UnitCost > 100);
}

使用匿名委託,定義內聯的過濾方法:

var view = (ListCollectionView)
    CollectionViewSource.GetDefaultView(lstProducts.ItemsSource);
view.Filter = delegate(object item)
              {
                  Product product = (Product)item;
                  return (product.UnitCost > 100);
              };

儘管這是一個整潔的,優雅的方法,在更復雜的過濾器場景下,你更可能創造一個專用的過濾類。那是由於在這些狀況下,你常常須要過濾使用幾個不一樣的準則,和後來你可能但願能修改過濾準則。

過濾類包裹過濾準則和執行過濾的回調方法。這裏是一個極端地簡單的過濾類,過濾掉單價小於最小价格的產品:

public class ProductByPriceFilter
{
    public decimal MinimumPrice
    {
        get; set;
    }
    public ProductByPriceFilter(decimal minimumPrice)
    {
        MinimumPrice = minimumPrice;
    }
    public bool FilterItem(Object item)
    {
        var product = item as Product;
        if (product != null)
        {
            return (product.UnitCost > MinimumPrice);
        }
        return false;
    }
}

這裏是創造ProductByPriceFilterer和使用它應用最小价格過濾的代碼:

private void cmdFilter_Click(object sender, RoutedEventArgs e)
{
    decimal minimumPrice;
    if (Decimal.TryParse(txtMinPrice.Text, out minimumPrice))
    {
        var view =
            CollectionViewSource.GetDefaultView(lstProducts.ItemsSource)
            as ListCollectionView;
        if (view != null)
        {
            var filter =
                new ProductByPriceFilter(minimumPrice);
            view.Filter = new Predicate<object>(filter.FilterItem);
        }
    }
}

你可能想創造不一樣的過濾器對於過濾不一樣的數據的類型。例如,你可能計劃創造(和重用)一個MinMaxFilter,一個StringFilter,等等。不管如何,一般更有幫助的是對於每一個窗口創造一個單個的過濾類。那是由於你不能鏈一個以上過濾在一塊兒。

若是你但願不從新創造ProductByPriceFilter對象狀況下,修改過濾,你須要在你的窗口類存儲一個成員變量引用過濾對象。而後你能修改過濾屬性。可是,你也須要調用視圖對象的Refresh()方法強迫列表被從新過濾。這裏是一些代碼,當包含最小价格的文本框的TextChanged事件發生時,調整過濾設置:

private void txtMinPrice_TextChanged(object sender, TextChangedEventArgs e)
{
    var view =
      CollectionViewSource.GetDefaultView(lstProducts.ItemsSource)
      as ListCollectionView;
    if (view != null)
    {
        decimal minimumPrice;
        if (Decimal.TryParse(txtMinPrice.Text, out minimumPrice) &&
          (filter != null))
        {
            filter.MinimumPrice = minimumPrice;
            view.Refresh();
        }
    }
}

最後,依靠設置Filter屬性爲空,你能徹底清除過濾器。

view.Filter = null;

 

過濾DataTable

詳見656頁。

排序

最簡單的方法是基於每一個數據項目一個或多個屬性值分類。每一個SortDescription表明一個屬性:

var view = CollectionViewSource.GetDefaultView(lstProducts.ItemsSource);
view.SortDescriptions.Add(
  new SortDescription("ModelName", ListSortDirection.Ascending));

也能夠自定義排序規則:

public class SortByModelNameLength : IComparer
{
    public int Compare(object x, object y)
    {
        var productX = (Product)x;
        var productY = (Product)y;
        return productX.ModelName.Length.CompareTo(productY.ModelName.Length);
    }
}

自定義規則鏈接到視圖:

var view = (ListCollectionView)
    CollectionViewSource.GetDefaultView(lstProducts.ItemsSource);
view.CustomSort = new SortByModelNameLength();

分組

正如排序,你能用容易的辦法分組(基於單個的屬性值)或困難地辦法(使用一個自定義回調)。

簡單分組法:

var view = CollectionViewSource.GetDefaultView(lstProducts.ItemsSource);
view.GroupDescriptions.Add(new PropertyGroupDescription("CategoryName"));

當你使用分組,你的列表爲每一個分組創造一個獨立的GroupItem對象,而且它添加這些GroupItem對象到列表。GroupItem是一個內容控件,因此每一個GroupItem持有合適的容器(好像ListBoxItem對象),容器帶有你的實際的數據。顯示你的分組的關鍵是格式化GroupItem元素因此它脫穎而出。

你能使用一個樣式,應用格式化到一個列表中全部的GroupItem對象。可是,你可能但願不只僅格式化—例如,你可能但願顯示組標頭,這要求一個模板的幫助。幸運地,ItemsControl類使兩個任務容易,經過它的ItemsControl.GroupStyle屬性,這提供一個GroupStyle對象的集合。儘管名字,GroupStyle類不是一個樣式。它只是一個方便的包那包裹幾個有用的設置對於配置你的GroupItem對象。

GroupStyle屬性:

名字 描述
ContainerStyle 設置被應用於GroupItem樣式,對於每一個分組生成。
ContainerStyleSelector 代替使用ContainerStyle,你能使用ContainerStyleSelector提供一個類那選擇正確樣式使用,基於分組。
HeaderTemplate 容許你創造一個模板顯示每一個分組的開始內容。
HeaderTemplateSelector 代替使用HeaderTemplate,你能使用HeaderTemplateSelector提供一個類那選擇正確標頭模板使用,基於分組。
Panel 容許你改變被用於持有分組的模板。例如,你能使用一個WrapPanel代替標準StackPanel創造一個列表平鋪分組從左到右和而後向下。

這個例子,只設置每一個分組以前的標頭。

爲添加一個分組標頭,你須要設置GroupStyle.HeaderTemplate。你能用一個普通的數據模板填充這屬性。你能使用元素的任意組合和你模板內部的數據綁定表達式。

可是,存在一訣竅。當你寫你的綁定表達式,你不是綁定你列表的數據對象(在這種狀況下,Product對象)。而是,你綁定分組的PropertyGroupDescription對象。那意味着若是你但願爲了那個分組顯示字段值,你須要綁定PropertyGroupDescription.Name屬性而不是Product.CategoryName。

這是完整的模板:

<ListBox Name="lstProducts" DisplayMemberPath="ModelName">
  <ListBox.GroupStyle>
    <GroupStyle>
      <GroupStyle.HeaderTemplate>
        <DataTemplate>
          <TextBlock Text="{Binding Path=Name}" FontWeight="Bold"
           Foreground="White" Background="LightGreen"
           Margin="0,5,0,0" Padding="3"/>
        </DataTemplate>
      </GroupStyle.HeaderTemplate>
    </GroupStyle>
  </ListBox.GroupStyle>
</ListBox>

ListBox.GroupStyle屬性其實是一個GroupStyle對象的集合。這容許你添加多個分組的水平。爲作如此,你須要添加一個以上PropertyGroupDescription(順序你但願你的分組和子組應用)和而後添加一個匹配GroupStyle對象格式每一個水平。

你可能但願使用分組協同排序。若是你但願分類你的組,只確保第一SortDescription你使用分類基於分組字段。下列代碼分類目錄按字母順序依靠目錄名字和而後分類每一個產品依靠模型名字順序。

view.SortDescriptions.Add(new SortDescription("CategoryName",
  ListSortDirection.Ascending));
view.SortDescriptions.Add(new SortDescription("ModelName",
  ListSortDirection.Ascending));

分組範圍

本節講基於數值的範圍分組,依靠的是值轉換器,將不一樣值轉換爲一個相同值。詳見661頁。

分組和虛擬化

見663頁。

Live Shaping

見663頁。

相關文章
相關標籤/搜索