WPF之數據綁定


Windows GUI運行的機理是使用消息(Mesage)來驅使程序向前運行,消息的主要來源是用戶的操作(如單擊鼠標、按下按鈕),消息又會被Windows翻譯並送達目標程序然后被程序所處理。程序是被來自UI的事件(即封裝過的消息)驅使問前的,簡稱“消息驅動”或“事件驅動”。因為消息和事件大都來自於UI,所以統稱它們為“UI驅動程序”。使用“UI驅動程序”開發程序是“為了GUI而GUI”、單純地為了實現程序的GUI化,已經背離了程序的本質一一數據加算法。

Data Binding在WPF中的地位

一般情況下,應用程序會具有三層結構:

  • 數據存儲層:由數據庫和文件系統構成;
  • 數據處理層:也叫邏輯層,與業務邏輯相關、用於加工處理數據的算法都集中在這里;
  • 數據展示層:把加工后的數據通過可視的界面展示給用戶或者通過其他種類的接口展示給別的應用程序(界面和接口兩個詞在英文中均為interface),還需要收集用戶的操作、把它們反饋給邏輯層。

WPF作為一種專門的展示層技術,華麗的外觀和動畫只是它的表層現象。WPF引入了Data Binding概念以及與之配套的Dependency Property系統和DataTemplate,幫助程序員把思維的重心固定在了邏輯層、讓展示層永遠處於邏輯層的從屬地位。

展示層與邏輯層的溝通使用Data Binding來實現,加工好的數據會自動送達用戶界面加以顯示,被用戶修改過的數據也會自動傳回邏輯層,一旦數據被加工好又會被送達用戶界面。用加工好的數據驅動程序的用戶界面以文字、圖形、動畫等形式把數據顯示出來——這就是“數據驅動UI”。

經過Data Binding優化,所有與業務邏輯相關的算法都處在數據邏輯層,邏輯層成為一個能夠獨立運轉的、完整的體系,而用戶界面層則不含任何代碼、完全依賴和從屬於數據邏輯層。

Binding基礎

Binding:音譯“綁定”,英文中有“捆綁”、“關聯”和“鍵聯”的含義,Binding更注重表達它是一種像橋梁一樣的關聯關系

把Binding比作數據的橋梁,它的兩端分別是Binding的源(Source)目標(Target)。數據從哪里來哪里就是源,Binding是架在中間的橋梁,Binding目標是數據要往哪兒去。一般情況下,Binding源是邏輯層的對象,Binding目標是UI層的控件對象

先創建一個Student類,類的實例作為數據源,代碼如下:

public class Student : INotifyPropertyChanged
{
    private string name;

    public string Name
    {
        get { return name; }
        set
        {
            name = value;
            if (PropertyChanged != null)
            {
                this.PropertyChanged.Invoke(this, new PropertyChangedEventArgs("Name"));
            }
        }
    }
    public event PropertyChangedEventHandler PropertyChanged;
}

准備一個TextBox和Button,代碼如下:

<StackPanel Margin="0,0,2,0">
    <TextBox x:Name="textBoxName" BorderBrush="Black" Margin="5"/>
    <Button Content="Add Age"  Margin="5"  Click="Button_Click"/>
</StackPanel>

使用Binding把數據源和UI元素連接起來,C#代碼如下:

public partial class MainWindow : Window
{
    Student stu = null;
    public MainWindow()
    {
        InitializeComponent();
        //准備數據源
        stu = new Student();
        //准備Binding
        Binding binding = new Binding();
        binding.Source = stu;
        binding.Path = new PropertyPath("Name");

        //使用Binding 連接數據源與Binding目標
        BindingOperations.SetBinding(this.textBoxName,TextBox.TextProperty,binding);
        //或
        this.textBoxName.SetBinding(TextBox.TextProperty, binding);
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        stu.Name += "Name";            
    }
}

上面這段代碼可以簡化成這樣:

public partial class MainWindow : Window
{
    Student stu = null;
    public MainWindow()
    {
        InitializeComponent();
        //三合一操作
        this.textBoxName.SetBinding(TextBox.TextProperty,new Binding("Name") { Source = stu = new Student()});
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        stu.Name += "Name";            
    }
}

UI上的元素關心的是哪個屬性值的變化,這個屬性就稱為Binding的路徑(Path)。Binding是一種自動機制,當值變化后屬性要有能力通知Binding,讓Binding把變化傳遞給UI元素,需要在屬性的set 語句中激發一個PropertyChanged事件。這個事件不需要自己聲明,讓作為數據源的類實現System.ComponentModel名稱空間中的INotifyPropertyChanged接口,Binding會自動偵聽來自這個接口的PropertyChanged事件。

Binding模型如下所示:

Binding的源與路徑

Binding的源也就是數據的源頭,只要它是一個對象,並且通過屬性(Property)公開自己的數據,它就能作為Binding的源。如果想讓作為Binding源的對象具有自動通知Binding自己的屬性值已經變化的能力,那么就需要讓類實現INotifyPropertyChanged 接口並在屬性的set 語句中激發PropertyChanged事件。

除了使用這種對象作為數據源外,還有:

  • 把自己或自己的容器或子級元素當源
  • 用一個控件作為另一個控件的數據源
  • 把集合作為ItemsControl的數據源
  • 使用XML作為TreeView或Menu的數據源
  • 把多個控件關聯到一個“數據制高點”上,甚至干脆不給Binding 指定數據源、讓它自己去找

把控件作為Binding源與Binding標記擴展

為了讓UI元素產生一些聯動效果會使用Binding在控件間建立關聯,下面把一個TextBox的Text屬性關聯在Slider的Value屬性上,代碼如下:

<StackPanel>
    <TextBox x:Name="textBox1" Text="{Binding Path=Value, ElementName=slider1}" BorderBrush="Black" Margin="5"/>
    <Slider x:Name="slider1" Maximum="100" Minimum="0" Margin="5"/>
</StackPanel>

上面的代碼使用了Binding標記擴展語法:

<TextBox x:Name="textBox1" Text="{Binding Path=Value, ElementName=slider1}" BorderBrush="Black" Margin="5"/>

等效C#代碼如下:

this.textBox1.SetBinding(TextBox.TextProperty, new Binding("Value") { ElementName = "slider1" });
//一般也不會使用Binding的ElementName屬性,而是直接把對象賦值給Binding的Source屬性,方便重構
this.textBox1.SetBinding(TextBox.TextProperty, new Binding("Value") {Source=slider1});

Binding類的構造器本身可以接收Path作為參數,也常寫為:

<TextBox x:Name="textBox1" Text="{Binding Value, ElementName=slider1}" BorderBrush="Black" Margin="5"/>

控制Binding的方向及數據更新

控制Binding數據流向的屬性是Mode,它的類型是BindingMode枚舉,可取值為:

  • TwoWay:導致更改源屬性或目標屬性時自動更新另一方,雙向模式
  • OneWay:在更改綁定源(源)時更新綁定目標(目標),從源向目標的單向溝通
  • OnTime:在應用程序啟動或數據上下文更改時,更新綁定目標,只在Binding關系確立時讀取一次數據
  • OneWayToSource:在目標屬性更改時,更新源屬性,從目標向源的單向溝通
  • Default:使用綁定目標的默認 Mode 值,Binding的模式會根據目標的實際情況來確定(若是可編輯的就采用雙向模式,如TextBox.Text屬性;若是只讀的則采用單向模式,如TextBlock.Text)。

