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
商业转载请事先与我联系:silverfox715@sina.com