在使用WPF進行編程的過程中,我們常常需要使用XAML的標記擴展:{Binding},{x:Null}等等。那么為什么WPF提供了XAML標記擴展這一功能,我們又如何創建自定義的標記擴展呢。這就是本文將要討論的內容。
一.從標記擴展的分析說起
在WPF中,軟件開發人員需要以類似於XML的格式編寫XAML。如下面代碼所示:
1 <Window …>
2 <StackPanel …>
3 <TextBlock …/>
4 </StackPanel>
5 </Window>
但是在實際開發過程中,我們卻常常需要使用標記擴展,如對綁定的使用:
1 <Window …>
2 <StackPanel>
3 <TextBlock Text="{Binding src:DataSource.Description}"/>
4 </StackPanel>
5 </Window>
您會好奇,為什么提供這種特殊的語法?其實這是因為XAML本身無法完成某些特定的功能所導致的。如果需要深刻地了解產生該問題的原因,我們就需要從XAML編譯器是如何對XAML進行解析的講起。
無論XAML的最終表示形式是怎樣,編譯器在處理XAML文件時所得到的都是一個個字符串。一個XML元素的開始常常表示類型實例,而以屬性(Attribute)或子元素所表示的XML組成則是在對該類型實例的屬性進行設置。在分析對XML屬性(Attribute)進行賦值的字符串時,XAML處理器會根據字符串的內容決定自身的分析邏輯。
對於普通的屬性賦值字符串,XAML處理器會根據屬性的類型決定是否需要執行對字符串的轉化。如果屬性的類型不是字符串,那么XAML處理器會調用相應的轉化邏輯,如對於枚舉類型的屬性,XAML處理器將通過Enum的Parse方法得到相應類型的數值。而對於自定義類型,XAML會根據該自定義類型聲明或屬性聲明上所標明的TypeConverter將字符串轉換為該自定義類型。
也就是說,可以被XAML編譯器正確解釋的自定義類型需要滿足如下條件:屬性的類型需要是值類型,具有默認構造函數的類型或者標明了專用類型轉換器的類型,即標明了特性TypeConverterAttribute。
如果一個類型不能提供滿足上面條件的實線,那該怎么辦呢?解決問題的方法就是使用XAML標記擴展。XAML編譯器會按照如下方式分析XAML標記擴展:如果XAML處理器遇到一個大括號,或者遇到一個從MarkupExtension派生的對象元素時,那么XAML編譯器將按照標記擴展分析該字符串,直至遇到表示結束的花括號。首先,編譯器會根據字符串決定標記擴展所對應的MarkupExtension類派生類。接下來,編譯器將按照下面的規則對擴展標記字符串進行處理:1) 逗號代表各個標記的分隔符。2) 如果分隔的標記沒有任何等號賦值,那么它將被視為構造函數的參數。這些參數需要與構造函數的參數個數匹配。如果兩個構造函數的參數個數相同,那么XAML編譯器將無法分析。該行為沒有定義。3) 如果每個標記都包含等號,那么XAML處理器將首先調用默認構造函數並對這些屬性進行賦值。4) 如果標記擴展同時使用了構造函數參數以及屬性賦值,那么XAML處理器內部將調用對應的構造函數並對屬性進行賦值。最后,編譯器會在應用程序加載時調用該類型的ProvideValue()函數,用來定義該標記應該返回哪個對象。該函數調用會傳入有關當前上下文的信息,以允許ProvideValue()函數根據該上下文創建相應的對象。
如果標記擴展之間存在着嵌套,那么XAML編譯器將首先計算標記擴展的最內層,如下面示例將首先計算x:Static:
1 <Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.Control}}"/>
可以看到,XAML編譯器對屬性賦值進行分析的方式主要會根據其是否是標記擴展而分為使用轉化或調用標記擴展的ProvideValue()函數兩種。這兩種方法之間的最大不同在於ProvideValue()函數可以根據上下文提供更復雜的實例創建或引用邏輯。另外,標記擴展允許軟件開發人員在XAML中使用帶有一個參數的非默認構造函數。這也是標記擴展的一個優點。
二.WPF中的標記擴展
在開始講解之前,您最好得到WPF的實現代碼。雖然說本文會提供必要的代碼片斷,但能從全局層面上分析可能會給您更多的收獲。在“從Dispatcher.PushFrame()說起”一文中,我們已經介紹了如何獲得.net的源碼,而在“資源下載”一文中,我們也提供了這些源碼的下載地址。
首先來看看比較典型的標記擴展{x:Type}的實現:
1 [MarkupExtensionReturnType(typeof(System.Type)),
2 TypeConverter(typeof(TypeExtensionConverter))]
3 public class TypeExtension : MarkupExtension
4 {
5 ……
6 public TypeExtension(System.Type type)
7 {
8 ……
9 this._type = type;
10 }
11
12 public override object ProvideValue(IServiceProvider serviceProvider)
13 {
14 if (this._type == null)
15 {
16 ……
17 IXamlTypeResolver service = serviceProvider.GetService(
18 typeof(IXamlTypeResolver)) as IXamlTypeResolver;
19 ……
20 this._type = service.Resolve(this._typeName);
21 ……
22 }
23 return this._type;
24 }
25
26 [ConstructorArgument("type"), DefaultValue((string) null)]
27 public System.Type Type
28 {
29 get { return this._type; }
30 set
31 {
32 ……
33 this._type = value;
34 this._typeName = null;
35 }
36 }
37 ……
38 }
首先來看看最重要的組成ProvideValue()函數。該函數首先會通過GetService()函數得到IXamlTypeResolver服務。該服務所提供的Resolve()函數會根據TypeName屬性所記錄的字符串解析出TypeName屬性所指定的Type實例對象。
標記擴展{x:Type}的實現所展示的ProvideValue()函數實現是標記擴展實現中的典型實現。通過GetService()函數所可能得到的常用服務有:IProvideValueTarget服務,以知曉標記擴展所在的目標元素和屬性;IUriContext,即可獲得當前上下文中的基准Uri;IXamlTypeResolver,用來將XAML元素名稱解析為.net類型實例,最典型的例子就是x:Type標記擴展。
同時上面所展示的代碼使用了三個特性:ConstructorArgument、TypeConverter以及MarkupExtensionReturnType。接下來,我們就來看看這三個特性各自的功能。
首先就是ConstructorArgument特性。該特性用來提示XAML編譯器標記擴展中所標示的構造函數參數實際上與哪個屬性相對應。通過該特性所關聯的屬性則必須是一個可讀寫的屬性。
那么問題接踵而至:ConstructorArgument特性是使用在類型為Type的屬性之上,而XAML編譯器所輸入的則是字符串類型。為了解決這種類型上的不匹配,標記擴展TypeExtension使用了另一個特性TypeConverter提示XAML編譯器使用類型轉換器類型TypeExtensionConverter處理標記擴展聲明中所標示的字符串類型參數。
最后一個要提及的特性就是MarkupExtensionReturnType。該特性用來標明ProvideValue()函數所返回的類型。
三.自定義標記擴展
現在我們就來開始編寫自定義標記擴展。自定義標記擴展常常從MarkupExtension派生,並重寫該類的ProvideValue()函數。在本節中,我們就以延遲綁定為例演示如何創建一個自定義綁定。
想象下面一種情況:在一個程序的XAML中聲明的綁定會在程序啟動時加載,並請求綁定源屬性的值。對該源屬性值的求解將會導致其它功能被加載。試想一下,如果Ribbon所羅列的所有功能都會在程序啟動時被加載,那么程序的啟動性能將變得非常差。
這也就是延遲綁定所需要解決的問題。只有在程序界面變為可見時,綁定才會被添加到界面元素中並對其進行求解。
可能您的第一反應是創建一個自定義綁定以解決該問題。的確,BindingBase類提供了虛函數CreateBindingExpressionOverride()以供自定義綁定實現者提供自定義功能。但是本文不采用該方法,其原因有二:該函數所提供的靈活性較差;該函數具有較強的語義特征。其用於創建BindingExpression類型實例,而並不適用於延遲綁定的實現。
因此,使LazyBinding派生自MarkupExtension並重寫它的ProvideValue()函數可能是一個更好的選擇。下面就是實現LazyBinding的代碼:
1 [MarkupExtensionReturnType(typeof(object))]
2 public class LazyBindingExtension : MarkupExtension
3 {
4 public LazyBindingExtension()
5 { }
6
7 public LazyBindingExtension(string path)
8 {
9 Path = new PropertyPath(path);
10 }
11
12 public override object ProvideValue(IServiceProvider serviceProvider)
13 {
14 IProvideValueTarget service = serviceProvider.GetService
15 (typeof(IProvideValueTarget)) as IProvideValueTarget;
16 if (service == null)
17 return null;
18
19 mTarget = service.TargetObject as FrameworkElement;
20 mProperty = service.TargetProperty as DependencyProperty;
21 if (mTarget != null && mProperty != null)
22 {
23 // 偵聽IsVisible屬性的更改,以在界面元素顯示時通過OnIsVisibleChanged
24 // 函數添加綁定
25 mTarget.IsVisibleChanged += OnIsVisibleChanged;
26 return null;
27 }
28 else
29 {
30 Binding binding = CreateBinding();
31 return binding.ProvideValue(serviceProvider);
32 }
33 }
34
35 private void OnIsVisibleChanged(object sender,
36 DependencyPropertyChangedEventArgs e)
37 {
38 // 添加綁定
39 Binding binding = CreateBinding();
40 BindingOperations.SetBinding(mTarget, mProperty, binding);
41 }
42
43 private Binding CreateBinding() // 創建綁定類型實例
44 {
45 Binding binding = new Binding(Path.Path);
46 if (Source != null)
47 binding.Source = Source;
48 if (RelativeSource != null)
49 binding.RelativeSource = RelativeSource;
50 if (ElementName != null)
51 binding.ElementName = ElementName;
52 binding.Converter = Converter;
53 binding.ConverterParameter = ConverterParameter;
54 return binding;
55 }
56
57 #region Fields
58 private FrameworkElement mTarget = null;
59 private DependencyProperty mProperty = null;
60 #endregion
61
62 #region Properties
63 public object Source…
64 public RelativeSource RelativeSource…
65 public string ElementName…
66 public PropertyPath Path…
67 public IValueConverter Converter…
68 public object ConverterParameter…
69 #endregion
70 }
在這里,LazyBinding僅僅探測IsVisibileChanged事件,以在UI元素顯示時動態添加綁定。在該類的真正實現中,以何種方式完成延遲功能則需要您根據需求決定。
在XAML中,軟件開發人員可以像普通綁定一樣使用它。但需要注意的一個問題就是MarkupExtension的嵌套使用。如果您按照下面的方法使用LazyBinding:
1 <TextBlock Text="{local:LazyBinding ElementName=mMainWindow, Path=Source, Converter={StaticResource testConverter}}"/>
那么編譯器會在編譯時報錯。從網絡上的討論來看,這是一個Bug,但是無論在VS2008還是VS2010中,其都沒有得到修正。如果我是錯誤的,請通知我。
作為一個變通的方法,我們可以在程序中通過XML元素的方法完成對LazyBinding的使用:
1 <TextBlock>
2 <TextBlock.Text>
3 <local:LazyBinding ElementName="mMainWindow" Path="Source" Converter="{StaticResource testConverter}"/>
4 </TextBlock.Text>
5 </TextBlock>
四.命名空間管理
其實這本不屬於與標志擴展關聯密切的話題。只是由於WPF中的眾多標記擴展都使用了x:作為前綴,並且其在編寫類庫中非常常見,因此在本文中,我們將以一小部分篇幅完成對該功能的介紹。
在開發WPF程序時,XAML一般包含兩個xmlns聲明:
1 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
2 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
第一個聲明用來指定WPF命名空間為默認命名空間,而第二個聲明用來指定x:前綴對應XAML命名空間。這兩個聲明的關系是:XAML是實現標准,用來定義為實現兼容而要實現的元素,而WPF是將XAML作為語言而使用的實現。如x:Type等就是標准的標記擴展,而StaticResource則是WPF的特定擴展。因此,有些派生自MarkupExtension類的標記擴展實際上是XAML的語言規范的一部分。它們通常使用x:前綴。
軟件開發人員可以通過XmlnsDefinitionAttribute特性將多個CLR命名空間映射到單個XML命名空間。為了達到該目的,軟件開發人員僅需要將該特性聲明置於AssemblyInfo中即可,並標以assembly范圍。該特性可重復使用,以將多個CLR命名空間映射到一個XML命名空間。
需注意的是,如果使用該映射命名空間的XAML文件與該特性處於同一項目中,那么該特性聲明的XML命名空間將不包含同一項目中的類型。這是因為編譯時原程序集已清空而其中的類型無法在編譯時解析的緣故。我並沒有找到在官方文檔中對該問題的說明,因此如果您找到解釋該行為的文檔,請告知。
轉載請注明原文地址:http://www.cnblogs.com/loveis715/archive/2012/02/06/2340669.html
商業轉載請事先與我聯系:silverfox715@sina.com