控制Binding數據更新的屬性是UpdateSourceTrigger,它的類型是UpdateSourceTrigger枚舉,可取值為:

  • PropertyChanged:每當綁定目標屬性發生更改時,都會更新綁定源。
  • LostFocus:每當綁定目標元素失去焦點時,都會更新綁定源。
  • Explicit:僅在調用 UpdateSource() 方法時更新綁定源。
  • Default:綁定目標屬性的默認 UpdateSourceTrigger 值,大多數依賴屬性的默認值為PropertyChanged,而Text屬性的默認值為LostFocus

Binding還具有NotifyOnSourceUpdatedNotifyOnTargetUpdated兩個bool類型的屬性。如果設為true,則當源或目標被更新后Binding會激發相應的SourceUpdated事件和TargetUpdated事件。實際工作中,我們可以通過監聽這兩個事件來找出有哪些數據或控件被更新了。

Binding的路徑(Path)

作為Binding源的對象可能有很多屬性,需要由Binding的Path屬性來指定Binding到底需要關注哪個屬性的值,如前面把Slider控件對象當作源、把它的Value屬性作為路徑。盡管在XAML代碼中或者Binding類的構造器參數列表中以一個字符串來表示Path,但Path的實際類型是PropertyPath

關聯在Binding源的屬性上

XAML語法如下:

<TextBox x:Name="textBox1" Text="{Binding Path=Value,ElementName=sliderl}"/>

等效的C#代碼如下:

Binding binding = new Binding(){ Path = new PropertyPath("Value"), Source = this.slider1 }; 
this.textBox1.SetBinding(TextBox.TextProperty, binding);

使用Binding的構造器簡寫為:

Binding binding = new Binding() { Path = new PropertyPath("Value"), Source = this.slider1 };
this.textBox1.SetBinding(TextBox.TextProperty, binding);

Binding的多級路徑

Binding支持多級路徑(通俗地講就是一路“點”下去),如讓一個TextBox顯示另外一個TextBox的文本長度,XAML代碼如下:

<StackPanel>
    <TextBox x:Name="textBox1" BorderBrush="Black" Margin="5"/>
    <TextBox x:Name="textBox2" Text="{ Binding Path=Text.Length, ElementName=textBox1, Mode=OneWay}" BorderBrush="Black" Margin="5"/>
</StackPanel>

等效的C#代碼是:

this.textBox2.SetBinding(TextBox.TextProperty, new Binding("Text.Length")
{
    Source = this.textBox1,
    Mode =BindingMode.OneWay
});

效果如下:

使用索引器作為Path

集合類型的索引器(Indexer)又稱為帶參屬性,也能作為Path來使用。如讓一個TextBox顯示另一個TextBox文本的第四個字符,XAML代碼如下:

<!--XAML編輯器會認為這是錯誤的語法,但不影響編譯-->
<TextBox x:Name="textBox2" Text="{Binding Path=Text.[3],ElementName=textBox1,Mode=OneWay}" BorderBrush="Black" Margin="5"/>

等效的C#代碼是:

//可以把Text與[3]之間的那個“.”省略掉,一樣可以正確工作
this.textBox2.SetBinding(TextBox.TextProperty, new Binding("Text.[3]")
{
    Source = this.textBox1,
    Mode =BindingMode.OneWay
});

效果如下:

使用集合或者DataView作為Binding源

當使用一個集合或者DataView作為Binding源時,如果想把它的默認元素當作Path使用,則需要使用這樣的語法:

List<string> stringList = new List<string>() { "Tim", "Tom", "Blog" };
textBox1.SetBinding(TextBox.TextProperty, new Binding("/") { Source = stringList });
textBox2.SetBinding(TextBox.TextProperty, new Binding("/Length") { Source = stringList, Mode = BindingMode.OneWay });
textBox3.SetBinding(TextBox.TextProperty, new Binding("/[2]") { Source = stringList, Mode = BindingMode.OneWay });

//等效代碼
textBox1.SetBinding(TextBox.TextProperty, new Binding("[0]") { Source = stringList });
textBox2.SetBinding(TextBox.TextProperty, new Binding("[0].Length") { Source = stringList, Mode = BindingMode.OneWay });
textBox3.SetBinding(TextBox.TextProperty, new Binding("[0].[2]") { Source = stringList, Mode = BindingMode.OneWay });

效果如下:

如果集合元素的屬性仍然還是一個集合,想把子級集合中的元素當做Path,則可以使用多級斜線的語法(即一路“斜線”下去),代碼如下:

//相關類型
class City
{
    public string Name { get; set; }
}
class Province
{
    public string Name { get; set; }
    public List<City> CityList { get; set; }
}
class Country
{
    public string Name { get; set; }
    public List<Province> ProvinceList { get; set; }
}


//Binding
List<Country> countryList = new List<Country> 
{ 
    new Country() 
    { 
        Name = "中國", 
        ProvinceList = new List<Province>() 
        { 
            new Province() 
            { 
                Name = "四川", 
                CityList = new List<City>() 
                { 
                    new City(){ Name = "成都" }
                } 
            } 
        } 
    } 
};
textBox1.SetBinding(TextBox.TextProperty, new Binding("/Name") { Source = countryList });
textBox2.SetBinding(TextBox.TextProperty, new Binding("/ProvinceList/Name") { Source = countryList });
textBox3.SetBinding(TextBox.TextProperty, new Binding("/ProvinceList/CityList/Name") { Source = countryList });
//等效C#代碼
textBox1.SetBinding(TextBox.TextProperty, new Binding("[0].Name") { Source = countryList });
textBox2.SetBinding(TextBox.TextProperty, new Binding("[0].ProvinceList[0].Name") { Source = countryList });
textBox3.SetBinding(TextBox.TextProperty, new Binding("[0].ProvinceList[0].CityList[0].Name") { Source = countryList });

效果如下:

“沒有Path”的Binding

有時候在代碼中看到一些Path是一個“.”或者干脆沒有Path的Binding,這是一種比較特殊的情況——Binding源本身就是數據且不需要Path來指明。如string、int等基本類型的實例本身就是數據,無法指出通過它的哪個屬性來訪問這個數據,這時只需將Path的值設置為“.”就可以了。
注:在XAML代碼里這個“.”可以省略不寫,但在C#代碼里卻不能省略
代碼如下:

<StackPanel>
    <StackPanel.Resources>
        <sys:String x:Key="myString">
            菩提本無樹,明鏡亦非台。
            本來無一物,何處惹塵埃。
        </sys:String>
    </StackPanel.Resources>
    <TextBlock x:Name="textBlock1" TextWrapping="Wrap" Margin="5" FontSize="16" Text="{Binding Path=.,Source={StaticResource ResourceKey=myString}}"/>
</StackPanel>

效果如下:

上面的代碼可以簡寫成以下形式:

<--形式1-!>
Text="{Binding .,Source={StaticResource ResourceKey=myString}}"
<--形式2-!>
Text="{Binding Source={StaticResource ResourceKey=myString}}"

等效的C#代碼如下:

string myString = "菩提本無樹,明鏡亦非台。本來無一物,何處惹塵埃。";
//形式1
textBlock1.SetBinding(TextBlock.TextProperty, new Binding(".") { Source = myString });
//形式2,容易被誤解為沒有指定Path
textBlock1.SetBinding(TextBlock.TextProperty, new Binding() { Source = myString });

為Binding 指定源(Source)的幾種方法

