在我看來數據綁定是XAML類程序(WPF,Silverlight,WP7,Windows8)最最關鍵的概念,也是MVVM模式的基礎,如果一位開發者在開發XAML類程序時並沒有用上數據綁定,那么我覺得他/她還沒有掌握開發XAML類程序的能力。數據綁定可以說每個XAML類程序開發者必須掌握的基本技能之一。下面是《數據綁定》樣章的一部分,如果覺得翻譯的不錯而且條件許可,請購買正版書,謝謝您為中國文化事業的貢獻。如果覺得有問題請指出,我們會總結堪錯列表,謝謝!
---------------------------------------------------------------以下是《數據綁定》樣章的一部分-------------------------------------------------------------
假設你想讓用戶與Slider控件進行交互,而且你也想與ColorScroll程序一樣,通過TextBlock顯示Slider當前的值。其實很簡單。只要為Slider控件的ValueChanged事件創建一個處理程序就可以了,當調用處理程序的時候,從Slider取出Value屬性的值並將其轉換成字符串,然后把該字符串設置給TextBlock的Text屬性。
像這樣的任務非常普遍,因此Silverlight提供了一種簡便的機制來實現這些任務。這種機制稱為數據綁定(data binding),或者就簡稱為綁定(binding)。數據綁定是一個對象的一個屬性與另外一個對象的一個屬性之間的一條鏈接(link),因此在綁定的情況下當一個屬性發生改變時,另一個屬性也隨之更新。綁定可以是雙向的(bidirectional),在這種情況下,其中一個屬性發生的變化會引起另一個屬性也隨之發生變化。
從本質上說,數據綁定可能像你所期望的那樣:由於注冊了一個事件處理程序,使得一個屬性從另外一個屬性中獲取更新,期間可能進行了一些數據轉換。通常你可以完全通過XAML來定義數據綁定,這意味着你不必編寫任何代碼。從語法上看,這好像不需要移動任何部件就能傳輸數據了。
演示數據綁定最簡單的方法是使用兩個可視化元素,例如Slider和TextBlock元素,我也從這兩個元素開始。但是,如果把可視化元素和基礎數據源進行綁定的話,更能體現出數據綁定的強大威力。
本章的目標是避免在代碼隱藏文件中顯式地使用事件處理程序,但是在本章的結束部分我不得不使用幾個事件處理程序。當然,我們還是需要一些其他代碼來支持XAML中的數據綁定,但這些代碼中的大部分可以恰當地歸類為業務對象(business object),而不是用戶界面元素。
綁定源與目標
在典型的數據綁定中,一個對象的屬性發生變化時,另一個對象的屬性也隨之自動更新。提供數據的對象,例如Slider,被認為是數據綁定的源(source);接收數據的對象(如TextBlock)是綁定的目標(target)。
通常給數據綁定源指定一個名字:
<Slider Name="slider" .../>
你可以把目標屬性作為一個屬性元素 並賦值給類型為Binding的對象:
<TextBlock ...> <TextBlock.Text> <Binding ElementName="slider" Path="Value" /> </TextBlock.Text> </TextBlock>
使用ElementName屬性指定源元素的名稱;使用Path屬性指定源屬性的名稱,在這個例子中Path是Slider的Value屬性。有時候把這種類型的綁定稱為元素名稱綁定,因為綁定源是一個可視化元素,並通過名稱來引用。
為了使語法變得更加友好,Silverlight為Binding提供了一個標記擴展(markup extension),在此,所有的東西都定義在一對花括號里面。(這是Silverlight for Windows Phone的幾個標記擴展中的一個。第7章介紹過StaticResource,第16章將介紹TemplateBinding。)這里是更精簡的語法:
<TextBlock ... Text="{Binding ElementName=slider, Path=Value}" ... />
請注意,ElementName和Path的設置用一個逗號分隔,而slider和Value名稱的引號已經去掉了。引號永遠都不會出現在標記擴展的大括號中。
SliderBindings程序使用了這樣的綁定方式,你可以試驗一下,嘗試做一些修改。這一切都在XAML文件里面:
Silverlight項目:SliderBindings 文件:MainPage.xaml(節選) <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"> <Grid.RowDefinitions> <RowDefinition Height="*" /> <RowDefinition Height="*" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <Slider Name="slider" Value="90" Grid.Row="0" Maximum="180" Margin="24" /> <TextBlock Name="txtblk" Text="{Binding ElementName=slider, Path=Value}" Grid.Row="1" FontSize="48" HorizontalAlignment="Center" VerticalAlignment="Center" /> <Rectangle Grid.Row="2" Width="{Binding ElementName=slider, Path=Value}" RenderTransformOrigin="0.5 0.5" Fill="Blue"> <Rectangle.RenderTransform> <RotateTransform x:Name="rotate" Angle="90" /> </Rectangle.RenderTransform> </Rectangle> </Grid>
這個頁面包含了一個范圍從0到180的Slider,以及一個TextBlock,TextBlock的Text屬性綁定到Slider的Value屬性上,另外還有一個Rectangle,它的Width屬性也綁定到Slider相同的Value屬性上。Rectangle還有一個RotateTransform屬性,這個屬性使得Rectangle元素旋轉了90°。
操作Slider時,TextBlock顯示Slider的值,而Rectangle的高度也隨之變大或者變小。(Binding的目標是Rectangle的Width屬性,而Rectangle的轉角是90°。)
在Binding擴展標記中,屬性的順序無關緊要。你可以把Path屬性放在前面:
<TextBlock ... Text="{Binding Path=Value, ElementName=slider}"
事實上,如果路徑出現第一個位置上,可以刪除“Path=”部分,只是使用屬性名:
<TextBlock ... Text="{Binding Value, ElementName=slider}"
在本章的后面以及隨后的章節中,我將使用這種縮略形式的語法,但對於元素名稱綁定,我卻不喜歡這樣做,因為這樣做我就無法知道綁定底層的工作原理。Binding類首先需要在可視化樹中找到一個名為slider的元素,然后它需要使用反射來找到該元素的Value屬性。我偏向於下面這樣的語法,這種語法按照內部操作的過程來排列屬性的順序:
<TextBlock ... Text="{Binding ElementName=slider, Path=Value}"
為什么Binding的這個屬性叫做Path而不是Property呢?畢竟,Style類就有一個叫做Property的屬性。為什么Binding卻沒有呢?
簡單的答案是Path能把多個屬性名組合在一起使用。例如,假設Slider沒有名稱。如果你知道該Slider是ContentPanel元素的Children集合的第一個子元素,你可以間接地引用Slider:
Text="{Binding ElementName=ContentPanel, Path=Children[0].Value}"
或者,使用可視化樹更上一層的元素:
Text="{Binding ElementName=LayoutRoot, Path=Children[1].Children[0].Value}"
Path的組成部件必須是通過點號連接的屬性或者索引器(indexer)。
Target和Mode
綁定包含一個源和一個目標。綁定目標被認為是綁定要設置的屬性,該屬性必須始終都是一個依賴屬性,永遠都是。當你在代碼中創建綁定的時候,這一限制非常明顯。
修改一下SliderBindings程序,刪除TextBlock的Text屬性上的綁定。在MainPage.xaml.cs文件中,你需要添加using指令來引用System.Windows.Data命名空間,這個命名空間包含了Binding類。在構造函數里面,調用完InitializeComponent函數之后,生成一個類型為Binding的對象,並設置它的屬性:
Binding binding = new Binding(); binding.ElementName = "slider"; binding.Path = new PropertyPath("Value");
ElementName和Path屬性是綁定源。下面看看將TextBlock的Text屬性作為綁定目標的代碼:
txtblk.SetBinding(TextBlock.TextProperty, binding);
SetBinding方法定義在FrameworkElement里面,第一個參數是依賴屬性,也就是目標屬性。該目標也是調用SetBinding方法的元素。你也可以使用其他替代方案,使用靜態方法BindingOperations.SetBinding來綁定目標:
BindingOperations.SetBinding(txtblk, TextBlock.TextProperty, binding);
但你仍然需要依賴屬性。因此,這就是可視化對象的屬性應該是依賴屬性的另一個原因。你不僅可以為這些屬性定制樣式,而且可以把它們制作成動畫,但所有這些數據綁定的目標必須是依賴屬性。
就依賴屬性的優先級而論,數據綁定與本地設置的級別相同。
使用BindingOperations.SetBinding方法意味着你可以在任何依賴屬性上設置綁定。對於Silverlight for Windows Phone來說,事實並非如此。在Windows Phone中綁定的目標必須是FrameworkElement的屬性。
例如,你會發現在MainPage.xaml中的Rectangle元素包含了RotateTransform屬性,該屬性設置為一個RotateTransform對象。嘗試把TextBlock的Text屬性和Rectangle的Width屬性上的綁定也應用到Angle屬性上:
<RotateTransform x:Name="rotate" Angle="{Binding ElementName=slider, Path=Value}" />
這看起來好像沒問題,但卻不能正常工作。你會在運行時得到一個XamlParseException異常。Angle本身是依賴屬性,不是條件充足了嗎?但是RotateTransform並不是派生自FrameworkElement,所以它不可以作為綁定的目標。(在Silverlight 4中,應用於RotateTransform的Angle屬性的綁定可以正常工作。但是Silverlight for Windows Phone大體上還是Silverlight 3 。)
如果想要這么做,你需要刪除RotateTransform的Angle屬性上的綁定,以及已添加到MainPage.xaml.cs的所有代碼。把Slider的Value屬性的值初始化為90:
<Slider Name="slider" Value="90" ... />
綁定的目標是TextBlock的Text屬性:
<TextBlock Name="txtblk" Text="{Binding ElementName=slider, Path=Value}" ... />
讓我們切換一下,把TextBlock的Text屬性初始化為90:
<TextBlock Name="txtblk" Text="90" .../>
然后把Slider的Value屬性作為綁定目標:
<Slider Name="slider" Value="{Binding ElementName=txtblk, Path=Text}" .../>
乍一看這似乎能正常地工作。Slider的滾動塊最初放在中間,這表示Slider的值為90,該值從TextBlock獲取,而Rectangle的大小仍然與Slider綁定。然而,當你滑動Slider時,Rectangle的高度改變了,但TextBlock卻沒有發生任何變化。Slider上的Binding對象正等待着TextBlock的Text屬性發生變化,但Text屬性沒有任何的改變。
現在為Slider上的綁定添加Mode設置,下面的代碼表示這個數據綁定是雙向(two-way)的。
<Slider Name="slider" Value="{Binding ElementName=txtblk, Path=Text, Mode=TwoWay}" .../>
現在能正常工作了!綁定的目標仍然是Slider的Value屬性。TextBlock的Text屬性變化時會影響Slider的Value屬性,同樣,現在Slider的Value屬性變化時也反過來影響到TextBlock。
Mode屬性的值為BindingMode枚舉類型的成員。Mode屬性的默認值是BindingMode. OneWay,除此之外還有BindingMode.TwoWay和BindingMode.OneTime,BindingMode. OneTime表示源只傳輸一次數據到目標。
使用同樣的技巧,可以為RotateTransform的Angle屬性建立起綁定關系。首先,把TextBlock上的綁定還原到原始狀態:
<TextBlock Name="txtblk" Text="{Binding ElementName=slider, Path=Value}" .../>
現在為Slider設置雙向綁定,指向RotateTransform的Angle屬性:
<Slider Name="slider" Value="{Binding ElementName=rotate, Path=Angle, Mode=TwoWay}" .../>
運行得很好!當滑動Slider時,Rectangle元素也相應地旋轉,如圖12-1所示。
綁定轉換器
當你運行SliderBindings程序時(或者當你看到該截圖的時候會感到驚訝),開始的時候你可能看到TextBlock顯示Slider的值有時是整數,有時是包含一或兩個小數位的浮點數,但更多的時候看到的是15位的雙精度浮點數。
有辦法解決這個問題嗎?
有,Binding類的其中一個叫做Converter的屬性,這個屬性的作用是引用一個可以在從源到目標的過程中進行數據轉換的類,(如果有需要)也可以從目標轉換回源。顯然,我們已經使用了一些隱式的數據轉換,例如數字轉換為字符串,或者字符串轉換為數字。但是,我們可以提供一些更明確的手段來協助轉換的過程。
Binding類Converter屬性是IValueConverter類型,IValueConverter是一個接口,它包含了兩個方法:Convert和ConvertBack。Convert方法處理從源到目標的數據轉換,而ConvertBack方法處理TwoWay(雙向)綁定時另外一個方向的轉換。
如果你的轉換類從來不使用在雙向綁定上,那么在ConvertBack方法中簡單地返回null就可以了。
為了給SliderBindings程序添加一個簡單的轉換器,在項目中增加一個名為Truncation- Converter的類。其實這個類已經在項目中,如下所示:
Silverlight項目:SliderBindings 文件:TruncationConverter.cs using System; using System.Globalization; using System.Windows.Data; namespace SliderBindings { public class TruncationConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { if (value is double) return Math.Round((double)value); return value; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { return value; } } }
Convert方法的value參數是從源傳遞到目標的對象。這個方法檢查value是否為double類型。如果是double,那么顯式地調用Math.Round方法把value轉換成double。
如果需要在MainPage.xaml引用這個類,你需要使用XML命名空間聲明:
xmlns:local="clr-namespace:SliderBindings"
然后把TruncationConverter類作為資源:
<phone:PhoneApplicationPage.Resources> <local:TruncationConverter x:Key="truncate" /> ... </phone:PhoneApplicationPage.Resources>
你會發現這些已經存在於SliderBindings項目的MainPage.xaml文件中了。
然后Binding擴展標記就可以引用這個資源:
<TextBlock Name="txtblk" Text="{Binding ElementName=slider, Path=Value,
我把擴展標記分成三行使各個部件更加清晰可見。注意StaticResource也是一個擴展標記,它嵌套在第一個標記擴展中,因此整個表達式包含了兩個花括號。
現在TextBlock顯示的數字被截斷了,如圖12-2所示。
記住,把轉換器定義為StaticResource。很多時候,我可能不由自主地將Binding的Converter屬性設置為靜態資源的鍵名:
<! — 這是錯誤的 !--> <TextBlock Name="txtblk" Text="{Binding ElementName=slider, Path=Value, Converter=truncate}" ... />
我自己經常這樣做,但是這種問題卻難以追蹤。
使用轉換器最常用的方法是把轉換器定義為資源,但是這不是唯一的方法。如果你使用Binding的元素語法,可以直接把TrunctionConverter類嵌入到標記中:
<TextBlock ... > <TextBlock.Text> <Binding ElementName="slider" Path="Value"> <Binding.Converter> <local:TruncationConverter /> </Binding.Converter> </Binding> </TextBlock.Text> </TextBlock>
不過,如果你在一個XAML文件中多次使用相同的轉換器,最好還是把它定義為資源,這樣能共享唯一實例。
TrucationConverter實際上是一個糟糕的數據轉換器。當然它能完成它應該做的工作,但完成的方式並不靈活。如果你要在轉換器類中調用Math.Round方法,那么提供一個小數位舍入功能不是更好嗎?試想一下,如果有一個不僅支持數字,而且還支持不同的格式和各種數據類型的功能,那豈不是更好?
這種魔法般的功能由Petzold.Phone.Silverlight庫里面的StringFormatConverter類所提供:
Silverlight項目:Petzold.Phone.Silverlight 文件:StringFormatConverter.cs using System; using System.Globalization; using System.Windows.Data; namespace Petzold.Phone.Silverlight { public class StringFormatConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { if (targetType == typeof(string) && parameter is string) return String.Format(parameter as string, value); return value; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { return value; } } }
除了Converter屬性以外,Binding類包含一個叫做ConverterParameter的屬性。該屬性的值是調用Convert時傳遞給parameter的參數。Convert方法假設該parameter是.NET標准的格式化字符串,該字符串可用來調用String.Format。
為了在SliderBindings程序中使用這個轉換器,你需要引用Petzold.Phone.Silverlight庫。(這里已經引用了。)並在文件中添加XML命名空間聲明:
xmlns:petzold="clr-namespace:Petzold.Phone.Silverlight;assembly=Petzold.Phone.Silv-erlight"
下面的代碼用於實例化頁面中Resources集合里的StringFormatConverter:
<phone:PhoneApplicationPage.Resources> ... <petzold:StringFormatConverter x:Key="stringFormat" /> </phone:PhoneApplicationPage.Resources>
現在你可以在Binding標記表達式中引用這個轉換器了。將ConverterParameter設置為.NET格式化字符串的一個占位符(Placeholder)的:
Text="{Binding ElementName=slider,
Path=Value,
Converter={StaticResource stringFormat},
ConverterParameter=...}"
當你輸入.NET格式化字符串時,你會發現一個問題。標准.NET的格式化字符串需要使用大括號,但你也知道當XAML解析器(XAML parser)解碼Binding標記表達式時,並不會喜歡非法嵌入的大括號。
簡單的解決方法是把ConverterParameter的值用單引號括起來:
Text="{Binding ElementName=slider,
Path=Value,
Converter={StaticResource stringFormat},
ConverterParameter='{0:F2}'}"
Visual Studio中的XAML解析器和可視化設計器(visual designer)不喜歡這種特定的語法,但是運行時卻沒有問題。如果你想讓設計器接受這種語法,在第一個單引號后插入一個空格(或者其他字符)就可以了。
因為ConverterParameter是String.Format調用的第一個參數,你可以把它完善一下:
Text="{Binding ElementName=slider,
Path=Value,
Converter={StaticResource stringFormat},
ConverterParameter='The slider is {0:F2}'}"
結果如圖12-3所示。
相對綁定源
根據綁定數據源來分類,Silverlight for Windows Phone支持三種不同的綁定類型。到目前為止,這一章已經介紹了ElementName綁定,這種綁定指向一個命名元素。在本章后面的部分,主要使用Source屬性來代替ElementName屬性指向數據源。
第3類綁定稱為RelativeSource(相對綁定源)。在Windows Presentation Foundation中的RelativeSource比Silverlight中的RelativeSource靈活很多,所以你可能對這個選項沒有深刻的印象。使用RelativeSource的一個目的是與模板相關聯,你將在第16章看到這樣的應用。最后一個選項叫做Self,使用Self可以定義指向自身元素屬性的綁定。下面的程序演示了這種語法:
Silverlight項目:BindToSelf 文件:MainPage.xaml(節選) <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"> <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center"> <TextBlock Text="{Binding RelativeSource={RelativeSource Self}, Path=FontFamily}" /> <TextBlock Text=" - " /> <TextBlock Text="{Binding RelativeSource={RelativeSource Self}, Path=FontSize}" /> <TextBlock Text=" pixels" /> </StackPanel> </Grid>
RelativeSource屬性是另一個擴展標記,其中包含RelativeSource和Self。Path指向同一個元素的另一個屬性。在這個例子中,這兩個TextBlock元素分別顯示自身的TextBlock的FontFamily和FontSize屬性。
this綁定源
也許你的應用程序有這樣一個需求:需要顯示很多簡短的文本字符串,這些字符串都由邊界所包圍。你決定創建一個派生自UserControl類的控件,並將其命名為BorderedText,如下所示:
<petzold:BorderedText Text="Ta Da!" FontFamily="Times New Roman" FontSize="96" FontStyle="Italic" FontWeight="Bold" TextDecorations="Underline" Foreground="Red" Background="Lime" BorderBrush="Blue" BorderThickness="8" CornerRadius="36" Padding="16 4" HorizontalAlignment="Center" VerticalAlignment="Center" />
從XML命名空間的前綴可以判斷出,這個類也在Petzold.Phone.Silverlight庫中。
BorderedText派生自UserControl,而UserControl繼承自Control,所以我們知道通過類繼承BorderedText具備Control類的一些屬性。BorderedText需要自己定義Text、TextDecorations、CornerRadius等屬性,以及一些使其變得更加靈活的其他屬性。
BorderedText.xaml文件中的可視化樹很可能包含了一個Border,並且Border里包含了一個TextBlock。TextBlock和Border的屬性都是通過BorderedText的屬性來設置。
在前面的章節中,你已經見過實現這種功能的方法:ColorColumn類定義了Label和Value屬性,然后在代碼中使用屬性變更處理程序為可視化樹中元素的屬性設置新值。使用數據綁定能簡化這項工作。
BorderedText的代碼隱藏文件定義了一些屬性,其父類Control類並沒有提供這些虛屬性供其子類使用:
Silverlight項目:Petzold.Phone.Silverlight 文件:BorderedText.xaml.cs using System; using System.Windows; using System.Windows.Controls; namespace Petzold.Phone.Silverlight { public partial class BorderedText : UserControl { public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(BorderedText), new PropertyMetadata(null)); public static readonly DependencyProperty TextAlignmentProperty = DependencyProperty.Register("TextAlignment", typeof(TextAlignment), typeof(BorderedText), new PropertyMetadata(TextAlignment.Left)); public static readonly DependencyProperty TextDecorationsProperty = DependencyProperty.Register("TextDecorations", typeof(TextDecorationCollection), typeof(BorderedText), new PropertyMetadata(null)); public static readonly DependencyProperty TextWrappingProperty = DependencyProperty.Register("TextWrapping", typeof(TextWrapping), typeof(BorderedText), new PropertyMetadata(TextWrapping.NoWrap)); public static readonly DependencyProperty CornerRadiusProperty = DependencyProperty.Register("CornerRadius", typeof(CornerRadius), typeof(BorderedText), new PropertyMetadata(new CornerRadius())); public BorderedText() { InitializeComponent(); } public string Text { set { SetValue(TextProperty, value); } get { return (string)GetValue(TextProperty); } } public TextAlignment TextAlignment { set { SetValue(TextAlignmentProperty, value); } get { return (TextAlignment)GetValue(TextAlignmentProperty); } } public TextDecorationCollection TextDecorations { set { SetValue(TextDecorationsProperty, value); } get { return (TextDecorationCollection)GetValue(TextDecorationsProperty); } } public TextWrapping TextWrapping { set { SetValue(TextWrappingProperty, value); } get { return (TextWrapping)GetValue(TextWrappingProperty); } } public CornerRadius CornerRadius { set { SetValue(CornerRadiusProperty, value); } get { return (CornerRadius)GetValue(CornerRadiusProperty); } } } }
這些代碼有點長但其實很簡單,因為都是一些屬性的定義。並沒有使用任何屬性變更處理程序。下面是包含了Border和TextBlock的XAML文件:
Silverlight項目:Petzold.Phone.Silverlight 文件:BorderedText.xaml <UserControl x:Class="Petzold.Phone.Silverlight.BorderedText" 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" Name="this"> <Border Background="{Binding ElementName=this, Path=Background}" BorderBrush="{Binding ElementName=this, Path=BorderBrush}" BorderThickness="{Binding ElementName=this, Path=BorderThickness}" CornerRadius="{Binding ElementName=this, Path=CornerRadius}" Padding="{Binding ElementName=this, Path=Padding}"> <TextBlock Text="{Binding ElementName=this, Path=Text}" TextAlignment="{Binding ElementName=this, Path=TextAlignment}" TextDecorations="{Binding ElementName=this, Path=TextDecorations}" TextWrapping="{Binding ElementName=this, Path=TextWrapping}" /> </Border> </UserControl>
請注意根元素的命名:
Name="this"
你可以把這個根元素設置成你想要的名稱,但使用C#的關鍵字this是最常見的做法,因為在該XAML文件中,this是指BorderedText類當前的實例,因此可以使用this這一熟悉的概念。你可以通過this這一名字把BorderedText的屬性與可視化樹中元素的屬性建立綁定關系。
這個文件不需要為Foreground屬性或者一些其他字體相關的屬性進行數據綁定,因為這些屬性都可以從可視化樹中繼承過來。TextBlock的Inlines屬性使我感覺不爽。這是因為TextBlock把Inlines屬性定義為只讀(get-only)屬性,所以沒辦法為它定義數據綁定。
BorderedTextDemo程序測試這個新控件:
Silverlight項目:BorderedTextDemo 文件:MainPage.xaml(節選) <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"> <petzold:BorderedText Text="Ta Da!" FontFamily="Times New Roman" FontSize="96" FontStyle="Italic" FontWeight="Bold" TextDecorations="Underline" Foreground="Red" Background="Lime" BorderBrush="Blue" BorderThickness="8" CornerRadius="36" Padding="16 4" HorizontalAlignment="Center" VerticalAlignment="Center" /> </Grid>
通知機制
為了使數據綁定能正常運作,綁定源必須實現某種通知機制(notification mechanism)。當屬性的值發生改變的時候,這種通知機制會發送通知信號,使得新的值可以從源傳遞到目標。當你綁定Slider的Value屬性到TextBlock的Text屬性時,同時使用到兩個依賴屬性。雖然你在公共的編程接口中看不出來,但是就是這些依賴屬性提供了通知機制。
使用數據綁定可以非常方便地關聯兩個可視化元素,但以可視化元素作為綁定目標,使用業務對象(business object)來代替可視化元素作為綁定源的數據綁定功能最強大。
這里需要提醒一下:
有時候,當程序員學習了操作系統的一個全新且重要的功能時(例如我在前面討論過的依賴屬性),他們就覺得需要在各個地方使用該功能,也許這樣做只是為了多練習一下。對於依賴特性來說,這種做法不太可取。當然,如果你的父類的父類已經是DependencyObject的派生類,那么可以使用依賴屬性,否則不應該單純地為了使用依賴屬性而繼承DependencyObject類。
換句話說:不要為了使用依賴屬性而重寫業務對象!
數據綁定的目標必須是依賴屬性,但是對於綁定的源,並沒有嚴格要求。綁定源可以是普通類的普通屬性。但是如果你希望綁定源在變化的時候,綁定目標也隨之自動更新,那么綁定源必須實現某種通知機制。
通常用作綁定源的業務對象需要實現的通知機制稱為INotifyPropertyChanged接口。
INotifyPropertyChanged定義在System.ComponentModel命名空間。(這清楚地表明該接口不僅僅在Silverlight中,而且在.NET中扮演着非常重要的角色。)這就是業務對象提供數據變更通知的方法。
INotifyPropertyChanged的定義非常簡單,如下所示:
public interface INotifyPropertyChanged { event PropertyChangedEventHandler PropertyChanged; }
實現INotifyPropertyChanged接口的類只需要簡單地定義一個公共事件Property- Changed。理論上,這個派生類並不需要為這個事件做任何特殊的處理,但是當它的某個屬性發生變化時,可以通過這個事件來觸發變更事件。
PropertyChangedEventHandler委托與PropertyChangedEventArgs類相關聯,Pro- pertyChangedEventArgs類只有一個string類型的只讀屬性PropertyName,你要將發生變化的屬性名傳遞給PropertyChangedEventArgs的構造函數。
有時候實現INotifyPropertyChanged接口的類需要定義一個受保護的虛方法OnPropertyChanged,該方法具有一個PropertyChangedEventArgs類型的參數。這個方法不是必須的,但能給派生類帶來便利。我在這個例子中使用了該方法,因為在該方法中可以方便觸發事件。
由於實現了INotifyPropertyChanged接口的業務對象並沒有繼承FrameworkElement,它們並不是XAML文件中可視化樹的組成部分,因此它們通常會被實例化為XAML的資源或者位於代碼隱藏文件中。
簡單的綁定服務
有時我覺得使用業務對象的本意是在XAML文件中進行綁定,這種綁定稱為綁定服務(binding server)。綁定服務公開了一些公共屬性,當這些屬性發生變化時,綁定服務會觸發相應的PropertyChanged事件。
例如,假設你想在Windows Phone 7應用程序中顯示當前時間,並且提供靈活的呈現方式。有時你只想顯示秒,並完全通過XAML來完成這個功能。例如,你想通過XAML顯示:“目前是X秒”,中間放一個每秒鍾更新一次的數字。當然這里講述的技巧可以擴展到許多其他的應用中,而不僅僅是一個鍾表應用。
雖然可以完全通過XAML實現整體的可視元素,但是你還是需要一些輔助的代碼(可能是一個命名為Clock的類,該類包含了Year、Month、Day、DayOfWeek、Hour、Minute和Second等屬性。)我們將在XAML文件中實例化這個Clock類並通過數據綁定來訪問它的屬性。
如你所知,在.NET中已經存在一個包含Year、Month和Day等相關屬性的結構體:DateTime。盡管DateTime對於編寫Clock類是必不可少的,但是它並不大符合我們的需求,因為DateTime的屬性不可以動態更改。相反,我要演示的Clock類所包含的屬性會實時地反映當前的時間,而且會通過PropertyChanged事件通知外部世界相應的變化。
Clock類包含在Petzold.Phone.Silverlight庫中,如下所示:
Silverlight項目:Petzold.Phone.Silverlight 文件:Clock.cs using System; using System.ComponentModel; using System.Windows.Threading; namespace Petzold.Phone.Silverlight { public class Clock : INotifyPropertyChanged { int hour, min, sec; DateTime date; public event PropertyChangedEventHandler PropertyChanged; public Clock() { OnTimerTick(null, null); DispatcherTimer tmr = new DispatcherTimer(); tmr.Interval = TimeSpan.FromSeconds(0.1); tmr.Tick += OnTimerTick; tmr.Start(); } public int Hour { protected set { if (value != hour) { hour = value; OnPropertyChanged(new PropertyChangedEventArgs("Hour")); } } get { return hour; } } public int Minute { protected set { if (value != min) { min = value; OnPropertyChanged(new PropertyChangedEventArgs("Minute")); } } get { return min; } } public int Second { protected set { if (value != sec) { sec = value; OnPropertyChanged(new PropertyChangedEventArgs("Second")); } } get { return sec; } } public DateTime Date { protected set { if (value != date) { date = value; OnPropertyChanged(new PropertyChangedEventArgs("Date")); } } get { return date; } } protected virtual void OnPropertyChanged(PropertyChangedEventArgs args) { if (PropertyChanged != null) PropertyChanged(this, args); } void OnTimerTick(object sender, EventArgs args) { DateTime dt = DateTime.Now; Hour = dt.Hour; Minute = dt.Minute; Second = dt.Second; Date = DateTime.Today; } } }
Clock類實現了INotifyPropertyChanged接口,因此包含PropertyChanged公共事件。靠近結尾的地方,有一個受保護的OnPropertyChanged方法,負責觸發實際的事件。在Clock類的構造函數中創建DispatcherTimer的實例並為它的Tick事件注冊了一個事件處理程序,這個處理程序的調用間隔(Interval)為1/10秒。在類最底部的OnTimerTick處理程序負責給這個類的Hour、Minute、Second和Data屬性設置新值,這些屬性的結構都非常相似。
例如,看一下Hour屬性:
public int Hour { protected set { if (value != hour) { hour = value; OnPropertyChanged(new PropertyChangedEventArgs("Hour")); } } get { return hour; } }
set存儲器是受保護的。因此該值只能在內部設置,我們並不希望外部的類設置該屬性。set存取器檢查正在設置的值與存儲在字段中的值是否相同,如果不相同,就把新值設置到hour字段中,同時調用OnPropertyChanged來觸發更新事件。
有些程序員不使用if語句來檢查屬性是否發生變化,其結果是一旦屬性被設置就馬上觸發PropertyChanged事件,即使該屬性的值並沒有發生變化也是如此。這並不是一個好主意,特別是像這樣的類。我們不希望每隔1/10秒PropertyChanged事件就報告Hour屬性發生了變化,其實該屬性每一小時才改變一次。
要在XAML文件中使用Clock類,你需要聲明一個XML命名空間來引用Petzold.Phone.Silverlight庫:
xmlns:petzold="clr-namespace:Petzold.Phone.Silverlight;assembly=Petzold.Phone.Silverlight"
當綁定源不是DependencyObject的子類時,你會在Binding中使用Source屬性來代替ElementName屬性。我們要創建的綁定的Source屬性設置為Petzold.Phone.Silverlight庫中的Clock對象。
你可以直接在Binding中插入Clock類的引用:
<TextBlock> <TextBlock.Text> <Binding Path="Second"> <Binding.Source> <petzold:Clock /> </Binding.Source> </Binding> </TextBlock.Text> </TextBlock>
Binding的Source屬性是一個屬性元素,並設置為Clock類的實例。Path屬性指向Clock類的Second屬性。
或者按照慣例把Clock類定義為XAML的資源:
<phone:PhoneApplicationPage.Resources> <petzold:Clock x:Key="clock" /> ... </phone:PhoneApplicationPage.Resources>
然后就可以在Binding擴展標記中引用該資源了:
TextBlock Text="{Binding Source={StaticResource clock}, Path=Second}" />
注意StaticResource的嵌入標記表達式(embedded markup expression)。
TimeDisplay項目演示了這種方法,該項目使用了水平StackPanel來串聯文本:
Silverlight項目:TimeDisplay 文件:MainPage.xaml <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"> <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center"> <TextBlock Text="The current seconds are " /> <TextBlock Text="{Binding Source={StaticResource clock}, Path=Second}" /> </StackPanel> </Grid>
運行效果如圖12-4所示。
再次強調:綁定目標(例如TextBlock的Text屬性)必須是依賴屬性。為了使綁定目標隨着綁定源(例如Clock的Second屬性)的變化而更新,綁定源必須實現某種通知機制。
當然,我並不想使用承載多個TextBlock元素的StackPanel。可以使用StringFormat- Converter(我已在TimeDisplay項目中把它定義為資源,並指定了它的鍵為StringFormat,因此你現在可以使用它了),我可以簡單地在一個TextBlock中包括整段文字,如下所示:
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"> <TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" Text="{Binding Source={StaticResource clock}, Path=Second, Converter={StaticResource stringFormat}, ConverterParameter='The current seconds are {0}'}" /> </Grid>
現在Binding標記表達式包含了兩個嵌入標記表達式。
如果想顯示Clock類的多個屬性,你可以回去使用多個TextBlock元素。例如,可以通過冒號來分割時、分和秒,同時在分和秒前補0:
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"> <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center"> <TextBlock Text="{Binding Source={StaticResource clock}, Path=Hour}" /> <TextBlock Text="{Binding Source={StaticResource clock}, Path=Minute, Converter={StaticResource stringFormat}, ConverterParameter=':{0:D2}'}" /> <TextBlock Text="{Binding Source={StaticResource clock}, Path=Second, Converter={StaticResource stringFormat}, ConverterParameter=':{0:D2}'}" /> </StackPanel> </Grid>
如你所見,3個綁定都使用了相同的Source設置。是否有方法讓我們避免這種重復呢?有,並且這種技巧是一個非常重要的概念。
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
原文首發《圖靈社區》