這一篇我要總結的內容是XAML中的擴展標記(Markup Extensions).
擴展標記
通過類型轉換器和屬性元素,我們可以將大多數屬性初始化為常數值或者固定結構,不過在某些情況下我們需要更強的靈活性。舉個例子,雖然我們可能會設置一個等價於某些特定靜態屬性的屬性,但是我們並不知道在編譯時該屬性值將等於什么,這就像用來表示用戶自定義顏色的屬性一樣。XAML以擴展標記的形式提供一個強大的解決方案。一個擴展標記就是一個在運行時決定如何設置屬性值的類。
擴展標記類派生自MarkupExtension,下面代碼顯示了其非私有成員。這個類定義在System.Windows.Markup命名空間中。
例1:擴展標記類:
public abstract class MarkupExtension
{
protected MarkupExtension() { }//構造函數,初始化從MarkupExtension派生的類的新實例。
public abstract object ProvideValue(IServiceProvider serviceProvider);//在派生類中實現時,返回一個對象,此對象是作為此標記擴展的目標屬性的值提供的。
}
下面是一個關於擴展標記的例子。
例2:使用一個擴展標記:
...
<Style TargetType="{x:Type Button}"/>
...
其中Style的TargetType屬性有一個用大括號括起來的值,通過這一點XAML編譯器就能識別使用了擴展標記。大括里的第一個字符串是擴展標記類名,接下來的內容將會在初始化時傳入擴展標記。
在上例中我們使用了x:Type。雖然由XAML命名空間表示的.NET命名空間中沒有任何一個叫做Type的類,但是當XAML編譯器找不擴展標記類時,它就會為名稱添加Extension並且重試。由於有一個TypeExtension類,因此XAML的編譯器會使用這個類,並將字符串"Button"傳遞給它的構造函數,這樣就初始化了一個TypeExtension類的實例。然后,它會調用擴展標記的ProvideValue方法來獲得一個用於屬性的真實值,而TypeExtension則會返回Type對象供Button類使用。
如例1所示,ProvideValue只擁有一個類型為IserviceProvider的參數。這是一個標准的.NET接口,通過它可以提供一套服務,而每一個服務都是一些接口的實現。這里提供了兩個服務:IProvideValueTarget和IXamlTypeResolver.通過前者,擴展標記可以獲得所有提供值的對象和屬性(這里是Style對象的TargetType屬性)。通過后者可以將類型名轉換成類型(這里是將Type轉換成TypeExtension類),並將使用了擴展標記的位置納入XML命名空間的范疇。TypeExtension就是通過這個服務將其參數轉換成Type對象。
XAML的編譯器對特定的擴展標記進行了特殊的處理,並在編譯時進行求值,由於編譯器作者知道那些擴展標記總是返回同樣的值給特定的輸入。(大多數擴展標記在運行的時候進行求值,其中包括您所寫的任何一個自定義擴展名)。TypeExtension就是其中的一個特殊情況,因此,在編譯時,編譯器將執行等同於例3所示的代碼。(由於傳遞給ProvideValue方法的服務提供者是一個XAML編譯器詳細的執行過程,因此這里沒有顯示出來)。
例3:編譯時TypeExtension所產生的效果:
TypeExtension te = new TypeExtension("Button");//初始化TypeExtension的實例
object val = te.ProvideValue(serviceProviderImpl);//調用ProvideValue方法,通過IProvideValueTarget接口獲得了Style對象的TargetType屬性。
例2中TypeExtension類在運行時所產生的效果等同於例4所示的代碼。這種特殊的處理其主要原因還是效率的問題,也就是說由於普遍使用了TypeExtension,因此在運行時查找名稱將會降低速度。
例4:TypeExtension的運行時效果:
Style s = new Style();
s.TargetType=typeof(Button);
XAML可以通過兩種方式將數據傳遞進擴展標記。一種就是如例2所示的構造函數參數,其中TypeExtension提供一個可以帶有字符串的構造函數,並且那個示例將字符串"Button"傳進了構造函數。(可以用逗號分隔來傳遞多個參數)。另一種是設置屬性,您可以通過將PropertyName=value對列成一個清單。如例5.
例5:使用帶有擴展標記的Name=Value值對:
<TextBlock Text="{Binding Path=SimpleProperty,Mode=OneTime}"/>
傳遞給擴展標記的屬性也與其他所有屬性一樣,是使用類型轉換器來進行解析的。例5等價於例6所示的代碼。
例6:為綁定設置屬性:
Binding b = new Binding();
b.Path = new PropertyPath("SimpleProperty");
b.Mode = BindingMode.OneTime;
內置的擴展標記
Silverlight提供了許多實用的內置擴展標記。其中一些定義在XAML XML命名空間中,因此按照慣例您可以使用x:前綴來對它們進行訪問。下表是常用的擴展標記。
類型 |
說明 |
x:NullExtension |
用來表示空值(編譯時進行求值) |
x:TypeExtension |
獲得類型對象(在編譯時進行求值) |
x:ArraryExtension |
創建一個數組 |
x:StaticExtension |
獲得靜態屬性值 |
下面就來分別介紹這些內置的擴展標記。
NullExtension
NullExtension提供了一種將屬性設置為NULL的方法。在某些情況下顯示將屬性設置為NULL非常重要。比如一個Style可能會將所有元素的Background屬性設置為某一個特定值,而您可能會在某一個指定元素上屏蔽這個值。如果想移除背景色,不是將它設置為另一種顏色,那么就要通過顯示將該元素的屬性的Background設置為NULL來實現。
下面使用NullExtension設置了一個按鈕的背景色,這樣可以防止按鈕的背景色被填充。
<Button Background="{x:Null}">Click</Button>
以上代碼等價於:
Button b = new Button();//創建一個Button類對象
b.Background = null;//設置Background屬性
b.Content = "Click";//設置Content屬性
TypeExtension
TypeExtension會為已命名的類型返回一個System.Type對象。它往往帶有一個參數:那就是類型名。在應用了TypeExtension時,TypeExtension會通過IServiceProvider傳遞給它的ProvideValue方法來獲得有關XAML解析環境的信息,並且使它能夠像XAML編譯器處理元素名稱那樣來處理類型名。這就意味着,您不必提供一個完全限定的含有.NET命名空間擴展標記的類型名稱,相反您只需按以下方式做就可以。
<Style TargetType="{x:Type Button}"/>
TypeExtension將會把字符串"Button"像XAML編譯器一樣處理成一個類型,也就是說,它將會被包括進默認的XML命名空間中以及任何存在的命名空間映射中。這個擴展標記的作用等價於:
Style s = new Style();
s.TargetType=typeof(Button);
同樣,實際應用過程中會復雜很多,不過上面的例子解釋了擴展標記的目的和作用。
ArrayExtension
通過ArrayExtension您可以創建一組元素。這個擴展標記有點特殊,因為該標記不需要使用大括號,也就是說要通過一個完整元素來代替。這是因為一個組可以包括多個選項,並且有了ArrayExtension,這些就表現為擴展標記子元素,(不過如果想獲得一個空數組,可以使用大括號來實現)。
下例使用ArrayExtension來創建了一個數組作為資源,接着將這一數組作為一個ListBox的數據源。ArrayExtension必須通過其Type屬性來指定數組類型,對於這個屬性我們可以使用前面介紹的TypeExtension,下面我們來創建一個Brush類型的數組。
<Grid>
<Grid.Resources>
<x:ArrayExtension Type="{x:Type Brush}" x:Key="brushes">
<SolidColorBrush Color="Blue"/>
<LinearGradientBrush StartPoint="0,0" EndPoint="0.8,1.5">
<LinearGradientBrush.GradientStops>
<GradientStop Color="Green" Offset="0"/>
<GradientStop Color="Cyan" Offset="1"/>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<LinearGradientBrush.GradientStops>
<GradientStop Color="Black" Offset="0"/>
<GradientStop Color="Red" Offset="1"/>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</x:ArrayExtension>
</Grid.Resources>
<ListBox ItemsSource="{StaticResource brushes}" Name="myListBox">
<ListBox.ItemTemplate>
<DataTemplate>
<Rectangle Fill="{Binding}" Width="100" Height="40" Margin="2"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
以上代碼等價於以下代碼:
Brush[] brushes = new Brush[3];//聲明Brush類型的數組,大小為3
SolidColorBrush scb = new SolidColorBrush();//創建SolidColorBrush類對象
scb.Color = Colors.Blue;//設置屬性Color
brushes[0] = scb;//初始化數組的第一個元素
LinearGradientBrush lgb = new LinearGradientBrush();//創建對象
lgb.StartPoint = new Point(0,0);//設置屬性
lgb.EndPoint = new Point(0.8,1.5);
GradientStop gs = new GradientStop();//創建對象
gs.Color = Colors.Green;//設置屬性
gs.Offset = 0;
lgb.GradientStops.Add(gs);//調用Add()方法添加對象
gs = new GradientStop();//再創建一個GradientStop對象
gs.Color = Colors.Cyan;
gs.Offset = 1;
lgb.GradientStops.Add(gs);
brushes[1] = lgb;//實始化數組的第二個元素
lgb = new LinearGradientBrush();
lgb.StartPoint = new Point(0,0);
lgb.EndPoint = new Point(0,1);
gs = new GradientStop();
gs.Color = Colors.Black;
gs.Offset = 0;
lgb.GradientStops.Add(gs);
gs = new GradientStop();
gs.Color = Colors.Red;
gs.Offset = 1;
lgb.GradientStops.Add(gs);
brushes[2] = lgb;//實始化數組的第三個元素
myGrid.Resources["brushes"] = brushes;
...
myListBox.ItemsSource=myListBox.Resources["brushes"];
結果如圖所示:
注意這個示例可能不是使用標記的最佳選擇,不過對於創建一個等價的數組來說,代碼還可以編寫得更簡潔一些。
Brush[] brushes=new Brush[3];
brushes[0] = Brushes.Blue;
brushes[1] = new LinearGradientBrush(Colors.Green,Colors.Cyan,new Point(0,0),new Point(0.8,1.5));
brushes[2] = new LinearGradientBrush(Colors.Black,Colors.Red,new Point(0,0),new Point(0,1));
如果使用標記或代碼都可以實現您所要的功能,使用代碼往往會更簡潔一些,而ArrayExtension存在的主要原因是得益於基於XAML的工具。通過ArrayExtension,這些工具就可以創建一個數組而不需要生成代碼。
StaticExtension
通過StaticExtension可以將目標屬性設置成指定靜態屬性或者字段的值。這個擴展標記往往帶有一個命名資源屬性或者字段的參數,該參數的形式為ClassName.MemberName。下面示例就是使用這個擴展標記獲得了SystemColors類其中的一個屬性值。
<TextBlock Background="{x:Static SystemColors.ActiveCaptionBrush}" Text="Foo"/>
請注意在實際過程中您往往不會像以上示例那樣使用標記。由於它沒有適當地與資源系統相結合,因此如果應用程序資源包含一個重載系統畫刷的畫刷,那這個示例將不會使用那個應用程序級的資源。同樣的,也不會在屬性發生變化時自動更新屬性,也就是說StaticExtension只會獲得一次屬性值。其作用等價於以下代碼。
TextBlock tb = new TextBlock();
tb.Background = SystemColors.ActiveCaptionBrush;
tb.Text = "Foo";
Code Behind(隱藏代碼文件)
在前已經講過,XAML是支持隱藏代碼(Code Behind)這種理念,所謂隱藏代碼理念,就是指界面和行為分開處理,其中XAML文件用來定義用戶界面,而隱藏代碼文件提供行為。
XAML是通過命名用partial類(分部類,前面已經講過)來支持隱藏代碼。通過partial類,一個類定義就可以在多個源文件中使用,而每一個單獨的文件只包含一個partial類定義。在編譯時,編譯器會將這些結合起來組成一個完整的類定義。partial類的主要目的是允許生成的代碼和手寫的代碼共用一個類而不必共用一個源文件。
如果您已經在XAML中定義了一個元素,並且想要在隱藏代碼中進行使用,只需要設置其Name屬性(類似於ASP.NET中的ID),如下例。
例1:已命名的元素
<Button Name="myButton">Click</Button>
通過以上標記,XAML編譯器將會給類添加一個myButton的字段,並且會在初始化時進行設置以引用這個按鈕,這樣就可以在隱藏代碼中編寫代碼來使用這個元素。如。
例2:從隱藏代碼文件中使用已命名的元素
myButton.Background = Brushes.Green;
注意,並不是所有的類型都有一個Name屬性,不過,你可以通過一個x:Name屬性來代替隱藏代碼為它們生成一個字段,並且將x:作為XAML命名空間的前綴,如。
例3:x:Name屬性
<Button Content="Click">
<Button.Background>
<SolidColorBrush x:Name="bgBrush" Color="Yellow"/>
</Button.Background>
</Button>
在隱藏代碼中使用該元素,在這里更改為元素的Color屬性的值,如:
bgBrush.Color = Colors.Red;
那為什么會這樣呢?事實上,你甚至可以在Name合法元素上使用x:Name樣式,例如將例1中的Name替換成x:Name也不會改變它的行為。這是因為FrameworkElement將Name屬性識別成x:Name屬性的映射。這可以通過RuntimeNamePropertyAttribute來實現,如下例。
例4:將Name屬性映射到x:Name
[RuntimeNamePropertyAttribute("Name"),...]
public class FrameworkElement : UIElement, ...
如果類型進行了這樣的屬性注釋,那么在XAML中已命名的屬性和x:Name之間就可以互相轉換。但是如果類型中沒有這種自定義屬性的話,您就必須通過XAML中的x:Name屬性來生成隱藏代碼中的字段,即使目標類型有一個Name屬性。
隱藏代碼的其中一個主要任務就是制定應用程序響應用戶輸入的功能,因此,需要經常在XAML中把事件處理函數附加到元素上。首先的方法就是編寫在隱藏代碼初始化的過程中附加處理函數。如下示例。
<Button x:Name="myButton" Click="myButton_Click">Click</Button>
以上標記等價於在隱藏代碼中附加處理函數,如下代碼。
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
myButton.Click += myButton_Click;
}
}
盡管以上代碼看起來與屬性語法類似,不過XAML編譯器將識別Click成員是一個事件,而不是一個屬性。它會認為隱藏代碼中提供了一個叫做myButton_Click的函數,並且它會將函數作為按鈕Click的事件處理進行添加。但是,你必須確保函數有正確的簽名,也就是說所有.NET事件都會有一個特定類型的函數簽名。
至此,XAML基礎的所有內容都總結完了,從下一篇開始將要總結的是Silverlight基礎,歡迎大家與我討論,祝大家新年快樂,龍年大吉。