Binding的源是數據的來源,只要一個對象包含數據並能通過屬性把數據暴露出來,它就能當作Binding的源來使用。常見的辦法有:

  • 把普通CLR類型單個對象指定為Source:包括.NET Framework自帶類型的對象和用戶自定義類型的對象(需實現INotifyPropertyChanged接口),方法有兩種——把對象賦值給Binding.Source屬性或把對象的Name賦值給Binding.ElementName。
  • 把普通CLR集合類型對象指定為Source:包括數組、List ObservableCollection 等集合類型(一般是把ItemsControl派生類控件的ItemsSource 屬性關聯到一個集合對象上)。
  • 把ADO.NET數據對象指定為Source:包括DataTableDataView等對象。
  • 使用XmlDataProvider把XML數據指定為Source:XML可以用來表示單個數據對象或者集合,可以把樹狀結構的XML數據作為源指定給與級聯式的WPF控件(如TreeView和Menu)關聯的Binding。
  • 把依賴對象(Dependency Object)指定為Source:依賴對象可以作為Binding的目標或源,有可能形成Binding鏈,依賴對象中的依賴屬性可以作為Binding的Path
  • 把容器的DataContext 指定為Source(WPF Data Binding的默認行為):建立一個只設置Path不設置Source的Binding(只確定屬性、不確定對象),Binding會自動把控件的DataContext當作自己的Source(沿着控件樹向外層尋找帶有Path指定屬性的對象)。
  • 通過ElementName 指定Source:在C#代碼里可以直接把對象作為Source賦值給Binding,但XAML無法訪問對象,所以只能使用對象的Name屬性來找到對象
  • 通過Binding的RelativeSource屬性相對地指定Source:當控件需要關注自己的、自己容器的或者自己內部元素的某個值就需要使用這種辦法。
  • 把ObjectDataProvider對象指定為Source:當數據源的數據不是通過屬性而是通過方法暴露給外界的時候,可以使用這兩種對象來包裝數據源再把它們指定為Source。
  • 把使用LINQ檢索得到的數據對象作為Binding的源:LINQ查詢的結果是一個IEnumerable 類型對象

沒有Source的Binding——使用DataContext作為Binding的源

DataContext屬性被定義在FrameworkElement類(WPF控件的基類)里,所有WPF控件(包括容器控件)都具備這個屬性。WPF的UI布局是樹形結構,每個結點都是控件——在UI元素樹的每個結點都有DataContext。當一個Binding只知道自己的Path而不知道自己的Soruce時,它會沿着UI元素樹一路向樹的根部找過去,每路過一個結點就要看看這個結點的DataContext是否具有Path所指定的屬性:

  • 如果有,那就把這個對象作為自己的Source;
  • 如果沒有,那就繼續找下去;
  • 如果到了樹的根部還沒有找到,那這個Binding就沒有Source,因而也不會得到數據。

下面實現一個簡單的例子,代碼如下:

//創建一個名為Student的類,具有Id、Name、Age三個屬性
public class Student
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
}
<StackPanel Background="LightBlue">
    <StackPanel.DataContext>
        <local:Student Id="6" Age="29" Name="Tim"/>
    </StackPanel.DataContext>
    <Grid>
        <StackPanel>
            <TextBox Text="{Binding Path=Id}" Margin="5"/>
            <TextBox Text="{Binding Path=Name}" Margin="5"/>
            <TextBox Text="{Binding Path=Age}" Margin="5"/>
        </StackPanel>
    </Grid>
</StackPanel>

UI布局的樹狀圖如下:

三個TextBox的Text通過Binding獲取值,但只為Binding指定了Path,沒有指定Source,可以簡寫為:

<TextBox Text="{Binding Id}" Margin="5"/>
<TextBox Text="{Binding Name}" Margin="5"/>
<TextBox Text="{Binding Age}" Margin="5"/>

效果如下:

當某個DataContext是一個簡單類型對象的時候,Path可以設置為“.”或者省略不寫,可能看到一個“既沒有Path又沒有Source的”Binding:

<StackPanel>
    <StackPanel.DataContext>
        <sys:String>Hello DataContext!</sys:String>
    </StackPanel.DataContext>
    <Grid>
        <StackPanel>
            <TextBlock Text="{Binding}" Margin="5"/>
            <TextBlock Text="{Binding}" Margin="5"/>
            <TextBlock Text="{Binding}" Margin="5"/>
        </StackPanel>
    </Grid>
</StackPanel>

DataContext是一個“依賴屬性”,當沒有為控件的某個依賴屬性顯式賦值時,控件會把自己容器的屬性值“借過來”當作自己的屬性值。“Binding沿着UI元素樹向上找”只是一個錯覺,實際上是屬性值沿着UI元素樹向下傳遞了

在實際工作中DataContext的用法是非常靈活的,比如:

  • 當UI上的多個控件都是用Binding關注同一個對象時,不妨使用DataContext。
  • 當作為Source的對象不能被直接訪問的時候——比如B窗體內的控件想把A窗體內的控件(private訪問級別)當作自己的Binding源時,可以把這個控件(或者控件的值)作為窗體A的DataContext(public訪問級別)從而暴露數據。

使用集合對象作為列表控件的ItemsSource

WPF中的列表式控件們派生自ItemsControl類,繼承了ItemsSource屬性。ItemsSource屬性可以接收一個IEnumerable接口派生類的實例作為自己的值(所有可被迭代遍歷的集合都實現了這個接口,包括數組、List 等)。每一個ItemsControl的派生類都具有自己對應的Item Container(條目容器,如ListBox的ListBoxItem、ComboBox的ComboBoxItem)。只要為一個ItemsControl對象設置了ItemsSource屬性值,ItemsControl對象就會自動迭代其中的數據元素,為每個數據元素准備一個條目容器,並使用Binding在條目容器與數據元素之間建立起關聯。

使用DisplayMemberPath

XAML代碼如下:

<StackPanel>
    <TextBlock Text="Student ID:" FontWeight="Bold" Margin="5"/>
    <TextBox x:Name="textBoxId" Margin="5"/>
    <TextBlock Text="Student List:" FontWeight="Bold" Margin="5"/>
    <ListBox x:Name="listBoxStudents" Height="110" Margin="5"/>
</StackPanel>

C#代碼如下:

//准備數據源
List<Student> stuList = new List<Student>()
{
    new Student(){Id=0,Name="Tim",Age=29},
    new Student(){Id=1,Name="Tom",Age=28},
    new Student(){Id=2,Name="Kyle",Age=27},
    new Student(){Id=3,Name="Tony",Age=26},
    new Student(){Id=4,Name="Vina",Age=25},
    new Student(){Id=5,Name="Mike",Age=24},
};

//為ListBox設置Binding
listBoxStudents.ItemsSource = stuList;
listBoxStudents.DisplayMemberPath = "Name";

//為TextBox設置Binding
Binding binding = new Binding("SelectedItem.Id") { Source = listBoxStudents };
textBoxId.SetBinding(TextBox.TextProperty, binding);

效果如下:

當DisplayMember屬性被賦值后,ListBox在獲得ItemsSource的時候就會創建等量的ListBoxItem並以DisplayMemberPath屬性值為Path創建Binding,Binding的目標是ListBoxItem的內容插件(實際上是一個TextBox)。這個創建Binding的過程是在DisplayMemberTemplateSelector類的SelectTemplate方法里完成的,方法定義的格式如下:

public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
    //...
    FrameworkElementFactory text = ContentPresenter.CreateTextBlockFactory();
    Binding binding = new Binding();
    binding.XPath = _displayMemberPath;
    binding.StringFormat = _stringFormat;
    text.SetBinding(TextBlock.TextProperty, binding);
    //...
}

使用DataTemplate

刪除C#代碼中的listBoxStudents.DisplayMemberPath = "Name";,XAML代碼如下:

<StackPanel>
    <TextBlock Text="Student ID:" FontWeight="Bold" Margin="5"/>
    <TextBox x:Name="textBoxId" Margin="5"/>
    <TextBlock Text="Student List:" FontWeight="Bold" Margin="5"/>
    <ListBox x:Name="listBoxStudents" Height="150" Margin="5">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="{Binding Path=Id}" Width="30"/>
                    <TextBlock Text="{Binding Path=Name}" Width="60"/>
                    <TextBlock Text="{Binding Path=Age}" Width="30"/>
                </StackPanel>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
</StackPanel>

效果如下:

使用集合類型作為列表控件的ItemsSource時一般會考慮使用ObservableCollection 代替List ,因為ObservableCollection 類實現了INotifyCollectionChanged和INotifyPropertyChanged接口,能把集合的變化立刻通知顯示它的列表控件,改變會立刻顯現出來。

使用ADO.NET對象作為Binding的源

在.NET開發中使用ADO.NET類對數據庫進行操作。常見的工作是從數據庫中把數據讀取到DataTable中,再把DataTable顯示在UI列表控件里(如成績單、博客文章列表、論壇帖子列表等)。在流行的軟件架構中並不把直接顯示DataTable,而是先通過LINQ等手段把DataTable里的數據轉換成恰當的用戶自定義類型集合,但WPF也支持在列表控件與DataTable之間直接建立Binding。
假設已有一個DataTable的實例,數據內容如表所示:

Id Name Age
1 Tim 29
2 Tom 28
3 Tony 27
4 Kyle 26
5 Vina 25
6 Emily 24

加載方法如下所示:

private DataTable Load()
{
    DataTable dt = new DataTable();
    dt.Columns.Add("Id");
    dt.Columns.Add("Name");
    dt.Columns.Add("Age");
    dt.Rows.Add("1", "Tim", "29");
    dt.Rows.Add("2", "Tom", "28");
    dt.Rows.Add("3", "Tony", "27");
    dt.Rows.Add("4", "Kyle", "26");
    dt.Rows.Add("5", "Vina", "25");
    dt.Rows.Add("6", "Emily", "24");
    return dt;
}

使用ListBox顯示DataTable

XAML代碼如下:

<StackPanel Background="LightBlue">
    <ListBox x:Name="listBoxStudents" Height="130" Margin="5"/>
    <Button Content="Load" Height="25" Margin="5,0" Click="Button_Click"/>
</StackPanel>

C#代碼如下:

private void Button_Click(object sender, RoutedEventArgs e)
{
    //獲取DataTable實例
    DataTable dt = Load();

    listBoxStudents.DisplayMemberPath = "Name";
    listBoxStudents.ItemsSource = dt.DefaultView;
}

效果如下:

最重要的代碼是listBoxStudents.ItemsSource = dt.DefaultView;,DataTable的DefaultView屬性是一個DataView類型的對象(實現了IEnumerable接口),可以被賦值給ListBox.ItemsSource屬性。

使用ListView顯示DataTable

多數情況下會選擇ListView控件來顯示一個DataTable,XAML代碼如下:

<StackPanel Background="LightBlue">
    <ListView x:Name="listViewStudents" Height="200" Margin="5">
        <ListView.View>
            <GridView>
                <GridViewColumn Header="Id" Width="60" DisplayMemberBinding="{Binding Id}"/>
                <GridViewColumn Header="Name" Width="80" DisplayMemberBinding="{Binding Name}"/>
                <GridViewColumn Header="Age" Width="60" DisplayMemberBinding="{Binding Age}"/>
            </GridView>
        </ListView.View>
    </ListView>
    <Button Content="Load" Height="25" Margin="5,0" Click="Button_Click"/>
</StackPanel>

C#代碼如下:

private void Button_Click(object sender, RoutedEventArgs e)
{
    //獲取DataTable實例
    DataTable dt = Load();

    listViewStudents.ItemsSource = dt.DefaultView;  
}

效果如下:

有幾點需要注意的地方:

  • ListView和GridView不是同一級別的控件,ListView是ListBox的派生類,而GridView是ViewBase的派生類。ListView的View屬性是一個ViewBase類型的對象,GridView可以作為ListView的View來使用,而不能當作獨立的控件來使用。目前,ListView的View只有一個GridView可用,估計微軟在這里還會有擴展。
  • GridView的內容屬性是Columns(GridViewColumnCollection類型對象),此處省略了<GridView.Columns>...</GridView.Columns>這層標簽(XAML支持對內容屬性的簡寫),直接在 的內容部分定義了三個GridViewColumn對象。
  • GridViewColumn對象最重要的一個屬性是DisplayMemberBinding(類型為BindingBase),可以指定這一列使用什么樣的Binding去關聯數據(與ListBox不同,ListBox使用的是DisplayMemberPath屬性)。
  • 如果想用更復雜的結構來表示這一列的標題(Header)或數據,則可以為GridViewColumn設置HeaderTemplate和CellTemplate屬性,它們的類型都是DataTemplate。

DataTable不能直接拿來為ItemsSource賦值。不過可以把DataTable對象放在一個對象的DataContext屬性里,並且為ItemsSource設置一個既無Path又無Source的Binding,Binding能自動找到它的DefaultView並當作自己的Source來使用:

private void Button_Click(object sender, RoutedEventArgs e)
{
    //獲取DataTable實例
    DataTable dt = Load();

    listViewStudents.DataContext = dt;
    listViewStudents.SetBinding(ListView.ItemsSourceProperty, new Binding());
}

使用XML數據作為Binding源

.NET Framework提供了兩套處理XML數據的類庫:

  • 符合DOM(Document Object Model,文檔對象模型)標准的類庫:包括XmlDocument、XmlElement、XmlNode、XmlAttribute等類,特點是中規中矩、功能強大、但背負了太多XML的傳統和復雜。
  • 以LINQ(Language-Integrated Query,語言集成查詢)為基礎的類庫:包括XDocument、XElement、XNode、XAttribute等類,特點是可以使用LINQ進行查詢和操作、方便快捷、但會產生很多臨時對象
    本小節主要講解基於DOM標准的XML類庫,基於LINQ的部分放在下一節里討論。

現代程序設計的大多數數據傳輸都基於SOAP(Simple Object Access Protocol,簡單對象訪問協議)相關的協議,而SOAP又是通過將對象序列化為XML文本進行傳輸。XML文本是樹形結構的,所以XML可以方便地用於表示線性集合(如Array、List等)和樹形結構數據**。

當使用XML數據作為Binding的Source時將使用XPath屬性而不是Path屬性來指定數據的來源,XPath作為XML語言的功能有着一整套語法,詳盡的講解可以查閱:

從XML文檔顯示線性集合

下面的XML文本是一組學生的信息(假設存放在D:\RawData.xml文件中),內容如下:

<?xml version="1.0" encoding="utf-8" ?>
<StudentList>
  <Student Id="1">
    <Name>Tim</Name>
  </Student>
  <Student Id="2">
    <Name>Tom</Name>
  </Student>
  <Student Id="3">
    <Name>Vina</Name>
  </Student>
  <Student Id="4">
    <Name>Emily</Name>
  </Student>
</StudentList>

把它顯示在一個ListView控件里,XAML代碼如下:

