上一篇WPF Unleashed Chapter 2:XAML Demystified 翻译(第一部分)
声明: 本译文仅供学习讨论,禁止用于商业用途,否则后果自负
类型转换(Type Converters)
之前使用XAML语言创建的Button可以用下面的c#代码来实现:
System.Windows.Controls.Button b = new System.Windows.Controls.Button();
b.Content = “OK”;
b.Background = System.Windows.Media.Brushes.White;
您可能注意到了,XAML中的"White"字符是怎么转换成System.Windows.Media.Brushes类的静态成员White的呢。实 际上在XAML中使用字符串为属性赋值所得到的类型并不是System.String类型或System.Object类型。XAML的编译器和解析器会 去用“类别转换器”将字符串装换成合适的类型。WPF内置了多种类型转换的类,像Brush,Color,FontWeight,Point等这些类型都 有相应“类型转换器”,这些类型转换器都继承自TypeConverter类。如果需要的话,您还可以编写自定义的类型转换器。注:类型转化内的字符不像 XAML那样大小写敏感
如果不使用Brush类型转换的话,上面的例子也可以使用属性元素来代替:
<Button xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
Content=”OK”>
<Button.Background>
<SolidColorBrush Color=”White”/>
</Button.Background>
</Button>
上面的代码可以运行是因为Color的类型转换器在起作用,它将"Color"字符串转换成相应的颜色。那假设没有Color类型转换器的话该怎么办呢?请看下面的代码:
<Button xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
Content=”OK”>
<Button.Background>
<SolidColorBrush>
<SolidColorBrush.Color>
<Color A=”255” R=”255” G=”255” B=”255”/>
</SolidColorBrush.Color>
</SolidColorBrush>
</Button.Background>
</Button>
声明: 本译文仅供学习讨论,禁止用于商业用途,否则后果自负
类型转换(Type Converters)
之前使用XAML语言创建的Button可以用下面的c#代码来实现:
System.Windows.Controls.Button b = new System.Windows.Controls.Button();
b.Content = “OK”;
b.Background = System.Windows.Media.Brushes.White;
您可能注意到了,XAML中的"White"字符是怎么转换成System.Windows.Media.Brushes类的静态成员White的呢。实 际上在XAML中使用字符串为属性赋值所得到的类型并不是System.String类型或System.Object类型。XAML的编译器和解析器会 去用“类别转换器”将字符串装换成合适的类型。WPF内置了多种类型转换的类,像Brush,Color,FontWeight,Point等这些类型都 有相应“类型转换器”,这些类型转换器都继承自TypeConverter类。如果需要的话,您还可以编写自定义的类型转换器。注:类型转化内的字符不像 XAML那样大小写敏感
如果不使用Brush类型转换的话,上面的例子也可以使用属性元素来代替:
<Button xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
Content=”OK”>
<Button.Background>
<SolidColorBrush Color=”White”/>
</Button.Background>
</Button>
上面的代码可以运行是因为Color的类型转换器在起作用,它将"Color"字符串转换成相应的颜色。那假设没有Color类型转换器的话该怎么办呢?请看下面的代码:
<Button xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
Content=”OK”>
<Button.Background>
<SolidColorBrush>
<SolidColorBrush.Color>
<Color A=”255” R=”255” G=”255” B=”255”/>
</SolidColorBrush.Color>
</SolidColorBrush>
</Button.Background>
</Button>
Color的A,R,G,B属性的类型为byte,所以像"255"这种字符串之所以能够赋值给A,R,G,B,也是类型转换器起了作用。如果没有类型转 换,您可能就挂了。可见类型转换不但能够提高XAML代码的可读性,还能够提升XAML的表达力(Without this type converter, you would be stuck. Therefore, type converters don’t just enhance the readability of XAML, but they also enable concepts to be expressed that wouldn’t otherwise be expressible)
标记扩展(Markup Extensions)
标记扩展(英文是Markup Extensions,不知道翻译的对不对,先这么叫这吧),用于增强XAML语言的表达能力。标记扩展能够在程序运行时计算字符串所代表的值(出于性能 的考虑,还有小部分标记扩展在编译时就可以求值)。和类型扩展一样,WPF也内置了一些标记扩展的类。和类型转换不同之处在于,要想在XAML中使用标记 扩展,需要使用一套特定的语法格式。标记扩展的功能非常强大,它不存在类型转换的那种限制。举例来说,如果使用字符串创建一个fancy gradient brush,并让这个刷子作为控件的背景色,我们可以写一个自定义的标记扩展来实现,而这个要求是内置的BrushConverter所办不到的。
要使用标记扩展,需要将属性用两个大括号({})包含起来,这样XAML会将内部的值作为标记扩展来编译和解析,而不是当成文字(或者类型转换)。在下面的例子中,Button控件使用了三种类型扩展给它的三个属性赋值:
大括号内的第一个标识符是标记扩展类的名称。一般每个标记扩展类后面都带有"Extension"这个字符串作为后缀,但也可以省略(上面的代码就省略
了,译者注)。上面的代码用到了NullExtension(见x:Null)和StaticExtnesion(x:Static)这两个标记扩展类。
这两个标记扩展类的命名空间是System.Windows.Markup,这个命名空间不在默认的命名空间内(见第二章"命名空间的DIGGING
DEEPER部分"),所以需要添加X标记来定位。而Binding(Binding也继承自MarkupExtension类,译者注)位于
System.Windows.Data命名空间下,属于默认的命名空间,所以没有添加X标记
在标记扩展中,我们将那些没有指定属性名称而直接赋值的参数称为"位置参数"(Positional Parameters),上面代码中的SystemParameters.IconHeight就是位置参数。这种参数将被当作一个字符串传递给是标记扩 展类的构造函数;对应的,我们将那些指定了属性名称的参数称之为"命名参数"(Named Parameters)",如果有多个命名参数,我们可以用逗号进行分割。代码中的Path和RelativeSource就是命名参数,XAML会将这 种参数会按照其对应的属性赋值给扩展标记类的实例的属性。扩展标记中的参数本身也可以作为扩展标记(使用大括号标记,例如RelativeSource) 或者作为字符串然后使用类型转换的方式来处理。这种设计和用法和.net中的自定义属性非常类似
之前的Button控件声明中,我们使用NullExtension将Background属性设置为null,这一点是BrushConverter所 无能为力的(其他的类型转换也都存在这类似的问题)。当然这仅仅是为了证明标记扩展的强大能力,将空值赋值给Background的情况并不常见。 StaticExtension可以引用静态的属性,域,常量,枚举等值,这样可以避免在XAML使用硬编码赋值。在示例中的,我们将Button控件的 Height属性设置为当前系统的图标高度;并且使用Binding对象将Content属性的值设置为Height属性的值。Binding对象的使用 方法我们会在第九章的"Data Binding"进行深入介绍
在标记扩展中,我们将那些没有指定属性名称而直接赋值的参数称为"位置参数"(Positional Parameters),上面代码中的SystemParameters.IconHeight就是位置参数。这种参数将被当作一个字符串传递给是标记扩 展类的构造函数;对应的,我们将那些指定了属性名称的参数称之为"命名参数"(Named Parameters)",如果有多个命名参数,我们可以用逗号进行分割。代码中的Path和RelativeSource就是命名参数,XAML会将这 种参数会按照其对应的属性赋值给扩展标记类的实例的属性。扩展标记中的参数本身也可以作为扩展标记(使用大括号标记,例如RelativeSource) 或者作为字符串然后使用类型转换的方式来处理。这种设计和用法和.net中的自定义属性非常类似
之前的Button控件声明中,我们使用NullExtension将Background属性设置为null,这一点是BrushConverter所 无能为力的(其他的类型转换也都存在这类似的问题)。当然这仅仅是为了证明标记扩展的强大能力,将空值赋值给Background的情况并不常见。 StaticExtension可以引用静态的属性,域,常量,枚举等值,这样可以避免在XAML使用硬编码赋值。在示例中的,我们将Button控件的 Height属性设置为当前系统的图标高度;并且使用Binding对象将Content属性的值设置为Height属性的值。Binding对象的使用 方法我们会在第九章的"Data Binding"进行深入介绍
Digging deeper
Escaping the Curly Braces
如果想要将属性值设置成以"{"打头的字符串,而不想让它被识别为标记扩展,需要在{前面添加一个{}.示例 :
如果想要将属性值设置成以"{"打头的字符串,而不想让它被识别为标记扩展,需要在{前面添加一个{}.示例 :
<Button xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
Content=”{}{This is not a markup extension!}”/>
Content=”{}{This is not a markup extension!}”/>
或者是使用属性元素,因为花括号在属性元素的上下文中不会被当成标记扩展。代码如下:
<Button xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”>
{This is not a markup extension!}
</Button>
<Button xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”>
{This is not a markup extension!}
</Button>
标记扩展的实质是类,所以也可以将它作为属性元素来使用,其结果是一样的
<Button xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”>
<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>
Digging Deeper
Markup Extensions and Procedural Code
看一下如何用c#代码来实现之前的XAML代码。
System.Windows.Controls.Button b = new System.Windows.Controls.Button();
// Set Background:
b.Background = null;
// Set Height:
b.Height = System.Windows.SystemParameters.IconHeight;
// Set Content:
System.Windows.Data.Binding binding = new System.Windows.Data.Binding();
binding.Path = new System.Windows.PropertyPath(“Height”);
binding.RelativeSource = System.Windows.Data.RelativeSource.Self;
b.SetBinding(System.Windows.Controls.Button.ContentProperty, binding);
System.Windows.Controls.Button b = new System.Windows.Controls.Button();
// Set Background:
b.Background = null;
// Set Height:
b.Height = System.Windows.SystemParameters.IconHeight;
// Set Content:
System.Windows.Data.Binding binding = new System.Windows.Data.Binding();
binding.Path = new System.Windows.PropertyPath(“Height”);
binding.RelativeSource = System.Windows.Data.RelativeSource.Self;
b.SetBinding(System.Windows.Controls.Button.ContentProperty, binding);
虽然上面c#代码的运行结果和之前XAML中的运行结果是相同的。但其实现机制是不同的,XAML的解析和编译使用的标记扩展并在运行时取值(本质上是调用了ProvideValue方法,该方法在MarkupExtension抽象类中声明)。如果这个机制要用程序代码实现就过于复杂了,所幸没有必要这么做
子元素(Children of Object Elements)
XAML文件和XML文件一样,需要一个根级的对象.所以看到XAML文件中的一个元素包含着另一个元素时就不足为奇了。子元素可以分成3种:内容属性的值,集合项和可以被类型转成父元素的值
内容属性(Content Property)
wpf中的大部分类都有一个可以在XML元素中放置任意内容的属性。这就是"内容属性"(content property)。为了让xaml代码更加简介,可以在赋值时将它省略,这多少和vb中饱受批评的默认属性有些类似。
Button控件的内容属性的使用方法:
<Button xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
Content=”OK”/>
XAML文件和XML文件一样,需要一个根级的对象.所以看到XAML文件中的一个元素包含着另一个元素时就不足为奇了。子元素可以分成3种:内容属性的值,集合项和可以被类型转成父元素的值
内容属性(Content Property)
wpf中的大部分类都有一个可以在XML元素中放置任意内容的属性。这就是"内容属性"(content property)。为了让xaml代码更加简介,可以在赋值时将它省略,这多少和vb中饱受批评的默认属性有些类似。
Button控件的内容属性的使用方法:
<Button xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
Content=”OK”/>
也可以写成:
<Button xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”>
OK
</Button>
<Button xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”>
OK
</Button>
还有些其他的使用方法,用于放置复杂的对象
<Button xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”>
<Button.Content>
<Rectangle Height=”40” Width=”40” Fill=”Black”/>
</Button.Content>
</Button>
或者写成:
<Button xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”>
<Rectangle Height=”40” Width=”40” Fill=”Black”/>
</Button>
<Button xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”>
<Rectangle Height=”40” Width=”40” Fill=”Black”/>
</Button>
从上面的代码可以看出,为内容属性赋值是可以省略"Content"这个标记。像ComboBox,ListBox和TabControl使用Items属性作为它们的内容属性。
集合项(Collection Items)
XAML中支持两种可以使用索引的集合:list和dictionary
集合项(Collection Items)
XAML中支持两种可以使用索引的集合:list和dictionary
Lists
List是指实现了System.Collections.IList的对象。System.Collections.ArrayList就属于 list,WPF中也有许多的实现了IList接口的类。举例来说,ListBox控件的Items属性就实现了IList接口,下面我们向 ListBox中添加两个条目:
<ListBox xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”>
<ListBox.Items>
<ListBoxItem Content=”Item 1”/>
<ListBoxItem Content=”Item 2”/>
</ListBox.Items>
</ListBox>
List是指实现了System.Collections.IList的对象。System.Collections.ArrayList就属于 list,WPF中也有许多的实现了IList接口的类。举例来说,ListBox控件的Items属性就实现了IList接口,下面我们向 ListBox中添加两个条目:
<ListBox xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”>
<ListBox.Items>
<ListBoxItem Content=”Item 1”/>
<ListBoxItem Content=”Item 2”/>
</ListBox.Items>
</ListBox>
等效的c#代码:
System.Windows.Controls.ListBox listbox = new System.Windows.Controls.ListBox();
System.Windows.Controls.ListBoxItem item1 =
new System.Windows.Controls.ListBoxItem();
System.Windows.Controls.ListBoxItem item2 =new System.Windows.Controls.ListBoxItem();
item1.Content = “Item 1”;
item2.Content = “Item 2”;
listbox.Items.Add(item1);
listbox.Items.Add(item2);
System.Windows.Controls.ListBox listbox = new System.Windows.Controls.ListBox();
System.Windows.Controls.ListBoxItem item1 =
new System.Windows.Controls.ListBoxItem();
System.Windows.Controls.ListBoxItem item2 =new System.Windows.Controls.ListBoxItem();
item1.Content = “Item 1”;
item2.Content = “Item 2”;
listbox.Items.Add(item1);
listbox.Items.Add(item2);
Items也是内容属性,所以可以缩写成:
<ListBox xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”>
<ListBoxItem Content=”Item 1”/>
<ListBoxItem Content=”Item 2”/>
</ListBox>
以上这些代码都可以正常运行。这是因为ListBox控件会自动初始化一个空的集合对象分配给Items属性。如果集合属性在初始化时为null(假设集合对象是可读写的,ListBox的Items是只读的),则需要用一个包含项的元素为它初始化
字典(Dictionaries)
System.Windows.ResourceDictionary是wpf常用的集合类型。我们会在第八章"Resources"对其详细介绍。
ResourceDictionary实现了System.Collections.IDictionary接口,支持在程序代码中对元素进行添加,删
除,key/value的操作。和操作哈希表类似。
在XAML中,只要实现了IDictionary接口的对象都可以对其进行添加 key/value的操作:下面的代码将两个Color对象添加到了ResourceDictionary
<ResourceDictionary
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”>
<Color x:Key=”1” A=”255” R=”255” G=”255” B=”255”/>
<Color x:Key=”2” A=”0” R=”0” G=”0” B=”0”/>
</ResourceDictionary>
在XAML中,只要实现了IDictionary接口的对象都可以对其进行添加 key/value的操作:下面的代码将两个Color对象添加到了ResourceDictionary
<ResourceDictionary
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”>
<Color x:Key=”1” A=”255” R=”255” G=”255” B=”255”/>
<Color x:Key=”2” A=”0” R=”0” G=”0” B=”0”/>
</ResourceDictionary>
xaml中Key关键字(对应xml中的第二个命名空间)可以让我们为Color添加一个key(Color对象本身没有Key属性)。等效的c#程序代码:
System.Windows.ResourceDictionary d = new System.Windows.ResourceDictionary();
System.Windows.Media.Color color1 = new System.Windows.Media.Color();
System.Windows.Media.Color color2 = new System.Windows.Media.Color();
color1.A = 255; color1.R = 255; color1.G = 255; color1.B = 255;
color2.A = 0; color2.R = 0; color2.G = 0; color2.B = 0;
d.Add(“1”, color1);
d.Add(“2”, color2);
System.Windows.ResourceDictionary d = new System.Windows.ResourceDictionary();
System.Windows.Media.Color color1 = new System.Windows.Media.Color();
System.Windows.Media.Color color2 = new System.Windows.Media.Color();
color1.A = 255; color1.R = 255; color1.G = 255; color1.B = 255;
color2.A = 0; color2.R = 0; color2.G = 0; color2.B = 0;
d.Add(“1”, color1);
d.Add(“2”, color2);
更多类型转换(More Type Conversion )
我们经常在XAML中使用纯文本来表示子元素的值。例如下面使用XAML代码声明一个SolidColorBrush对象
<SolidColorBrush>White</SolidColorBrush>
等效代码:
<SolidColorBrush Color=”White”/>
<SolidColorBrush Color=”White”/>
在第一段代码中,我们没有将Color指定给SolidColorBrush的内容,类型转换会将"White"这个字符串转换成Color,并传递给
SolidColorBrush对象。 虽然类型转换可以提高XAML代码的可读性,但这过于神奇了,会让很多人感到困惑。难以理解它是如将字符串映射为
相应的.NET对象的。从我们现有的知识可以得到如下推论:因为抽象类是无法实例化的,所以我们不能在XAML代码中声明一个抽象类的元素...但实际上
不是这样的。System.Windows.Media.Brush是SolidColorBrush和GradientBrush等类的抽象类,但我们
却可以编写这样的XAML代码:
<Brush>White</Brush>
上面的代码是正确的。因为Brush类型的类型转换知道这实质上是SolidColorBrush类,所以能够在运行时进行正确的转换。这个特性可以让 XAML支持原始类型的表达,但并不常用。我们会在下面的"The Extensible Part of XAML"中介绍
<Brush>White</Brush>
上面的代码是正确的。因为Brush类型的类型转换知道这实质上是SolidColorBrush类,所以能够在运行时进行正确的转换。这个特性可以让 XAML支持原始类型的表达,但并不常用。我们会在下面的"The Extensible Part of XAML"中介绍
DIGGING DEEPER
The Extensible Part of XAML
XAML在设计时被设计成可以和.net的类型系统一起工作,可以转换成任意的.NET对象(甚至是COM对象,这要归功于COM的互操作性),也包括自 定义类型。如果要想自定义的类型被XAML支持,需要将他设计成声明友好(declarative-friendly)式的。如果这个类没有默认构造函数 或者没有暴露可用的实例属性,则不能直接在XAML中使用。所以在设计WPF的API时要考虑地非常周详,以支持XAML的声明模型
WPF的程序集被XmlnsDefinitionAttribute属性类所修饰,XmlnsDefinitionAttribute属性类用于将. NET的命名空间映射到XAML中。但是现在存在一个问题:并不是所有的程序集都是为了XAML所设计的,那么怎么才能在XAML中应用这些类?答案是将 他们的命名空间和程序集名称引用到XML文件中。下面的c#示例Hashtable,该类属于mscorlib.dll程序集:
System.Collections.Hashtable h = new System.Collections.Hashtable();
h.Add(“key1”, 7);
h.Add(“key2”, 23);
XAML在设计时被设计成可以和.net的类型系统一起工作,可以转换成任意的.NET对象(甚至是COM对象,这要归功于COM的互操作性),也包括自 定义类型。如果要想自定义的类型被XAML支持,需要将他设计成声明友好(declarative-friendly)式的。如果这个类没有默认构造函数 或者没有暴露可用的实例属性,则不能直接在XAML中使用。所以在设计WPF的API时要考虑地非常周详,以支持XAML的声明模型
WPF的程序集被XmlnsDefinitionAttribute属性类所修饰,XmlnsDefinitionAttribute属性类用于将. NET的命名空间映射到XAML中。但是现在存在一个问题:并不是所有的程序集都是为了XAML所设计的,那么怎么才能在XAML中应用这些类?答案是将 他们的命名空间和程序集名称引用到XML文件中。下面的c#示例Hashtable,该类属于mscorlib.dll程序集:
System.Collections.Hashtable h = new System.Collections.Hashtable();
h.Add(“key1”, 7);
h.Add(“key2”, 23);
等效的的XAML代码:
<collections:Hashtable
xmlns:collections=”clr-namespace:System.Collections;assembly=mscorlib”
xmlns:sys=”clr-namespace:System;assembly=mscorlib”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”>
<sys:Int32 x:Key=”key1”>7</sys:Int32>
<sys:Int32 x:Key=”key2”>23</sys:Int32>
</collections:Hashtable>
上面的XAML代码中的"clr-namespace"字符串允许我们将任意一个.NET的命名空间添加到XAML中。如果需要使用的.NET命名空间所 依附的程序集不在XAML默认的程序集内,还需要在命名空间后面添加它的程序集名称(例如mscorlib)。这里使用了简名,当然也可以使用 System.Reflection.Assembly.Load方法支持的规范名称。
示例证明了XAML不但与.NET类型是继承的,它也和.NET Framework基础类库进行了集成:
1 可以使用XAML的X:Key语法将子元素添加到父元素的Hashtable中。原因是:Hashtable和其他集合类实现了IDictionary接口。
2 代码中之所以可以使用System.Int32类型,是因为类型转换可以将字符串转换成整数;这些类型转换类继承自System.ComponentModel.TypeConverter类。
- 如果实现了IList接口,可以调用IList.Add方法添加子项
- 如果没有实现IList接口而实现了IDictionary,可以调用IDictionary.Add添加子项。在XAML中需要使用x:Key 属性。
- 既没有实现IList接口,也没有实现IDictionary接口。在这种情况下,如果父元素支持内容属性并且子元素和这个属性兼容,可以让把子元素赋值给父元素的内容属性。
- 如果前3个条件都不成立,并且子元素是以纯文本形式出现,并且类型转换能够将它转换成父类型。则子元素是类型转换类的输入参数,而结果就是父元素的实例。
- 出错
我们在上文中列举了3中类型的子元素。为了避免混淆,有以下几条规则需要遵循: