目录:点击这里
上一篇:【翻译】Pro.Silverlight.5.in.CSharp.4th.Edition - 第二章 XAML 01
复杂属性
尽管类型转换器使用很方便,但是并不能满足所有的情况。比如有一些属性是自身就带有一堆属性的一个完整对象,尽管创建一个字符串用来代表类型转换器确实没问题,但是这个语法会复杂得一塌糊涂而且很容易出错。
好在XAML给我们提供了另一个选择:属性-元素(property-element)语法。使用属性-元素语法,我们可以增加一个命名是【父元素.属性名】(Parent.PropertyName)这样的格式的子元素。比如,Grid的Background属性支持用笔刷绘制元素的背景区域。如果你想使用一个比较复杂的笔刷——比纯色填充更丰富的那种——你就需要增加一个名为“Grid.Background”的子标签。就像这样:
<Grid x:Name="grid1"> <Grid.Background> ... </Grid.Background> ... </Grid>
元素名中的那个句点(.)是关键,用来和嵌套的内容中的其他类型区分开。
另外还有一个细节,就是当你找到了要配置的复杂属性时,具体该如何设置?这个技巧这么玩:在嵌套的元素里面,我们增加另一个标签,本质上就相当于实例化了一个特定的类。在图2-1所示的eight ball示例中,背景是由一个渐变效果填充的。要定义你所需要的渐变效果,你必须创建一个LinearGradientBrush对象。
根据XAML的规则,我们可以通过使用一个名为LinearGradientBrush的元素来创建一个LinearGradientBrush对象:
<Grid x:Name="grid1"> <Grid.Background> <LinearGradientBrush> </LinearGradientBrush> </Grid.Background> ... </Grid>
LinearGradientBrush属于Silverlight那一系列命名空间中的一部分,因此我们可以让这个标签使用默认的XML命名空间(换句话说就是前面没有命名空间的前缀,比如x)。
到目前来说,这么标签内容还不足以创建一个可用的LinearGradientBrush渐变效果的笔刷——还有填色这项工作没有完成。具体来说就是要给LinearGradientBrush的GradientStops这个集合性质的属性增加一系列GradientStop对象的元素。这种情况下我们需要使用property-element语法:
<Grid x:Name="grid1"> <Grid.Background> <LinearGradientBrush> <LinearGradientBrush.GradientStops> <GradientStop Offset="0.00" Color="Yellow" /> <GradientStop Offset="0.50" Color="White" /> <GradientStop Offset="1.00" Color="Purple" /> </LinearGradientBrush.GradientStops> </LinearGradientBrush> </Grid.Background> ... </Grid>
备注:任何属性都可以使用属性元素语法。但是通常情况下,我们只对应用了合适的类型转换器特性的属性用这种处理方式,因为只有针对这样的属性来处理才能让代码更紧凑。
任何XAML标签都可以等价地用一些代码段来代替实现同样的功能。比如前面那个用渐变效果填充背景的示例的那些XAML标签内容就可以用如下的代码来代替:
LinearGradientBrush brush = new LinearGradientBrush(); GradientStop gradientStop1 = new GradientStop(); gradientStop1.Offset = 0; gradientStop1.Color = Colors.Yellow; brush.GradientStops.Add(gradientStop1); GradientStop gradientStop2 = new GradientStop(); gradientStop2.Offset = 0.5; gradientStop2.Color = Colors.White; brush.GradientStops.Add(gradientStop2); GradientStop gradientStop3 = new GradientStop(); gradientStop3.Offset = 1; gradientStop3.Color = Colors.Purple; brush.GradientStops.Add(gradientStop3); grid1.Background = brush;
附加属性
除了普通的属性之外,XAML还提供了另一个概念:附加属性(attached properties)——可以应用于多个元素但是在另一个不同的类中定义的属性。在Silverlight中附加属性经常用于控件的布局。
每个控件都有一系列自己固有的属性。比如,文本输入框的字体、文字颜色以及本文内容,这些分别是通过FontFamily,Foreground和Text这些属性来加以体现的。当我们把一个控件放置在一个容器中的时候,这个控件就会额外获得一些依赖于这个容器的类型的特性。再把栗子举起来,如果我们把一个TextBox放在Grid中,我们就可以设置这个TextBox的单元格的位置。这些额外增加的细节就是用附加属性来完成的。
附加属性的命名通常由两部分组成,用句读(.)隔开,格式是:指定的类型.属性名(DefiningType.PropertyName)。这种两部分命名的语法使得XAML解析器能够将普通的属性和附加属性区别开来。
在eight ball的示例中,那些单独的元素通过附加属性将各自置于Grid(不可见的网格)的不同行。
<TextBox ... Grid.Row="0"> </TextBox> <Button ... Grid.Row="1"> </Button> <TextBox ... Grid.Row="2"> </TextBox>
从本质上来讲,其实附加属性不是真正的属性。它们实际上会被翻译成方法来调用。XAML解析器在碰到附加属性的时候会调用DefiningType.SetPropertyName()这种格式的静态方法。比如,在之前的XAML片段中,所指定的类型是Grid类,属性是Row,据此可推导出解析器调用的方法是Grid.SetRow()。
调用SetPropertyName()的时候,解析器会传入两个参数:一个是正要修改的那个对象,另一个是要修改的属性值。例如,当你要设置txtQuestion这个文本框控件的Grid.Row属性值为0的时候,XAML解析器执行的代码就是这样的:
Grid.SetRow(txtQuestion, 0);
这种调用指定的类型的一个静态方法的模式非常方便,因为它将真实发生的动作隐藏起来了。乍一看这段标记还以为是行号存在Grid对象中;其实,这个行号是保存在它所应用的那个对象上——本例中是那个TextBox对象。
这招之所以起作用是因为TextBox继承于DependencyObject这个基类;其实是所有的Silverlight元素都继承于这个基类。DependencyObject是用于存储依赖属性(dependency property)的集合,其容量基本上没有限制;而附加属性本身则是依赖属性的其中一种。
还有一个事实使我们必须要了解的:Grid.SetRow()这个方法本质是和DependencyObject.SetValue()方法一样。前者可以理解为后者的一种快捷的写法。在上面设置txtQuestion的Grid.Row这个附加属性的那段代码如果用后面这种方式来写,则是这个样子:
txtQuestion.SetValue(Grid.RowProperty, 0);
附加属性这个概念是Silverlight的一个核心技术,相当于一个通用的具有扩展性的系统的角色。比如任何控件都可以通过附加属性的方式来定义行的属性。而另外一种选择——将它作为一个基类(比如FrameworkElement)的一部分,则会弄得很复杂。一方面会因为那些只在特殊场合才有意义的属性将整个公共接口弄得混乱不堪(比如本例中就是当元素是放在Grid中的情况下才用到“行”这样一个属性的设置);另一方面,这样处理也会导致无法增加要使用新属性的新类型容器。
嵌套元素
如你所见,XAML文档是由一个深度嵌套的元素树形成的。在当前我们所讨论的例子中,UserControl元素中包含了一个Grid元素,这个Grid元素则又包含了TextBox元素和Button元素。
XAML允许每个元素自行处理自己内部的元素嵌套的方式,处理原则是按照下面三条机制(按照顺序来决定原则的优先级):
- 如果父元素实现了IList<T>接口,那么解析器调用IList<T>.Add()方法,传入的参数为子元素。
- 如果父元素实现了IDictionary<T>接口,那么解析器调用IDictionary<T>.Add()方法,传入的参数是子元素。在使用字典集合的时候,必须设置x:Key属性以保证每个项都有一个key名称。
- 如果父元素应用了ContentProperty特性,解析器会使用子元素来设置这个属性。
比如说,本章之前介绍的示例中LinearGradientBrush可以包含一组GradientStop对象的集合,其语法如下:
<LinearGradientBrush> <LinearGradientBrush.GradientStops> <GradientStop Offset="0.00" Color="Yellow" /> <GradientStop Offset="0.50" Color="White" /> <GradientStop Offset="1.00" Color="Purple" /> </LinearGradientBrush.GradientStops> </LinearGradientBrush>
XAML解析器识别出LinearGradientBrush.GradientStops是一个复杂属性,因为他包含一个句号(.)。不过,他在内部标签(三个GradientStop元素)的处理上稍有不同。本例中,解析器识别出GradientStops属性返回的是一个GradientStopCollection对象,而这个GradientStopCollection实现了IList接口,因此根据上面的原则,解析器会知道要通过IList.Add()方法来将每个GradientStop元素添加到集合中:
GradientStop gradientStop1 = new GradientStop(); gradientStop1.Offset = 0; gradientStop1.Color = Colors.Yellow; IList list = brush.GradientStops; list.Add(gradientStop1);
有些属性可能支持不止一种集合。如果这样的话,我们就得增加一个标签来明确指定集合的类名,如下面所示(粗体部分):
<LinearGradientBrush> <LinearGradientBrush.GradientStops> <GradientStopCollection> <GradientStop Offset="0.00" Color="Yellow" /> <GradientStop Offset="0.50" Color="White" /> <GradientStop Offset="1.00" Color="Purple" /> </GradientStopCollection> </LinearGradientBrush.GradientStops> </LinearGradientBrush>
备注:如果集合默认是Null,那么我们也需要将指定集合类的标签加上,从而创建出集合对象。如果已经有一个集合的默认实例并且只需要将它填充,那么我们可以忽略这一部分。
嵌套内容并不是一直都是集合这样的形式。比如说一个Grid中包含各种其它的元素,示例如下:
<Grid x:Name="grid1"> ... <TextBox x:Name="txtQuestion" ... > </TextBox> <Button x:Name="cmdAnswer" ... > </Button> <TextBox x:Name="txtAnswer" ... > </TextBox> </Grid>
这些嵌套的标签并不是复杂属性,因为它们不包含句号(.)。此外,Grid控件也不是一个集合,因此它没有实现IList接口和IDictionary接口。Grid真正支持的是ContentProperty特性,这表明对应的属性可以接受任何嵌套的内容。技术上,ContentProperty特性是由Panel类所提供(Grid其实是继承于Panel),其实现是这样的:
[ContentPropertyAttribute("Children")] public abstract class Panel : FrameworkElement {...}
这表明任何嵌套元素都可以用来设置Children这个属性。根据内容属性是否是一个集合性质的属性(换句话说,就是它是否实现了IList或者IDictionary接口)这一情况的不同,XAML解析器在内容属性的处理上也有所不一样。由于Panel.Children属性返回的是一个UIElementCollection,而这个UIElementCollection实现了IList接口,所以解析器会使用IList.Add()方法来往Grid中添加嵌套内容。
换句话说,当XAML解析器碰到前面那段标记的时候,它会创建每个嵌套元素的一个实例并且将这个示例作为参数传给Grid.Children.Add()这个方法:
txtQuestion = new TextBox(); ... grid1.Children.Add(txtQuestion); cmdAnswer = new Button(); ... grid1.Children.Add(cmdAnswer); txtAnswer = new TextBox(); ... grid1.Children.Add(txtAnswer);
接下来发生的事情完全取决于控件实现内容属性的方式。在第3章中你会看到Grid通过一个不可见的行列布局显示出所有的元素。
使用可视化树(visual tree helper)浏览嵌套元素
Silverlight提供了一个VisualTreeHelper类来帮助我们了解所有元素的层级结构。这个VisualTreeHelper类提供了三个静态方法:GetParent(),用于返回制定的元素的父元素;GetChildrenCount(),用于返回制定的元素里面所嵌套的元素个数;第三个是GetChild(),通过位置的索引值来获取它的嵌套元素中的其中某一个。
VisualTreeHelper的优点是它在一个很通用的方式下运行,支持所有Silverlight元素,不管元素用的是什么内容模型(content model)。比如说,你可能已经知道的:list控件通过Items属性来展现元素集合,布局容器通过children属性来展现出它的子元素集合,而内容控件则通过Content这个属性来展现它的嵌套内容元素。只有VisualTreeHelper能使用相同的无缝代码来把这三种方式都挖掘出来。
不过,它有个缺点,就是会获取元素的视觉成分的所有细节,包括那些不重要的部分。比如说,当我们使用VisualTreeHelper浏览ListBox的时候,我们会碰到一些并不是我需要的细节,比如用于画边框的Border,实现滚动效果的ScrollViewer以及每行中用于布局行中内容的Grid。由于这个原因,在实际使用中我们只能用循环查询来处理——遍历集合直到找到你要的东西,找到后再进行我们其他逻辑的处理。下面的示例就是用这个方式来实现将一组层级的元素中的所有TextBox的文本清空:
private void Clear(DependencyObject element) { //如果是TextBox,就想Text清空 TextBox txt = element as TextBox; if (txt != null) txt.Text = ""; // 检查嵌套的子元素 int children = VisualTreeHelper.GetChildrenCount(element); for (int i = 0; i < children; i++) { DependencyObject child = VisualTreeHelper.GetChild(element, i); Clear(child); } }调用Clear()方法,传入的参数是你要处理的XAML标记的范围的最外层的那个对象。比如下面这个语句所处理的范围是整个页面:
Clear(this);
事件
到目前为止,我们所看到的所有的attribute都映射到相应的property上了。其实,attribute还可以用于注册事件处理程序。语法是【事件名称=”事件句柄对应的方法名称”】(EventName="EventHandlerMethodName")
比如,Button控件支持Click事件。我们就可以用这样的语句来注册Click事件:
<Button ... Click="cmdAnswer_Click">
这意味着在相应的code-hehind类中应该有一个名为“cmdAnswer_Click”的方法。而且这个事件处理程序必须有正确的函数前面(意思就是必须和Click事件的委托要匹配上)。示例如下:
private void cmdAnswer_Click(object sender, RoutedEventArgs e) { AnswerGenerator generator = new AnswerGenerator(); txtAnswer.Text = generator.GetRandomAnswer(txtQuestion.Text); }
在许多情况下,我们会在同一个元素中既用attribute来设置property,也会用它来注册事件处理程序。在Silverlight中通常的顺序是:先设置Name属性(如果需要的话),然后注册需要的事件处理程序,最后再设置其他的属性(Name之外的)。这就意味着如果注册了和某个属性值改变相关的事件,那么在首次设置这个属性的时候,会有相应的方法被调用到。
完整的Eight Ball示例
现在,根据目前所学到的内容,我们完全可以开始了解XAML的整体结构。就以图2-1所对应的XAML页面来研究:
<UserControl x:Class="EightBall.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <Grid x:Name="grid1"> <Grid.RowDefinitions> <RowDefinition Height="*" /> <RowDefinition Height="Auto" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <TextBox VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Margin="10,10,13,10" x:Name="txtQuestion" TextWrapping="Wrap" FontFamily="Verdana" FontSize="24" Grid.Row="0" Text="[Place question here.]"> </TextBox> <Button VerticalAlignment="Top" HorizontalAlignment="Left" Margin="10,0,0,20" Width="127" Height="23" x:Name="cmdAnswer" Click="cmdAnswer_Click" Grid.Row="1" Content="Ask the Eight Ball"> </Button> <TextBox VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Margin="10,10,13,10" x:Name="txtAnswer" TextWrapping="Wrap" IsReadOnly="True" FontFamily="Verdana" FontSize="24" Foreground="Green" Grid.Row="2" Text="[Answer will appear here.]"> </TextBox> <Grid.Background> <LinearGradientBrush> <LinearGradientBrush.GradientStops> <GradientStop Offset="0.00" Color="Yellow" /> <GradientStop Offset="0.50" Color="White" /> <GradientStop Offset="1.00" Color="Purple" /> </LinearGradientBrush.GradientStops> </LinearGradientBrush> </Grid.Background> </Grid> </UserControl>
记住,我们应该不会纯粹手工来写一个丰富的图形用户界面的完整XAML标记(这么搞的话会让自己特别伤不起)。但是另一方面,有时候,有一些修改工作如果让设计美工来做会特别棘手,这种情况下我们得自己会修改XAML代码我们所需要的功能调整;我们也需要通过修改XAML来让自己对页面的工作方式有更好的认识和其他想法。