<StackPanel Background="LightBlue">
    <ListView x:Name="listViewStudents" Height="200" Margin="5">
        <ListView.View>
            <GridView>
                <GridView.Columns>
                    <GridViewColumn Header="Id" Width="60" DisplayMemberBinding="{Binding XPath=@Id}"/>
                    <GridViewColumn Header="Name" Width="80" DisplayMemberBinding="{Binding XPath=Name}"/>
                </GridView.Columns>
            </GridView>
        </ListView.View>
    </ListView>
    <Button Content="Load" Height="25" Margin="5,0" Click="Button_Click"/>
</StackPanel>

Button的Click事件處理器中可以通過XmlDataProviderDocumentSource屬性指定XML,代碼如下:

//第一種:手動加載XML文檔並賦值給XmlDataProvider的Document屬性
private void Button_Click(object sender, RoutedEventArgs e)
{
    XmlDocument doc = new XmlDocument();
    doc.Load(@"D:\RawData.xml");

    XmlDataProvider xdp = new XmlDataProvider();
    xdp.Document = doc;

    //使用XPath選擇需要暴露的數據
    //現在是需要暴露一組Student
    xdp.XPath = @"/StudentList/Student";

    listViewStudents.DataContext = xdp;
    listViewStudents.SetBinding(ListView.ItemsSourceProperty, new Binding());
}

//第二種:直接指定XML文檔所在的位置(無論XML文檔存儲在本地硬盤還是網絡上)給XmlDataProvider的Source屬性
private void Button_Click(object sender, RoutedEventArgs e)
{
    XmlDataProvider xdp = new XmlDataProvider();
    xdp.Source = new Uri(@"D:\RawData.xml");
    xdp.XPath = @"/StudentList/Student";
    listViewStudents.DataContext = xdp;
    listViewStudents.SetBinding(ListView.ItemsSourceProperty, new Binding());
}

效果如下:

XAML代碼中關鍵的是DisplayMemberBinding="{Binding XPath=@Id}"DisplayMemberBinding="{Binding XPath=Name},它們分別為GridView的兩列指明了關注的XML路徑,使用@符號加字符串表示的是XML元素的Attribute,不加@符號的字符串表示的是子級元素。

從XAML代碼顯示樹形數據結構

XAML代碼如下:

<Window.Resources>
    <XmlDataProvider x:Key="xdp" XPath="FileSystem/Folder">
        <x:XData>
            <FileSystem xmlns="">
                <Folder Name="Books">
                    <Folder Name="Programming">
                        <Folder Name="Windows">
                            <Folder Name="WPF"/>
                            <Folder Name="MFC"/>
                            <Folder Name="Delphi"/>
                        </Folder>
                    </Folder>
                    <Folder Name="Tools">
                        <Folder Name="Development"/>
                        <Folder Name="Designment"/>
                        <Folder Name="Players"/>
                    </Folder>
                </Folder>
            </FileSystem>
        </x:XData>
    </XmlDataProvider>
</Window.Resources>

<Grid>
    <TreeView ItemsSource="{Binding Source={StaticResource xdp}}">
        <TreeView.ItemTemplate>
            <HierarchicalDataTemplate ItemsSource="{Binding XPath=Folder}">
                <TextBlock Text="{Binding XPath=@Name}"/>
            </HierarchicalDataTemplate>
        </TreeView.ItemTemplate>
    </TreeView>
</Grid>

效果如下:

如果把XmlDataProvider直接寫在XAML代碼里,它的XML數據需要放在<x:XData>...</x:XData>標簽里,StaticResourceHierarchicalDataTemplate等學完ResourceTemplate才會明白。

使用LINQ檢索結果作為Binding的源

.NET Framework 3.0開始支持LINQ(Language-Integrated Query,語言集成查詢),使用LINQ可以方便地操作集合對象、DataTable對象和XML對象,不必動輒就把好幾層foreach循環嵌套。
LINQ查詢的結果是一個IEnumerable 類型對象,而IEnumerable 又派生自IEnumerable,所以它可以作為列表控件的ItemsSource來使用。

創建了一個名為Student的類:

public class Student

{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
}

XAML代碼如下:

<StackPanel Background="LightBlue">
    <ListView x:Name="listViewStudents" Height="145" Margin="5">
        <ListView.View>
            <GridView>
                <GridViewColumn Header="Id" Width="60" DisplayMemberBinding="{Binding Id}"/>
                <GridViewColumn Header="Name" Width="100" DisplayMemberBinding="{Binding Name}"/>
                <GridViewColumn Header="Age" Width="80" DisplayMemberBinding="{Binding Age}"/>
            </GridView>
        </ListView.View>
    </ListView>
    <Button Content="Load" Height="25" Margin="5,0" Click="Button_Click"/>
</StackPanel>

從一個已經填充好的List 對象中檢索出所有名字以字母T開頭的學生,代碼如下:

private void Button_Click(object sender, RoutedEventArgs e)
{
    List<Student> stuList = new List<Student>()
    {
        new Student(){Id=0,Name="Tim",Age=29},
        new Student(){Id=1,Name="Tom",Age=28},
        new Student(){Id=2,Name="Kyle",Age=27},
        new Student(){Id=3,Name="Tony",Age=26},
        new Student(){Id=4,Name="Vina",Age=25},
        new Student(){Id=5,Name="Mike",Age=24}
    };

    listViewStudents.ItemsSource = from stu in stuList 
                                   where stu.Name.StartsWith("T") 
                                   select stu;
}

從一個已經填充好的DataTable對象中檢索出所有名字以字母T開頭的學生,代碼如下:

private void Button_Click(object sender, RoutedEventArgs e)
{
    DataTable dt = this.GetDataTable();
    listViewStudents.ItemsSource =
        from row in dt.Rows.Cast<DataRow>()
        where Convert.ToString(row["Name"]).StartsWith("T")
        select new Student()
        {
            Id = int.Parse(row["Id"].ToString()),
            Name = row["Name"].ToString(),
            Age = int.Parse(row["Age"].ToString())
        };
}

private DataTable GetDataTable()
{
    DataTable dt = new DataTable();            
    dt.Columns.Add("Id");
    dt.Columns.Add("Name");
    dt.Columns.Add("Age");
    dt.Rows.Add("1", "Tim", "29");
    dt.Rows.Add("2", "Tom", "28");
    dt.Rows.Add("3", "Tony", "27");
    dt.Rows.Add("4", "Kyle", "26");
    dt.Rows.Add("5", "Vina", "25");
    dt.Rows.Add("6", "Emily", "24");
    return dt;
}

從一個存儲數據的XML文件(D:RawData.xml)中檢索出所有名字以字母T開頭的學生,代碼如下:

<?xml version="1.0" encoding="utf-8"?>
<StudentList>
  <Class>
    <Student Id="0" Name="Tim" Age="29"/>
    <Student Id="1" Name="Tom" Age="28"/>
    <Student Id="2" Name="Mess" Age="27"/>
  </Class>
  <Class>
    <Student Id="3" Name="Tony" Age="26"/>
    <Student Id="4" Name="Vina" Age="25"/>
    <Student Id="5" Name="Emily" Age="24"/>
  </Class>
</StudentList>
private void Button_Click(object sender, RoutedEventArgs e)
{
    XDocument xdoc = XDocument.Load(@"D:\RawData.xml");

    listViewStudents.ItemsSource =
        from element in xdoc.Descendants("Student")
        where element.Attribute("Name").Value.StartsWith("T")
        select new Student()
        {
            Id = int.Parse(element.Attribute("Id").Value),
            Name = element.Attribute("Name").Value,
            Age = int.Parse(element.Attribute("Age").Value)
        };
}

注:xdoc.Descendants("Student")這個方法可以跨越XML的層級。

