说明:本系列基本上是《WPF揭秘》的读书笔记。在结构安排与文章内容上参照《WPF揭秘》的编排,对内容进行了总结并加入一些个人理解。
XAML中子内容的处理由其父对象来决定,这就需要父元素类型实现System.Windows.Serialization命名空间的IAddChild接口或是实现标准的集合接口 – Icollection。如果试图添加子元素到一个没有实现这两种接口的元素类型,编译器将会报错。
XAML中元素处理其子元素/内容基本上就是以这两种通用的模式来完成,实现这个处理功能不是由XAML来完成的,而是由于父元素实现了.NET类库中两个接口才得以实现。
IAddChild接口定义了两个方法:AddChild和AddText,AddChild用来添加元素,AddText用来添加普通文本。编译器为每段子内容调用这两个方法。
示例代码:
XAML:
<Window x:Class="WpfApplication1.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window1" Height="300" Width="300"> <Grid> </Grid> </Window> |
C#:(演示)
Window1 myWindow = new Window1(); myWindow.AddChild(new Grid()); |
Window元素(继承自ContentControl类的类型都可采用这种方式)也可以通过设置Content属性的方式来实现AddChild(Window将检测自身是否已包含子对象,因为Window元素的Content属性只支持单一对象,试图添加更多的对象将抛出异常)。这是下文将要重点介绍的子元素3大模式之一的 – 内容属性。
示例代码:(与前文代码等效,加入了粗体部分来演示内容属性的本质,语法上来看和属性元素一样)
<Window x:Class="WpfApplication1.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window1" Height="300" Width="300"> <Window.Content> <Grid> </Grid> </Window.Content> </Window>
|
C#:
Window1 myWindow = new Window1(); myWindow.Content = new Grid(); |
通过这种设置属性的方式也可以实现将Grid对象添加为Window的子元素。
XAML中,一个对象元素可以有3种类型的子元素:
1. 内容属性值
2. 集合项
3. 能够通过类型转换到它的父元素的值。
下文将分别详细介绍。
1. 内容属性
WPF中很多类指定了一个属性,通过设置该属性的值可以给此类的元素来设置内容,这个特性称为内容属性。在某些情况下内容属性是比属性元素更简单的表示方法。将属性作为内容属性的方法是使用System.Windows.Markup.ContentPropertyAttribute标记这个属性。
对比下面两组例子:
第一:普通方式(以Xml Attribute方式)设置属性的方式改写为内容属性的表示方式:
XML Attribute:
<Button Content="OK"/> |
内容属性方式:
<Button>OK</Button> |
第二:属性元素方式设置属性的方式改写为内容属性的表示方式
属性元素:
<Button> <Button.Content> <Rectangle Height="40" Width="40" Fill="Black"/> </Button.Content> </Button> |
重写为内容属性形式:
<Button> <Rectangle Height="40" Width="40" Fill="Black"/> </Button> |
另外,并不一定只有Content属性被设计为内容属性,像ComboBox, ListBox和TabControl等类将其Items属性作为内容属性。
注意像Button、Window等从ContentControl继承的元素都只能包含一个单独的子元素,根本原因在于它们的子元素被作为内容属性(Content属性),而只能给Content属性指定一个值。当添加的元素超过一个会抛出异常。
另外,WPF中继承自Panel基类的布局容器将Children属性作为内容属性,所以有如下写法。(Children属性不像Content属性那样被限制只能指定一个值)
属性元素写法:
<StackPanel> <StackPanel.Children> <Button>Foo</Button> <Button>Bar</Button> </StackPanel.Children> </StackPanel> |
内容属性写法:
<StackPanel> <Button>Foo</Button> <Button>Bar</Button> </StackPanel> |
2. 集合项
有一些类使用相同的方式表现子项的集合。如:XAML中两种类型的集合 - List与Dictionary支持向其中添加项。
List
Itmes集合
List指实现了System.Collection.IList接口的集合,如System.Collections.ArrayList类及WPF中定义的ListBox控件,RadioButtonList控件,ComboBox控件及TabControl控件。当添加一个子项到这些元素时,IAddChild接口就会将子项添加到Items集合中。
一般来说,设置它们的Items属性可以使用属性元素。如下示例:
向ListBox中添加两项:
<ListBox> <ListBox.Items> <ListBoxItem Content="Item1"/> <ListBoxItem Content="Item2"/> </ListBox.Items> </ListBox> |
因为Items是ListBox的内容属性,所以可以将XAML简化为内容属性的写法:
<ListBox> <ListBoxItem Content="Item1"/> <ListBoxItem Content="Item2"/> </ListBox> |
这时ListBox的Items属性首先会自动被初始化为一个空的集合对象,这样代码可以正常工作。
Dictionary
Dictionary指的是实现了System.Collections.IDictionary接口的集合。下面以WPF中一个常用的集合类型System.Windows.ResourceDictionary为例,来演示使用XAML向Dictionary中添加键值对的方式,以下代码直接给出内容属性的写法(粗体部分):
<Grid> <Grid.Resources> <ResourceDictionary> <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> </Grid.Resources> </Grid> |
此XAML使用了经过特殊处理的XAML Key关键字(定义于次级命名空间xmlns:x中),从而可以为每个Color值添加一个键。带有x:Key的XAML中指定的值总是被作为字符串处理,其不会尝试使用类型转换器,要想按其它类型处理只能使用标记扩展。
下面是上面XAML等价的C#代码:
System.Windows.ResourceDictionary d = new ResourceDictionary(); System.Windows.Media.Color color1 = new Color(); System.Windows.Media.Color color2 = new 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); |
Collections属性
当一个属性是集合类型(实现ICollection接口)时,通常不需要为集合自身再提供一个元素,而是让属性名来控制集合,通常用于只有一个属性的元素。(如下示例中,加粗代码表示的是不用再提供的元素)
注:例子使用属性元素,这样使示例代码的来龙去脉更清晰。(属性元素加虚下划线,去掉即是子内容的表示方式)
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1"> <LinearGradientBrush.GradientStops> <GradientStopCollection> <GradientStop Color="Black" Offset="0" /> <GradientStop Color="Red" Offset="1" /> </GradientStopCollection> </LinearGradientBrush.GradientStops> </LinearGradientBrush> |
所以最简单的子内容表示方法:
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1"> <GradientStop Color="Black" Offset="0" /> <GradientStop Color="Red" Offset="1" /> </LinearGradientBrush> |
这个例子中GradientStops是集合类型的属性,所以我们不需要在提供GradientStopCollection这一级元素,而是通过集合的子元素GradientStop直接控制集合。
3. 支持类型转化的普通文本
首先看一下下面这个XAML中声明的SolidColorBrush:
<SolidColorBrush>White</SolidColorBrush> |
这段代码看起来很像上文讲到的内容属性,但是其等价代码
<SolidColorBrush Color="White"/> |
中的Color属性并没有被指定为内容属性。这个代码之所以能工作是因为类型转换器会将字符串"White"转换为SolidColorBrush对象。另外,由于System.Windows.Media.Brush是SolidColorBrush, GradientBrush和其他一些具体笔刷的基类,故也可以将上面代码用如下来表示来替换:
<Brush>White</Brush> |
Brush类型转化器将其理解为SolidColorBrush。
下表总结了XAML中元素的子元素的处理规则:
当转换子元素时,任何一个有效的XAML分析器必须遵循下面的规则: (1) 如果该类型实现了ILits接口,就为每个子元素调用IList.Add。 (2) 否则,如果该类型实现了IDictionary,就为每个子元素调用IDictionary.Add,在该值的键和元素中使用x:Key特性值。 (3) 否则,如果父元素支持内容属性,而且子元素的类型与该内容属性是兼容的,就把子元素作为内容属性的值。 (4) 否则,如果子对象是普通文本,且有类型转换器将子对象转换为父元素的类型,则把子元素作为类型转换器的输入,将输出作为父元素的实例(没有在父元素上设置属性,子元素只相当于初始化父元素的一个参数)。 (5) 其他情况下,则抛出一个异常。 |
参考:
《WPF揭秘》