我們手動編寫代碼保證UI和數據同步。有效將兩組屬性隱式的綁定在一起, 一組來自Person對象,另一組來自顯示Person對象的控件。數據綁定用於顯式的 將屬性從一個對象綁定到另一個,保持它們的同步,並轉換為適當的類型,正如 圖4-7所示。
圖4-7
4.2.1 綁定
取代以在代碼中手動設置TextBox對象的Text屬性並保證它們是最新的,數據 綁定允許我們使用Binding對象的實例來設置Text屬性,正如示例4-8所示。
示例4-8
<TextBox >
<TextBox.Text>
<Binding Path="Age" />
</TextBox.Text>
</TextBox>
在示例4-8中,我們已經使用了在第一章介紹的屬性元素語法,創建了一個 Binding類的實例,初始化它的Path屬性為“Age”,而且將Binding對象設置為 TextBox對象的Text屬性值。使用綁定的標簽(也在第一章介紹過),我們可以 示例4-8簡寫為示例4-9。
示例4-9
<TextBox TextContent="{Binding Path=Age}" />
作為一個更短的刪節版,你可以一起省略指定Path,而且Binding也可以知道 這是什麼意思,正如示例4-10。
示例4-10
<TextBox TextContent="{Binding Age}" />
我更喜歡顯示的語法聲明,因此我不會使用示例4-10的語法,但我不做評價 ——如果你喜歡用的話
Binding類提供了各種各樣有趣的工具,用來管理屬性間的綁定,但是我們更關 心的是Path屬性。大多數場合,你可能認為Path作為一個對象的屬性名,是作為 數據源。因此,示例4-10的binding語句建立了一個TextBox的Text屬性和某個對 象的Name屬性之間的綁定,正如圖4-8所示。
圖4-8
在這個綁定中,TextBox控件是綁定目標,它扮演一個消費者的角色——當綁 定源——提供數據的對象有改變時。綁定目標可以是一個WPF元素,但是只允許 綁定到元素的依賴屬性。(在第9章介紹)
另一方面,你可以將任意公有的CLR屬性綁定到綁定源,綁定源在這個示例中 沒有具體命名,因此我們有一些自由度——關於這個對象在運行期來自哪裡,因 此也易於將多個控件綁定到同一個對象上(像name和age這兩個文本框,都綁定 到同一個Person對象)。
普遍地,綁定源數據來自數據上下文
4.2.2 隱式的數據上下文
數據上下文是綁定機制尋找數據源的地方如果沒有任何額外的特殊指令(我 們隨後要討論這些指令)。在WPF中,每個FrameworkElement和 FrameworkContentElement都有一個DataContext屬性,這個屬性是Object類型的 ,所以可以將任何對象放入其中,例如string,Person,List<Person>等 等。當尋找一個使用綁定源的對象時,綁定對象從它所定義的位置向上遍歷控件 樹,尋找一個非空的DataContext屬性。
這種遍歷是便捷的,因為這意味著在同一個父控件中,任意兩個控件都可以 綁定到同一個數據源。例如,我們的兩個文本框控件都是grid的子控件,而且每 一個都會在同一個數據上下文中搜索,如圖4-9所示。
圖4-9
工作步驟如下:
l 綁定機制在TextBox自身搜索非空DataContext屬性
l 綁定機制在Grid搜索非空DataContext屬性
l 綁定機制在Window搜索非空DataContext屬性
為這兩個文本框控件都提供一個非空DataContext,在Window1類的構造函數 中,設置共用的Person對象作為grid的DataContext屬性,正如示例4-11。
示例4-11
// Window1.xaml.cs
using System;
using System.Windows;
using System.Windows.Controls;
namespace PersonBinding {
public partial class Window1 : Window {
Person person = new Person("Tom", 9);
public Window1( ) {
InitializeComponent( );
// Let the grid know its data context
grid.DataContext = person;
this.birthdayButton.Click += birthdayButton_Click;
}
void birthdayButton_Click(object sender, RoutedEventArgs e) {
// Data binding keeps person and the text boxes synchronized
++person.Age;
MessageBox.Show(
string.Format(
"Happy Birthday, {0}, age {1}!",
person.Name,
person.Age),
"Birthday");
}
}
}
因此,為了我們的應用程序的功能性如圖4-9所示,數據同步的代碼減少到, 為每一個顯示數據的xaml屬性設置一個綁定對象,以及使用數據上下文為 Binding搜索數據。沒有必要初始化UI代碼或者事件句柄,來復制和轉換數據( 注意示例4-11中橢圓的不足)
清楚起見,實現INotifyPropertyChanged的用途絕非偶然。這是WPF數據綁定 引擎保持UI同步於對象屬性改變的接口。沒有這個接口,UI的改變仍然可以傳達 到對象,但是綁定引擎沒有辦法知道什麼時候改變UI。
一個沒有實現INotifyPropertyChanged接口的對象發生改變,綁定引擎沒有 辦法知道什麼時候改變UI——這種說法不是完全正確。一種可以知道的方法是, 如果一個對象實現了PropertyNameChanged事件——正如.NET 1.x中規定的數據 綁定(如SizeChanged,TextChanged等等)——而.NET是向後兼容的。另一種方 法是,手動調用BindingExpression對象的UpdateTarget方法,聯合赈災討論的 綁定機制。
然而,可靠的說,實現INotifyPropertyChanged是一種可取的方式,來支持 屬性改變通知,在WPF數據綁定中。
4.2.3可聲明的數據
當我們的程序還在嘗試去模仿更復雜的應用程序時,可能是從持久化形式加 載“私人數據”,以及將其保存在應用程序的Session中,不難想象這樣的場景 ,在編譯期就知道某些數據的位置。可能是示例數據(如前面示例中的Tom); 或者是眾所周知的數據,而且在Session中不會改變,如
應用程序默認設置或錯誤信息。很多應用程序有獨立於UI工作的字符串資源 ,但是仍然包裝在這個應用程序中——這樣做使之易於維護和本地化,將其脫離 與UI邏輯本身,降低了數據與UI的耦合。到目前為止,在我們的示例中,我們已 經將這些眾所周知的數據保存在代碼中,但是xaml是一個更好的選擇,不僅因為 易於在xaml中維護數據,還有xaml對本地化的支持(在第6章介紹)。
正如在第1章討論到的,xaml是一種描述對象圖表的語言,因此實際上任何帶 有默認構造函數的類型都可以在xaml中初始化,回憶示例4-2,Person類有一個 默認的構造函數,因此我們可以創建一個Person實例在我們的xaml應用程序中, 如示例4-12。
示例4-12
<?Mapping XmlNamespace="local" ClrNamespace="PersonBinding" ?>
<Window xmlns:local="local">
<Window.Resources>
<local:Person x:Key="Tom" Name="Tom" Age="9" />
</Window.Resources>
<Grid></Grid>
</Window>
我們在window標簽中的資源元素中創建了一些“數據島”,使用xaml的映射 語法(在第一章介紹),引進了Person類型。
通過在xaml中使用指定的Person標簽,我們能夠聲明性的設置grid的 DataContext屬性,而不是在後台代碼文件中以編程方式設置,如示例4-13。
示例4-13
<!-- Window1.xaml -->
<?Mapping XmlNamespace="local" ClrNamespace="PersonBinding" ? >
<Window xmlns:local="local">
<Window.Resources>
<local:Person x:Key="Tom" Name="Tom" Age="9" />
</Window.Resources>
<Grid DataContext="{StaticResource Tom}">
<TextBlock >Name:</TextBlock>
<TextBox Text="{Binding Path=Name}" />
<TextBlock >Age:</TextBlock>
<TextBox Text="{Binding Path=Age}" />
<Button x:Name="birthdayButton">Birthday</Button>
</Grid>
</Window>
現在,我們將Person對象的創建轉移到了xaml中,我們已經更新了Birthday 按鈕的Click事件句柄,從使用一個成員變量到使用定義在資源中的數據,正如 示例4-14所示。
示例4-14
public partial class Window1 : Window {
void birthdayButton_Click(object sender, RoutedEventArgs e) {
Person person = (Person)this.FindResource("Tom"));
++person.Age;
MessageBox.Show();
}
}
在示例4-14中,我們使用了FindResource方法(在第一章介紹過,會在第6章 詳細介紹),用來從主窗體的資源中拖出Person對象。通過最小的改動,結果
是再次呈現圖4-6所示的兩個窗體。唯一的不同是不必接觸後台代碼文件就可 以在編譯期維護或本地化已知數據。(第6章討論了本地化xaml資源)
4.2.4顯示的數據源
一旦你已經得到一個命名的資源,你可以在xaml中顯示的綁定對象源,而不 是依賴於隱式的綁定到控件樹上某處的一個非空的DataContext屬性。顯示的數 據源方式是有用的,如果你有多個數據源,例如,兩個Person對象。設置顯示的 數據源給綁定中的Source屬性,如示例4-15所示。
示例4-15
<!-- Window1.xaml -->
<Window >
<Window.Resources>
<local:Person x:Key="Tom" />
<local:Person x:Key="John" />
</Window.Resources>
<Grid>
<TextBox x:Name="tomTextBox"
Text="
{Binding
Path=Name,
Source={StaticResource Tom}}" />
<TextBox x:Name="johnTextBox"
Text="
{Binding
Path=Name,
Source={StaticResource John}}" />
</Grid>
</Window>
在示例4-14中,,我們綁定了兩個文本框到兩個Person對象,使用Binding對 象的Source屬性,顯示的綁定到每個Person對象。
隱式綁定和顯示綁定的對比
通常說,當我們在多個控件中共享數據時,我發現隱式的綁定方式最有用, 因為所有需要的是一點代碼,用來在一個單獨的父對象上設置DataContext屬性 。另一方面,如果我已經得到多個數據源,我真的喜歡使用Source屬性在我們 Binding對象上,從而使數據來源更加清晰。
4.2.5數值轉換
目前為止,我們的應用程序示例展示了綁定數據到文本框的文字。然而,這 並沒有完全阻止你綁定到控件的其他屬性,例如Foreground,FontWeight, Height等等。舉例來說,我們可能判定任何大於25歲的都是無所顧慮的,因此應 該在UI中標記為紅色(或者,它們是瀕臨滅絕的。無論哪一個都使你更加可能推 薦這本書給你的朋友)。當某人的年齡隨著點擊Birthday按鈕而增長,我們想要 保持UI是最新的,這意味著我們已經得到了數據綁定的完美候選者。想象下面示 例4-16的能力。
示例4-16
<!-- Window1.xaml -->
<Window >
<Window.Resources>
<local:Person x:Key="Tom" />
</Window.Resources>
<Grid>
<TextBox
Text="{Binding Path=Age}"
Foreground="{Binding Path=Age, }"
/>
</Grid>
</Window>
在示例4-16中,我們將age文本框的Text屬性綁定到Person對象的Age屬性, 正如我們已經看到的,但是我們也已經將文本框的Foreground屬性綁定到了 Person對象的同一個Age屬性。隨著Tom年齡的改變,我們想要更新文本框的前景 色。然而,由於Age是Int32類型的而Foreground是Brush類型的,這就需要一個 從Int32到Brush的映射,從而應用數據幫定從Age到Foreground。這就是值轉換 器的工作。
一個值轉換器(或者簡稱為“轉換器”),是IValueConverter接口的一種實 現,其中有兩個方法:Convert和ConvertBack。Convert方法用於將源數據轉換 為UI目標數據,如從Int32到Brush;ConvertBack方法用於將UI數據轉換回源數 據。在這兩種情形中,dangqian值和希望轉換的目標類型都需要傳入方法中。
為了將一個Age屬性的Int32類型轉換為Foreground屬性的Brush,我們可以在 Convert方法中,按照我們滿意的方式,實現無論哪一種映射。如圖4-17
示例4-17
public class AgeToForegroundConverter : IValueConverter {
// Called when converting the Age to a Foreground brush
public object Convert(object value, Type targetType, ) {
Debug.Assert(targetType == typeof(Brush));
// DANGER! After 25, it's all down hill
int age = int.Parse(value.ToString( ));
return (age > 25 ? Brushes.Red : Brushes.Black);
}
// Called when converting a Foreground brush back to an Age
public object ConvertBack(object value, ) {
// should never be called
throw new NotImplementedException( );
}
}
在示例4-17中,我們已經實現了Convert方法,以再次確認我們尋找到一個 Foreground筆刷,並以不同的年齡分發適當的筆刷。既然我們沒有提供任何工具 來改變用來顯示年齡的Foreground筆刷,就沒有理由實現ConvertBack方法了。
我選擇AgeToForegroundConverter作為類的名稱,是因為我有特殊的意圖: 我在我的轉換器中生成了將Int32裝換為Brush的簡單轉換。即使這個轉換器可以 被嵌入到任何地方用來將Int32裝換為Brush,我也可能有非常不一樣的需求對於 HeightToBackgroundConverter而言,僅僅作為一個示例。
一旦你得到一個轉換器類,這將易於在xaml中創建一個實例,就像我們剛剛 對Person對象所做的,如示例4-18所示。
示例4-18
<!-- Window1.xaml -->
<?Mapping XmlNamespace="local" ClrNamespace="PersonBinding" ? >
<Window xmlns:local="local">
<Window.Resources>
<local:Person x:Key="Tom" />
<local:AgeToForegroundConverter
x:Key="AgeToForegroundConverter" />
</Window.Resources>
<Grid DataContext="{StaticResource Tom}">
<TextBox
Text="{Binding Path=Age}"
Foreground="
{Binding
Path=Age,
Converter={StaticResource AgeToForegroundConverter}}"
/>
</Grid>
</Window>
在示例4-18中,一旦我們在xaml中有了一個命名的轉換器對象,我們確定它 作為Age屬性和Foreground筆刷的裝換器,通過設置綁定對象的Converter屬性。 圖4-10顯示了這一轉換的結果。
圖4-10
在圖4-10中,注意到隨著Tom的年齡從開端增長,轉換器轉換筆刷的前景色從 黑到紅。當數據改變時這個改變迅速發生,並沒有顯示的代碼強迫使之執行,正 如使用其他種類的數據綁定一樣。