效果如下:

使用ObjectDataProvider對象作為Binding的源

有時很難保證一個類的所有數據都使用屬性暴露出來,比如需要的數據可能是方法的返回值,重新設計底層類的風險和成本會比較高且黑盒引用類庫時不可能改變已經編譯好的類,這時就需要使用ObjectDataProvider來包裝作為Binding源的數據對象了。

ObjectDataProvider把對象作為數據源提供給Binding,之前的XmlDataProvider也是把XML數據作為數據源提供給Binding,兩者的父類都是DataSourceProvider抽象類

有一個名為Calculator的類,它具有計算加、減、乘、除的方法:

class Calculator
{
    //加法
    public string Add(string arg1, string arg2)
    {
        double x = 0;
        double y = 0;
        double z = 0;
        if (double.TryParse(arg1, out x) && double.TryParse(arg2, out y))
        {
            z = x + y;
            return z.ToString();
        }
        return "Input Error!";
    }

    //其他算法...
}

先隨便新建一個WPF項目,添加一個Button,Button的Click事件處理器如下:

private void Button_Click(object sender, RoutedEventArgs e)
{
    ObjectDataProvider odp = new ObjectDataProvider();
    odp.ObjectInstance = new Calculator();
    odp.MethodName = "Add";
    odp.MethodParameters.Add("100");
    odp.MethodParameters.Add("200");
    MessageBox.Show(odp.Data.ToString());
}

效果如下:

由上可知ObjectDataProvider對象與被它包裝的對象關系如下所示:

把ObjectDataProvider當作Binding的Source來使用,實現第三個TextBox能實時地顯示上面兩個TextBox輸入數字的和,代碼和截圖如下:

<StackPanel Background="LightBlue">
    <TextBox x:Name="textBoxArg1" Margin="5"/>
    <TextBox x:Name="textBoxArg2" Margin="5"/>
    <TextBox x:Name="textBoxResult" Margin="5"/>
</StackPanel>
//創建並配置ObjectDataProvider對象
ObjectDataProvider odp = new ObjectDataProvider();
odp.ObjectInstance = new Calculator();
odp.MethodName = "Add";
odp.MethodParameters.Add("0");
odp.MethodParameters.Add("0");

//以ObjectDataProvider對象為Source創建Binding,Path是ObjectDataProvider對象MethodParameters屬性所引用的集合中的元素
//Binding對象只負責把從UI收集到的數據寫入其直接Source(即ObjectDataProvider對象)而不是被ObjectDataProvider對象包裝着的Calculator對象
//UpdataSourceTrigger屬性設置為一有更新立刻將值傳回Source
Binding bindingToArg1 = new Binding("MethodParameters[0]")
{
    Source = odp,
    BindsDirectlyToSource = true,
    UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
};

Binding bindingToArg2 = new Binding("MethodParameters[1]")
{
    Source = odp,
    BindsDirectlyToSource = true,
    UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
};

//以ObjectDataProvider對象為Source創建Binding,ObjectDataProvider對象本身就代表了數據,Path使用的是.而非其Data屬性
Binding bindingToResult = new Binding(".") { Source = odp };

//將Binding關聯到UI元素上
textBoxArg1.SetBinding(TextBox.TextProperty, bindingToArg1);
textBoxArg2.SetBinding(TextBox.TextProperty, bindingToArg2);
textBoxResult.SetBinding(TextBox.TextProperty, bindingToResult);

ObjectDataProvider類的作用是用來包裝一個以方法暴露數據的對象,這里先創建了一個ObjectDataProvider對象,然后用一個Calculator對象為其ObjectInstance屬性賦值,就把一個Calculator對象包裝在了ObjectDataProvider對象里。

還有另一種辦法創建被包裝的對象,告訴ObjectDataProvider將被包裝對象的類型和希望調用的構造器,讓ObjectDataProvider自己去創建被包裝對象,一般會在XAML代碼中使用這種指定類型和構造器的辦法。代碼如下:

//...
odp.ObjectType = typeof(YourClass);
odp.ConstructorParameters.Add(arg1);
odp.ConstructorParameters.Add(arg2);
//...

重載方法的區別在於參數列表,上面代碼中向MethodParameters屬性中加入了兩個string類型的對象,相當於告訴ObjectDataProvider對象去調用Calculator對象中具有兩個string類型參數的Add方法(MethodParameters屬性是類型敏感的)。

三個TextBox都以ObjectDataProvider對象為數據源,只是前兩個TextBox在Binding的數據流向上做了限制,原因如下:

  • ObjectDataProvider的MethodParameters不是依賴屬性,不能作為Binding的目標
  • 數據驅動UI的理念要求盡可能地使用數據對象作為Binding的Source而把UI元素當做Binding的Target

使用Binding的RelativeSource

有時候只知道Source對象與作為Binding目標的對象在UI布局上的相對關系,比如控件自己關聯自己的某個屬性、關聯自己某級容器的數據,此時要使用Binding的RelativeSource屬性。RelativeSource屬性的數據類型為RelativeSource類,通過這個類的幾個靜態或非靜態屬性可以控制它搜索相對數據源的方式。

RelativeSource類的非靜態屬性有3個:

  • AncestorLevel屬性:以Binding目標控件為起點的層級偏移量(如下面例子中d2的偏移量是1、g2的偏移量為2,依次類推)。
  • AncestorType屬性:告訴Binding尋找哪個類型的對象作為自己的源,不是這個類型的對象會被跳過。
  • Mode屬性:是RelativeSourceMode枚舉,取值有:PreviousData 、TemplatedParent 、Self 和FindAncestor。

RelativeSource類的靜態屬性3個:PreviousSelfTemplateParent,它們的類型是RelativeSource類。這3個靜態屬性是為了在XAML代碼里直接獲取RelativeSource實例,本質是創建一個RelativeSource實例、把實例的Mode屬性設置為相應的值,然后返回這個實例

RelativeSource類的源碼參考RelativeSource,靜態屬性的部分源碼如下:

public static RelativeSource PreviousData
{
    get
    {
        if (s_previousData == null)
        {
            s_previousData = new RelativeSource(RelativeSourceMode.PreviousData);
        }

        return s_previousData;
    }
}

下面舉例說明,在多層布局控件內放置一個TextBox,XAML代碼如下:

<Grid x:Name="g1" Background="Red" Margin="10">
    <DockPanel x:Name="d1" Background="Orange" Margin="10">
        <Grid x:Name="g2" Background="Yellow" Margin="10">
            <DockPanel x:Name="d2" Background="LawnGreen" Margin="10">
                <TextBox x:Name="textBox1" FontSize="24" Margin="10"/>
            </DockPanel>
        </Grid>
    </DockPanel>
</Grid>

從Binding的第一層依此向外找,找到第一個Grid類型對象后把它當做自己的源,C#代碼、等效XAML、效果截圖如下:

RelativeSource rs = new RelativeSource(RelativeSourceMode.FindAncestor);
rs.AncestorLevel = 1;
rs.AncestorType = typeof(Grid);
Binding binding = new Binding("Name") { RelativeSource = rs };
textBox1.SetBinding(TextBox.TextProperty, binding);
Text="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type Grid},AncestorLevel=1},Path=Name}"

從Binding的第一層依此向外找,找到第二個DockPanel類型對象后把它當做自己的源,C#代碼、等效XAML、效果截圖如下:

RelativeSource rs = new RelativeSource(RelativeSourceMode.FindAncestor);
rs.AncestorLevel = 2;
rs.AncestorType = typeof(DockPanel);
Binding binding = new Binding("Name") { RelativeSource = rs };
textBox1.SetBinding(TextBox.TextProperty, binding);
Text="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type DockPanel},AncestorLevel=2},Path=Name}"

