WPF中XMAL物件屬性賦值及屬性型別轉換機制
XAML中為物件屬性賦值共有四種語法:
1)使用字串直接賦值;
<Rectange x:Name="rect" Width="100" Height="200" Fill="Black" />
這個不用多解釋,就是屬性型別正好是字串,直接賦值即可,如x:Name="rect"。
2)使用字串簡單賦值(後臺進行字串的型別轉換);
<Rectange x:Name="rect" Width="100" Height="200" Fill="Black" />
注意這裡<Rectange>標籤的Fill屬性型別是Brush,而不是字串。但我們賦值時和直接賦值完全相同,也是賦予一個字串。
那它們有區別嗎?當然有。只是後臺自動為我們將這個字串轉換為了Brush型別,所以我們感覺是一樣的。
那以上Fill="Black"語句是使用的哪個畫刷呢?答案是SolidColorBrush。
也許你會說單色畫刷太單調了,確實如此。 Brush是一個抽象類,它的派生類還有很多:
SolidColorBrush:單色畫刷
LinearGradientBrush:線性漸變畫刷
RadialGradientBrush:徑向漸變畫刷
ImageBrush:點陣圖畫刷
DrawingBrush:向量圖畫刷
VisulaBrush:可視元素畫刷
那我們是否可以換一個其他畫刷呢?可以。但是有點麻煩。因為SolidColorBrush畫刷是微軟已為我們完成了後臺型別轉換了,是預設畫刷,所以支援簡單賦值。而其他畫刷未實現後臺型別轉換,是不能簡單賦值。
要實現後臺型別轉換需要使用微軟提供的型別轉換機制,下面就舉例介紹一下這套機制。
大家要明白,之所以要使用後臺型別轉換的原因是XMAL的語法限制,屬性賦值只能是字串。
public class Student
{
public string Name{get; set;}
public Student Classmate{get;set;}
}
這個類有兩個屬性,一個是string型別,一個是Student型別。現在我們需要在XAML中宣告它,如下:
<Windows.Resources>
<loacl:Student x:Key="stu" Classmate="Tim" />
</Windwos.Resources>
我們希望以上寫法,Student的例項的Classmate賦一個Student型別的值,並且Student.Name就是賦值的字串值。
在後臺中如下使用:
Student stu = (Student)this.FindResource("stu");
MessageBox.Show(stu.Classmate.Name);
結果如何呢?編譯通過,但執行時丟擲異常,stu.Classmate例項不存在,也就是說編譯器無法將我們賦值的Classmate="Tim"字串轉換為Classmate例項。
這裡我們需要使用TypeConvert和TypeConverterAttribute這兩個類完成型別轉換工作。
首先,我們從TypeConvert類派生出自己的類,並重寫它的一個ConvertFrom方法。這個方法有一個引數名為Value,這個值就是我們在XAML裡的"Tim"。
public classs StringToStudentTypeConverter:TypeConverter
{
public override object ConvertFrom(lTypeDescriptorContext context,CultureInfo culture,object value)
{
if ( vlaue is string )
{
Student stu = new Student();
stu.Name = value as string;
return stu;
}
return base.ConvertFrom(context,tulture,value);
}
}
有了這個類還不夠,還要使用TypeConverterAttribute這個特徵類把StringToStudentTypeConverter這個類關聯到作為目標的Student類上。
[TypeConverterAttribute(typeof(StringToStudentTypeConverter))]
public class Student
{
public string Name{get; set;}
public Student Classmate{get;set;}
}
現在大功告成,再次編譯執行,將獲得彈出視窗顯示正確結果。
3)使用屬性元素(Property Element)進行復雜賦值。
除了使用後臺型別轉換機制實現簡單賦值,還有一種通過前臺複雜賦值,它使用屬性元素的方式。
在XAML中非空標籤均具有自己的內容。標籤的內容指的是夾在起始標籤和結束標籤之間的一些子級標籤,每個子級標籤都是父級標籤內容的一個元素。屬性元素指的是某個標籤的一個元素對應這個標籤的一個屬性,即以元素的形式表達一個例項的屬性。如下:
<Rectangex:Name="rect" Width="100" Height="200" >
<Rectange.Fill>
<SolidColorBrush Color="Black" />
</Rectange.Fill>
</Rectange>
這段程式碼與先前的效果一樣。所以,對於簡單賦值而言,屬性元素語法並沒有什麼優勢,反而程式碼冗長。但遇到屬性是複雜物件時,這種語法的優勢就體現出來了。如使用
線性漸變畫刷來填充這個矩形。
<Rectangex:Name="rect" Width="100" Height="200" >
<Rectange.Fill>
<LinearGradientBrush>
<LinearGradientBrush.StartPoint>
<Pontx X="0" Y="0" />
</LinearGradientBrush.StartPoint>
<LinearGradientBrush.EndPoint>
<Pontx X="1" Y="1" />
</LinearGradientBrush.EndPoint>
<LinearGradientBrush.GradientStops>
<GradientStopCollection>
<GradientStop Offset="0.2" Color="LightBlue" />
<GradientStop Offset="0.7" Color="Blue" />
<GradientStop Offset="1.0" Color="Black" />
</GradientStopCollection>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Rectange.Fill>
</Rectange>
這段程式碼中,針對屬性都使用了屬性標籤式賦值方法,完成了漸變畫刷的賦值。這段程式碼感覺有點冗長,可讀性不強,我不打算逐行解釋,建議使用Blend工具直接生成。
4)使用標記擴充套件(Markup Extensions)賦值
複雜賦值是為屬性生成一個新物件,但有時候需要把同一個物件賦值給兩個物件的屬性,還有時候需要給物件的屬性賦一個null值,WPF甚至允許將一個物件的屬性值依賴在其他物件的某個屬性上。當需要為物件的屬性進行這些特殊型別賦值時,就需要使用標記擴充套件。
所謂標記擴充套件,實際上一種特殊的Attribute=value語法,其特殊的地方在於Value字串是由一對花括號及其括起來的內容組成,XAML編譯器會對這樣的內容做出解析,生成相應的物件。
標記擴充套件的作用同類型轉換器,都是將字串轉換為相應型別的物件。WPF/Silverlight內建的標記擴充套件都派生自MarkupExtension。其應用場景如下,當我們想設定一個屬性的值為一些特定的靜態屬性值,但我們在編譯時不知道這個值(如一個由使用者的配置來決定的顏色),這時候就可以使用標記擴充套件。簡單說標記擴充套件是一種用來設定屬性值的類。
與型別轉換器不同的是,標記擴充套件通過XAML顯式的,一致的呼叫,因此這是更好的擴充套件XAML的方法。另外標記擴充套件也可以完成一些型別轉換器所不能完成的功能。例如,通過自定義一個標記擴充套件可以實現使用一個簡單的字串將控制元件的背景色設定為漸變筆刷,而使用型別轉換器是無法完成的。
a. 標記擴充套件的語法及組成
XAML分析器將由"{ }"括起來的Attribute值認作一個標記擴充套件。
花括號中第一個識別符號被識別符為標記擴充套件的名稱,即定義標記擴充套件的類的名稱,按照慣例這樣的擴充套件常以Extension字尾結尾,但當在XAML中使用時可以省略該字尾。XAML分析器會自動新增並進行進一步處理。
第二個識別符號是標記擴充套件接受的引數。如果標記擴充套件接受傳入引數,可以為其指定引數值,並以逗號分隔各引數。標記擴充套件接受的引數分為兩種:
定位引數:其被作為字串引數傳入擴充套件類的相應的建構函式。
命名引數:可以用來在已構造好的擴充套件物件上設定相應名字的屬性。這些屬性的值也可以是標記擴充套件(即標記擴充套件允許巢狀),也可以是文字值,通過型別轉換器在執行時轉換為相應的型別。
在XAML編譯時,標記擴充套件的引數將被傳入標記擴充套件類的過載的建構函式中來建立建構函式的一個新例項。在建構函式內部使用ProvideValue方法得到引數表示的實際值並提供給XAML的Attribute(即標記擴充套件的所服務的特性)。
我們通過下面的例子來看這個標記擴充套件引數處理過程可由:
<Style TargetType="{x:Type Button}"></Style> |
如上的標記擴充套件(其中的引數為定位引數),在XAML編譯時會使用類似如下的C#程式碼來給TargetType賦值:
TypeExtension te = new TypeExtension(); object val = te.ProvideValue(s, Style.TargetTypeProperty); |
XAML對擴充套件標記類的驗證有兩種方式:編譯時驗證與執行時驗證。XAML編譯器挑選出部分擴充套件標記在編譯時進行驗證(這些標記擴充套件對於特定的引數,總是返回相同的值),另外大多數擴充套件(包括自定義擴充套件等)都在執行時進行測試。
上面例子中的TypeExtension是屬於編譯時驗證的擴充套件標記類。驗證程式碼形如:
Style s = new Style(); s.TargetType = typeof(Button); |
標記擴充套件的設計與.NET Framework的擴充套件機制 - 特性(Attribute)的設計是一致的。
標記擴充套件示例:
<Button Background="{x:Null}" Height="{x:Static SystemParameters.IconHeight}" Content="{Binding Path=Height,RelativeSource={RelativeSource Self}}" /> |
在上面的例子中,NullExtension與StaticExtension位於System.Windows.Markup名稱空間,所以需要使用x字首來定位。即x:Null與x:Static,而Binding(無Extension字尾)位於System.Windows.Data名稱空間下,在XAML匯入的主名稱空間中,所以不用使用字首。關於這些XAML名稱空間內容可參見本系列第一篇文章中所介紹內容。
例子中SystemParameters.IconHeight與巢狀標記擴充套件中Self屬於定位引數。而Path與RelativeSource屬於命名引數。
StaticExtension允許使用靜態屬性,欄位,常量及列舉值,不使用XAML中硬編碼的值(如使用硬編碼值則無需使用標記擴充套件)。
WPF中的標記擴充套件
WPF提供了一些內建的擴充套件標記,大部分被定義於XAML的XML名稱空間,小部分位於XAML的WPF名稱空間,前者需要通過x:訪問,後者可以直接訪問。下面的列表給出了這些內建標記擴充套件。
類 型 |
XAML |
用途 |
NullExtension |
x:Null |
用來表示null() |
TypeExtension |
x:Type |
得到Type物件 |
StaticExtension |
x:Static |
得到靜態屬性值 |
StaticResource |
StaticResource |
執行一次性的資源查詢 |
DynamicResource |
DynamicResource |
設定動態資源繫結 |
ArrayExtension |
x:Array |
建立陣列 |
Binding |
Binding |
建立資料繫結 |
TemplateBinding |
TemplateBinding |
模板繫結 |
下面逐一分析這些標記擴充套件:
1. NullExtension
NullExtension提供設定屬性為空值的方法。在部分情況下,不設定屬性值與顯式設定為null的區別很大,尤其是屬性已經被設定為某值,這時候設定為空相當於清除之前的設定。
上面的例子中對Button的Background屬性的設定就展示了NullExtension的使用,將Background設定為null就可以清除之前所設定的背景,這是一個很好的例子。
2. TypeExtension
TypeExtension將返回一個System.Type物件給標記擴充套件所服務的Attribute。其接受一個定位引數,表示型別的名稱。XAML將通過TypeExtension把這個字串表示的型別名轉化為相應的型別,同時這個型別名也不需要提供其完整名稱空間(.NET),預設的名稱空間就是該XAML的主名稱空間與x:名稱空間。
前文給TargetType屬性設定值的標記擴充套件就是TypeExtension的一個例子。
3. StaticExtension
StaticExtension在前文有所提及,其將物件的屬性設定為特定的靜態值。其接受一個引數,確定屬性的來源,引數格式為ClassName.PropertyName。
前文示例中設定Button的Height的程式碼演示了StaticExtension的使用。
StaticExtension存在的問題在於當屬性(Property)變化時不能自動修改屬性(Attribute)的值。另外單獨使用StaticExtension不能很好的結合系統設定與程式設定。在實際應用中往往將StaticExtension與StaticResource擴充套件結合使用。
4. StaticResource
StaticResource返回一個指定資源的值。等效於呼叫元素的FindResource方法。StaticResource與下面將介紹的DynamicResource兩個標記擴充套件位於WPF的名稱空間,使用時無須x:字首。下面是一段示例:
XAML:
<TextBlock Name="myText" Background="{StaticResource {x:Static SystemColors.ActiveCaptionBrushKey}}"/> |
等效C#:
myText.Background = (Brush)myText.FindResource(SystemColors.ActiveCaptionBrushKey); |
這段程式碼對資源進行一次性查詢,屬性(Property)值將會在初始化期間被設定為資源值。但資源值的變化不會引起屬性值的變化,所以,如當改變系統顏色主題時,元素的背景不會隨之更新。有效的解決方法就是使用下面介紹的DynamicResource。
5. DynamicResource
DynamicResource將屬性(Property)值與資源值聯絡起來,其使用方式與StaticResource相似,但可以跟蹤資源改變。
<TextBlock Name="myText" Background="{DynamicResource {x:Static SystemColors.ActiveCaptionBrushKey}}"/> |
等效的C#:
myText.SetResourceReference(TextBlock.BackgroundProperty, SystemColors.ActiveCaptionBrushKey); |
由程式碼可以看出,在DynamicResource中,將StaticResource中資源賦值的方式改為設定引用,這樣資源的值的改變可以被跟蹤。這樣當系統資源改變時,控制元件的背景也會隨之改變。
6. ArrayExtension
ArrayExtension用來將元素值設定為一個元素的陣列,其需要一個數組型別變數作為指定型別的屬性值。由於這種型別的標記擴充套件所接受的引數往往很長,所以通常其內容不採用"{ }"的形式來表示,而是採用屬性元素這種語法,其中每個陣列的值都被表現為ArrayExtension元素的子項。(當然對於空陣列可以使用"{ }"以使程式碼更簡潔)。參見如下示例:
<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> |
等效C#:
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)); myGrid.Resources["brushes"] = brushes; myListBox.ItemsSource = myListBox.Resources["brushes"]; |
這段程式碼中ArrayExtension中建立一個數組作為資源,陣列中的每一項又使用了TypeExtension,從而建立一個Brush型別的資源陣列,最後將資源設定給列表框。
7. Binding
Binding標記擴充套件用來進行資料繫結。示例程式碼:
<TextBlock Text="{Binding Foo}" x:Name="txt"/> |
這段程式碼在資料上下文將物件的Text屬性繫結到Foo。
BindingOperations.SetBinding(txt, TextBlock.TextProperty, b); |
8. TemplateBinding
模板繫結用在控制元件模板中,用於將源物件屬性對映到模板中的物件屬性。示例:
<Rectangle Width="100" Height="200" Fill="{TemplateBinding Background}"/> |
等效C#:
FrameworkElementFactory factory = new FrameworkElementFactory(typeof(Rectangle)); factory.SetValue(Rectangle.WidthProperty, 100); factory.SetValue(Rectangle.HeightProperty, 200); TemplateBindingExpression tb = new TemplateBindingExpression(Button.BackgroundProperty); factory.SetValue(Rectangle.FillProperty, tb); |
模板繫結用在模板的上下文。模板元素使用FrameworkElementFactory來建立其內容。這是因為模板可以被例項化很多次。
"{ }"的"轉義"
如果你需要設定的一個屬性值的字面值以"{"開頭,則需要特殊的方法對其轉義,以免其被當作標記擴充套件處理,轉意方法是在"{"之前加上一對"{ }"。
程式碼示例:
<Button Content="{}{This is not a markup extension!}"/> |
或者使用屬性元素實現同樣的目的,等價程式碼:
<Button> {This is not a markup extension!} </Button> |
這段程式碼使用了隱式屬性元素這個語法,這得益於內容屬性這種語法。完整寫法如下:
<Button> <Button.Content> {This is not a markup extension!} </Button.Content> </Button> |
因為標記擴充套件是有預設建構函式的類,其可以與屬性元素一起使用,前文示例的標記擴充套件的程式碼等價於如下XAML:
<Button> <Button.Background> <x:Null/> </Button.Background> <Button.Height> <x:Static Member="SystemParameters.IconHeight"/> </Button.Height> <Button.Content> <Binding Path="Height"> <Binding.RelativeSource> <RelativeSource Mode="Self"/> </Binding.RelativeSource> </Binding> </Button.Content> </Button> |
程式碼中StaticExtension有一個Member屬性與傳入標記擴充套件x:Static的形參的實參含義相同,同理,RelativeSource有一個對應於的其建構函式引數的Mode屬性。
自定義標記擴充套件
通過編寫繼承自MarkupExtention的類可以建立自己的標記擴充套件,要確保XAML編譯器可以找到你的擴充套件型別,並將引數恰當的傳入,最主要要做的就是提供恰當的過載建構函式來接收引數(適用於定位引數)或建立合適的屬性(適用於命名引數)。下面的C#示例程式碼展示了怎樣用標記擴充套件中的命名引數給屬性賦值,通過這可以看出為什麼自定義標記擴充套件時要定義引數。
<TextBlock TextContent="{Binding Path=SimpleProperty, Mode=OneTime}"/> |
C#初始化標記擴充套件的方法:
Binding b = new Binding(); b.Path = new PropertyPath("SimpleProperty"); b.Mode = BindingMode.OneTime; |