天天看點

WPF - 自定義标記擴充

  在使用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

商業轉載請事先與我聯系:[email protected]

轉載于:https://www.cnblogs.com/loveis715/archive/2012/02/06/2340669.html

ui