TextBox關聯自身的Name屬性,C#代碼、等效XAML、效果截圖如下:

RelativeSource rs = new RelativeSource();
rs.Mode = RelativeSourceMode.Self;
Binding binding = new Binding("Name") { RelativeSource = rs };
textBox1.SetBinding(TextBox.TextProperty, binding);
<!--使用非靜態屬性-->
Text="{Binding RelativeSource={RelativeSource Mode=Self},Path=Name}"
<!--使用靜態屬性-->
Text="{Binding RelativeSource={ x:Static RelativeSource.Self} ,Path=Name}"

Binding對數據的轉換與校驗

Binding可以通過ValidationRules屬性對數據有效性進行校驗,通過Converter屬性為數據設置轉換器(兩端要求使用不同的數據類型時)。

Binding的數據校驗

Binding的ValidationRules屬性類型是Collection 可以為每個Binding設置多個數據校驗條件的,其中每一個條件是一個ValidationRule類型對象

ValidationRule類是個抽象類,在使用的時候需要創建它的派生類並實現它的Validate方法,對於Validate方法的返回值(ValidationResult類型對象):

  • 校驗通過,則把返回值的IsValid屬性設置為true。
  • 校驗失敗,則把返回值IsValid屬性設置為false並為ErrorContent屬性設置一個合適的消息內容(一般是個字符串)。

在UI上繪制一個TextBox和一個Slider,准備一個ValidationRule的派生類,代碼如下:

<StackPanel>
    <TextBox x:Name="textBox1" Margin="5"/>
    <Slider x:Name="slider1" Minimum="0" Maximum="100" Margin="5"/>
</StackPanel>
public class RangeValidationRule : ValidationRule
{
    //需要實現Validate方法
    public override ValidationResult Validate(object value, CultureInfo cultureInfo)
    {
        double d = 0;
        if (double.TryParse(value.ToString(), out d))
        {
            if (d >= 00 && d <= 100)
            {
                return new ValidationResult(true, null);
            }
        }

        return new ValidationResult(false, "Validation Failed");
    }
}

在后台C#代碼里使用Binding把Slider和TextBox關聯起來——以Slider為源、TextBox為目標。

只在Target被外部方法更新時校驗數據

在后台C#代碼里使用Binding以Slider為源、TextBox為目標關聯起來,Slider的取值范圍是0到100,需要校驗TextBox里輸入的值是不是在0到100這個范圍內,C#代碼如下:

//建立Binding
Binding binding = new Binding("Value") { Source = slider1 };
binding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
RangeValidationRule rvr = new RangeValidationRule();
binding.ValidationRules.Add(rvr);
textBox1.SetBinding(TextBox.TextProperty, binding);

當輸入0到100之間的值時程序正常顯示,區間之外的值或不能被解析的值時TextBox會顯示紅色邊框,錯誤值不會傳遞給Source,效果如下所示:

在Target被Source屬性更新時也校驗數據

Binding默認只在Target被外部方法更新時校驗數據,而來自Binding的Source屬性更新Target時是不會進行校驗的。當來自Source的數據也有可能出問題時,需要將校驗條件的ValidatesOnTargetUpdated屬性設置為true

把slider1的取值范圍由0到100改成-10到110,當Slider的滑塊移出有效范圍時,TextBox也會顯示校驗失敗的效果,代碼、效果如下:

<Slider x:Name="slider1" Minimum="-10" Maximum="110" Margin="5"/>
//建立Binding
Binding binding = new Binding("Value") { Source = slider1 };
binding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
RangeValidationRule rvr = new RangeValidationRule();
rvr.ValidatesOnTargetUpdated = true;
binding.ValidationRules.Add(rvr);
textBox1.SetBinding(TextBox.TextProperty, binding);

顯示校驗失敗的錯誤消息

當校驗錯誤的時候Validate方法返回的ValidationResult對象攜帶着一條錯誤消息,顯示錯誤消息需要用到后面才會詳細講解的知識——路由事件(Routed Event)

在創建Binding時要把Binding對象的NotifyOnValidationError屬性設為true。這樣,當數據校驗失敗的時候Binding會像報警器一樣發出一個信號,這個信號會以Binding對象的Target為起點在UI元素樹上傳播。信號每到達一個結點,如果這個結點上設置有對這種信號的偵聽器(事件處理器),那么這個偵聽器就會被觸發用以處理這個信號。信號處理完后,程序員還可以選擇是讓信號繼續向下傳播還是就此終止——這就是路由事件,信號在UI元素樹上的傳遞過程就稱為路由(Route)

代碼、效果如下:

//建立Binding
Binding binding = new Binding("Value") { Source = slider1 };
binding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
RangeValidationRule rvr = new RangeValidationRule();
rvr.ValidatesOnTargetUpdated = true;
binding.ValidationRules.Add(rvr);
binding.NotifyOnValidationError = true;
textBox1.SetBinding(TextBox.TextProperty, binding);
textBox1.AddHandler(Validation.ErrorEvent, new RoutedEventHandler(ValidationError));

//偵聽器
private void ValidationError(object sender, RoutedEventArgs e)
{
    if (Validation.GetErrors(textBox1).Count > 0)
    {
        textBox1.ToolTip = Validation.GetErrors(textBox1)[0].ErrorContent.ToString();
    }
}

Binding的數據轉換

Binding有一種機制稱為數據轉換(Data Convert),當Source端Path所關聯的數據與Target端目標屬性數據類型不一致時(如Slider的Value屬性是double類型值、TextBox的Text屬性是string類型值),可以添加數據轉換器(Data Converter)

double類型與string類型互相轉換處理起來比較簡單,WPF類庫自動替我們做了,但有些類型之間的轉換就不是WPF能替我們做的了,如下面這些情況:

  • Source里的數據是Y、N和X三個值(可能是char類型、string類型或自定義枚舉類型),UI上對應的是CheckBox控件,需要把這三個值映射為它的IsChecked屬性值(bool?類型)
  • 當TextBox里已經輸入了文字時用於登陸的Button才會出現,這是string類型與Visibility枚舉類型或bool類型之間的轉換(Binding的Mode將是OneWay)。
  • Source里的數據可能是Male或Female(string或枚舉),UI上對應的是用於顯示頭像的Image控件,這時候需要把Source里的值轉換成對應的頭像圖片URI(亦是OneWay)。

以上情況,只能自己動手寫Converter,方法是創建一個類並讓這個類實現IValueConverter接口,IValueConverter接口定義如下:

public interface IValueConverter
{
    object Convert(object value, Type targetType, object parameter, CultureInfo culture);

    object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture);
}

當數據從Binding的Source流向Target時,Converter方法將被調用;反之,ConvertBack方法將被調用。兩個方法的參數列表一模一樣:

  • 第一個參數為object,最大限度地保證了Converter的重用性(可以在方法體內對實際類型進行判斷);
  • 第二個參數用於確定方法的返回類型,可以理解為outputType避免與Binding的Target混淆
  • 第三個參數用於把額外的信息傳入方法,若需要傳遞多個信息則可把信息放入一個集合對象來傳入方法

Binding對象的Mode屬性為TwoWay或Default行為與TwoWay一致,則兩個方法都有可能被調用;Mode為OneWay或Default行為與OneWay一致則只有Convert方法會被調用,其他情況同理。

下面這個例子是一個Converter的綜合實例,程序的用途是在列表里向玩家顯示一些軍用飛機的狀態。
首先創建幾個自定義類型數據:

//種類
public enum Category
{
    Bomber,
    Fighter
}

//狀態
public enum State
{
    Available,
    Locked,
    Unknown
}

//飛機
public class Plane
{
    //Category屬性在UI里被映射為轟炸機或戰斗機的圖標,
    public Category Category { get; set; }
    public string Name { get; set; }
    //State屬性在UI里被映射為CheckBox
    public State State { get; set; }
}

轟炸機(Bomber.png)或戰斗機(Fighter.png)的圖標如下:

需要提供兩個Converter,一個是由Category類型單向轉換為string類型(XAML編譯器能夠把string對象解析為圖片資源),另一個是在State與bool?類型之間雙向轉換,代碼如下:

public class CategoryToSourceConverter : IValueConverter
{
    //將Category轉換為Uri
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        Category c = (Category)value;
        switch (c)
        {
            case Category.Bomber:
                return @"\Icons\Bomber.png";
            case Category.Fighter:
                return @"\Icons\Fighter.png";
            default:
                return null;
        }
    }

    //不會被調用
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

public class StateToNullableBoolConverter : IValueConverter
{
    //將State轉換為bool?
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        State s = (State)value;
        switch (s)
        {
            case State.Available:
                return true;
            case State.Locked:
                return false;
            case State.Unknown:
            default:
                return null;
        }
    }

    //將bool?轉換為State
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        bool? nb = (bool?)value;
        switch (nb)
        {
            case true:
                return State.Available;
            case false:
                return State.Locked;
            case null:
            default:
                return State.Unknown;
        }
    }
}

在XAML代碼中,以資源的形式創建了兩個Converter的實例,為listBoxPlane添加用於顯示數據的DataTemplate,代碼如下:

<Window.Resources>
    <local:CategoryToSourceConverter  x:Key="cts"/>
    <local:StateToNullableBoolConverter  x:Key="stnb"/>
</Window.Resources>

<StackPanel Background="LightBlue">
    <ListBox x:Name="listBoxPlane" Height="160" Margin="5">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <StackPanel Orientation="Horizontal">
                    <Image Width="40" Height="40" Source="{Binding Path=Category,Converter={StaticResource cts}}"/>
                    <TextBlock Text="{Binding Path=Name}" Width="60" Margin="80,0"/>
                    <CheckBox IsThreeState="True" IsChecked="{Binding Path=State,Converter={StaticResource stnb}}"/>
                </StackPanel>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
    <Button x:Name="buttonLoad" Content="Load" Height="25" Margin="5,0" Click="buttonLoad_Click"/>
    <Button x:Name="buttonSave" Content="Save" Height="25" Margin="5,0" Click="buttonSave_Click"/>
</StackPanel>

Load按鈕的Click事件處理器負責把一組飛機的數據賦值給ListBox的ItemsSource屬性,Save按鈕的Click事件處理器負責把用戶更改過的數據寫入文件,代碼如下:

//Load按鈕Click事件處理器
private void buttonLoad_Click(object sender, RoutedEventArgs e)
{
    List<Plane> planeList = new List<Plane>()
    {
        new Plane(){Category=Category.Bomber,Name="B-1",State=State.Unknown},
        new Plane(){Category=Category.Bomber,Name="B-2",State=State.Unknown},
        new Plane(){Category=Category.Fighter,Name="F-22",State=State.Unknown},
        new Plane(){Category=Category.Fighter,Name="Su-47",State=State.Unknown},
        new Plane(){Category=Category.Bomber,Name="B-52",State=State.Unknown},
        new Plane(){Category=Category.Fighter,Name="J-10",State=State.Unknown},
    };

    listBoxPlane.ItemsSource = planeList;
}

//Save按鈕Click事件處理器
private void buttonSave_Click(object sender, RoutedEventArgs e)
{
    StringBuilder sb = new StringBuilder();
    foreach (Plane p in listBoxPlane.Items)
    {
        sb.AppendLine(string.Format("Category={0},Name={1},State={2}", p.Category, p.Name, p.State));
    }
    File.WriteAllText(@"D:\PlaneList.txt", sb.ToString());
}

運行程序並單擊CheckBox更改飛機的State,效果如下:

單擊Save按鈕后打開D:\PlaneList.txt,數據如下:

Category=Bomber,Name=B-1,State=Locked
Category=Bomber,Name=B-2,State=Available
Category=Fighter,Name=F-22,State=Available
Category=Fighter,Name=Su-47,State=Unknown
Category=Bomber,Name=B-52,State=Unknown
Category=Fighter,Name=J-10,State=Unknown

MultiBinding(多路Binding)

當UI需要顯示的信息由不止一個數據來源決定時,需要使用MultiBinding,即多路Binding。MultiBinding與Binding一樣均以BindingBase為基類,凡是能使用Binding對象的場合都能使用MultiBinding。

MultiBinding具有一個名為Bindings的屬性(類型是Collection ),通過這個屬性MultiBinding把一組Binding對象聚合起來, 處在這個集合中的Binding對象可以擁有自己的數據校驗與轉換機制,它們匯集起來的數據將共同決定傳往MultiBinding目標的數據,示意圖如下所示:

有一個用於新用戶注冊的UI(包含4個TextBox和一個Button),還有如下一些限定:

  • 第一、二個TextBox輸入用戶名,要求內容一致。
  • 第三、四個TextBox輸入用戶E-Mail,要求內容一致。
  • 當TextBox的內容全部符合要求的時候,Button可用。

UI的XAML代碼如下:

<StackPanel Background="LightBlue">
    <TextBox x:Name="textBox1" Height="23" Margin="5"/>
    <TextBox x:Name="textBox2" Height="23" Margin="5,0"/>
    <TextBox x:Name="textBox3" Height="23" Margin="5"/>
    <TextBox x:Name="textBox4" Height="23" Margin="5,0"/>
    <Button x:Name="button1" Content="Sumbit" Width="80" Margin="5"/>
</StackPanel>

設置MultiBinding的代碼,實現Converter,代碼如下:

//准備基礎Binding
Binding b1 = new Binding("Text") { Source = textBox1 };
Binding b2 = new Binding("Text") { Source = textBox2 };
Binding b3 = new Binding("Text") { Source = textBox3 };
Binding b4 = new Binding("Text") { Source = textBox4 };
//准備MultiBinding
MultiBinding mb = new MultiBinding() { Mode = BindingMode.OneWay };
mb.Bindings.Add(b1); //注意:MultiBinding對於Add子Binding的順序是敏感的
mb.Bindings.Add(b2);
mb.Bindings.Add(b3);
mb.Bindings.Add(b4);
mb.Converter = new LogonMultiBindingConverter();
//將Button與MultiBinding對象關聯
button1.SetBinding(Button.IsEnabledProperty, mb);

//Converter
public class LogonMultiBindingConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        if (!values.Cast<string>().Any(text => string.IsNullOrEmpty(text))
            && values[0].ToString() == values[1].ToString()
            && values[2].ToString() == values[3].ToString())
        {
            return true;
        }
        return false;
    }

    //不會被調用
    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

需要注意以下幾點:

  • MultiBinding對於添加子級Binding的順序是敏感的,這個順序決定了匯集到Converter里數據的順序。
  • MultiBinding的Converter實現的是IMultiValueConverter接口

程序效果如下:

參考資料

WPF Binding
XPath 語法
XML Path Language (XPath)
RelativeSource
RelativeSourceMode 枚舉


免責聲明!

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



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