《深入浅出WPF》——XAML语法及基础知识
文章目录
- 一、 剖析一段简单的XAML代码
- 二、 XAML中为对象属性赋值的语法
- 2.1. 使用标签的Attribute为对象进行赋值
- 🔺2.2. 使用TypeConverter类将XAML标签的Attribute与对象的Property进行映射
- 2.3. 属性元素
- ⭐2.4. 标记扩展
- 三、 事件处理器与代码后置
- ⭐四、导入程序集和引用其中的名称空间
- 五、XAML的注释
- 六、小结
一、 剖析一段简单的XAML代码
下面是一段简单的xaml代码:
<Window x:Class="DManager.View.LedgerView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="700"
Title="Window1">
<Grid>
</Grid>
</Window>
这一大段代码,其中还有两个看着像主页地址的东西……它们都是些什么呢?让我们来一个一个地分析。
XAML是一种由XML派生而来的语言,所以很多XML中的概念在XAML是通用的。比如,使用标签声明一个元素(每个元素对应内存中的一个对象)时,需要使用起始标签<Tag>和终止标签</Tag>,夹在起始标签和终止标签中的XAML代码表示隶属于这个标签的内容。如果没有什么内容,则这个标签为空标签,可以写为<Tag/>
为了表示同类标签中的某个标签与众不同,可以给它的特征/特性(Attribute)赋值。为特征赋值的语法如下:
- 非空标签
<Tag Attribute1=Value1 Attribute2=Value2>Content</Tag>
- 空标签:
<Tag Attribute1=Value1 Attribute2=Value2 />
在这里,有必要把Attribute和Property这两个词仔细地辨别一下。
这两个词的混淆由来已久。混淆的主要原因就是大多数中文译本里既把Attribute译为“属性”,也把Property译为“属性”。其实这两个词表达的不是一个层面上的东西。
不过这种专业词汇看翻译软件的翻译意思往往意义不大,应该不太会有人看着英文专业文档,然后把百度翻译来的意思直接代入吧,虽然在大部分专业无关词汇的情况下,往往没问题。但一旦涉及某些小众的含义,就会出现混淆、甚至难以理解的现象。这个时候,不将其翻译往往会好一些,翻译成中文本来就会发生语义的转变。这时的Attribute就是Attribute啊,哪是什么属性。
言归正传,Property属于面向对象理论范畴。在使用面向对象思想编程时,常常需要对客观事物进行抽象,再把抽象出来的结果封装成类,类中用来表示事物状态的成员就是Property。比如要写一个模拟赛车的游戏,那么必不可少的就是对现实汽车的抽象。现实中的汽车身上会带有很多数据,但在游戏中可能只关心它的长度、宽度、高度、重量、速度等有限的几个数据,同时,还会把汽车“加速”、“减速”等一些行为也提取出来并用算法模拟,这个过程就是抽象(结果就是Car这个类)。显然,Car.Length、Car.Height、Car.Speed等表达的是汽车当前处在一个什么状态,而Car.Accelerate()、Car.Break()表达的是汽车能做什么。因此,Car.Length、Car.Height、Car.Speed就是Property的典型代表,将Property译为“属性”也很贴切。
总结一句话就是:Property(属性)是针对对象而言的,是对象的外在表现。
Attribute则是编程语言文法层面的东西。比如有两个同类的语法元素A和B,为了表示A与B不完全相同或者A与B在用法上有些区别,这时就要针对A和B加一些Attribute。也就是说,Attribute只与语言层面上的东西相关,与抽象出来的对象没什么关系。因为Attribute是为了表示“区分”的,所以这里把它翻译成“特征”。C#中的Attribute就是这种应用的典型例子,我们可以为一个类添加Attribute,这个类的类成员中有许多Property。显然Attribute只是用来影响类在程序中的用法,而Property则对应着抽象对象身上的性状,它们根本不是一个层面的东西。
实话讲,Attribute还是很难理解。
或许是因为我编译原理没有学好,一开始就被文法、语法给弄晕了。
不过从应用上区分它们并不难。
目前只知道,Property是针对面向对象而言的,是对象的外在体现,翻译成属性更佳。
Attribute是语言层面的,翻译成特征更佳。
习惯上,英文中把标签式语言中表示一个标签特征的“名称-值”对称作Attribute。如果恰好又是用一种标签语言在进行面向对象编程,这时两个概念就有可能混淆在一起了。实际上,使用能够进行面向对象编程的标签式语言只是把标签与对象做了一个映射,同时把标签的Attribute与对象的Property也做了一个映射——针对标签还是叫Attribute,针对对象还是叫Property,仍然不是一个层面上的东西。而且,标签的Attribute与对象的Property也不是完全映射的,往往是一个标签所具有的Attribute多于它所代表的对象的Property。
因为XAML是用来在UI上绘制控件的,而控件本身就是面向对象抽象的产物 (这句话挺有意思的),所以XAML标签的Attribute里就有一大部分是与控件对象的Property互相对应的。当然,这还意味着XAML标签还有一些Attribute并不对应控件对象的Property。
XAML是一种“声明”式语言,当你见到一个标签,就意味着声明了一个对象,对象之间的层级关系要么是并列、要么是包含,全都体现在标签的关系上。
下面这些代码都是<Window>标签的Attribute。
x:Class="DManager.View.LedgerView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="700"
Title="Window1"
其中,Height、Title一看就知道是与Window对象的Property相对应的。中间两行(即两个xmlns)是在声明名称空间。最上面一行是在使用名为Class的Attribute,这个Attribute来自x:前缀对应的名称空间。下面仔细解释。
前面说过,XAML语言是从XML语言派生出来的。XML语言有一个功能就是可以在XML文档的标签上使用xmlns特征来定义名称空间/命名空间(Namespace),xmlns也就是XML-Namespace的缩写了。定义命名空间的好处就是,当来源不同的类重名时,可也使用名称空间加以区分。xmlns特征的语法格式如下:
xmlns[:可选的映射前缀]="名称空间"
xmlns后可以跟一个可选的映射前缀,之间用冒号分隔。如果没有写可选的映射前缀,那就意味着所有来自于这个名称空间的标签前都不用加前缀,这个没有映射前缀的名称空间称为“默认名称空间”——默认名称空间只能有一个,而且应该选择其中元素被最频繁使用的名称空间来充当默认名称空间。文章开头的代码中,<Window>和<Grid>都来自由第二行声明的默认名称空间。而第一行中的Class特征则来自于第三行中x:前缀对应的名称空间。这里可以做一个小实验:如果给第二行声明的名称空间加上一个前缀,比如n,那么代码就必须改成这样才能编译通过:
<n:Window x:Class="DManager.View.LedgerView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="700"
Title="Window1">
<n:Grid>
</n:Grid>
</n:Window>
XAML中引用外来程序集和其中.NET名称空间的语法与C#是不一样的。在C#中,如果想使用System.Windows.Controls名称空间里的Button类,需要先把包含System.Windows.Controls名称空间的程序集PresentationFramework.dll通过添加引用的方式引用到项目中,然后再在C#代码的顶部写上一句using System.Windows.Controls;。在XAML中做同样的事情也需要先添加对程序集的引用,然后再在根元素的起始标签中写上一句:xmlns:c="clr-namespace:System.Windows.Controls;assembly=PresentationFramework"
。c是映射前缀,换成其他的字符串(如control)也可以。因为Button来自前缀c对应的名称空间,所以在使用Button的时候就要写成<c:Button>...</c:Button>
xmlns:c="clr-namespace:System.Windows.Controls;assembly=PresentationFramework"
,这么长的一串字符看上去的确有点恐怖,但不用担心,VS是有自动提示功能的。
在VS自动提示的顶部,你会看到几个看上去像网页地址的名称空间,其中就包含例子代码中的那两行。为什么名称空间看上去像是一个主页地址呢?其实就算把它拷贝到浏览器的地址栏里尝试跳转页不会打开网页。这里只是XAML解析器的一个硬性编码(hard-coding),只要见到这些固定的字符串,就会把一系列必要的程序集(Assembly)和程序集中包含的.NET名称空间引用进来(即没有为什么,我就是预先定义了这串地址的含义,看到它就把它解析为对应的含义)。
默认引用进来的两个名称空间格外重要,它们对应的程序集和.NET名称空间如下:
http://schemas.microsoft.com/winfx/2006/xaml/presentation
- System.Windows
- System.Windows.Automation
- System.Windows.Controls
- System.Windows.Controls.Primitives
- System.Windows.Data
- System.Windows.Documents
- System.Windows.Forms.Integration
- System.Windows.Ink
- System.Windows.Input
- System.Windows.Media
- System.Windows.Media.Animation
- System.Windows.Media.Effects
- System.Windows.Media.Imaging
- System.Windows.Media.Media3D
- System.Windows.Media.TextFormatting
- System.Windows.Navigation
- System.Windows.Shapes
也就是说,你在XAML代码中可以直接使用这些CLR名称空间中的类型(因为默认XML名称空间没有前缀)。从这些名称空间的名字也可以看出,大多是显示相关的,与硬编码字串中的presentation相符。
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
则对应一些与XAML语法和编译相关的CLR名称空间。使用这些名称空间中的类型时需要加 x 前缀,因为它们被映射到了名 x 的XML名称空间中。
从这两个名称空间的名字和它们所对应的.NET程序集上,我们不难看出,第一个名称空间对应的是与绘制UI相关的程序集,是表示(Presentation)层面上的东西;第二个名称空间则对应XAML语言解析处理相关的程序集,是语言层面上的东西。
还剩下 x:Class="DManager.View.LedgerView"
这个Attribute。x:前缀说明这个Attribute来自于x映射的名称空间——前面刚分析过,这个名称空间是对应XAML解析功能的。x:Class,顾名思义它与类有些关系,是何种关系呢?我们来做个实验:
首先,把x:Class="DManager.View.LedgerView"
这个Attribue删掉,再到 LedgerView.xaml.cs 文件里把构造器中对 InitializeComponent 方法的调用也删掉。编译程序,你会发现程序仍然可以运行。为什么呢?打开App.xaml这个文件,你能发现这样一个Attribute——StartupUri=“LedgerView.xaml”,它是在告诉编译器把由LedgerView.xaml解析后生成的窗体作为程序启动时的主窗体。也就是说,只要LedgerView.xaml文件能够被正确解析成一个窗体,程序就可以正常运行。
然后只恢复x:Class这个Attribute(不恢复对InitializeComponent方法的调用),并更改它的值为为 x:Class="DManager.WindowABC"
。编译之,仍然可以正确运行。这时,使用IL Disassembler(中间语言反编译器)打开项目的编译结果,你会发现在由项目编译生成的程序集里包含一个名为WindowABC的类。
这说明,x:Class 这个Attribute的作用是当XAML解析器将包含它的标签解析成C#类后,决定该类的类名是什么。这里,已经触及到XAML的本质。前面已经看到,示例代码的结构就是使用XAML语言直观地告诉我们,当前被设计的窗体是在一个<Window>里嵌套一个<Grid>。如果是使用C#来完成同样的设计呢?显然,我们不可能去更改Window这个类,我们能做的是从Window类派生一个类(比如叫WindowABC),再为这个类添加一个Grid类型的字段,然后把这个字段在初始化时赋值给派生类的内容属性(Content)。代码看起来大概是这样:
using System.Windows;
using System.Windows.Controls;
class WindowABC : Window
{
private Grid grid;
public WindowABC()
{
grid = new Grid();
this.Content = grid;
}
}
最后,回到最初的代码。你可能会问:在XAML里有x:Class="DManager.View.LedgerView"
,在LedgerView.xaml.cs里也声明了LedgerView这个类,难道它们不会冲突吗?仔细看看LedgerView.xaml.cs中LedgerView类的声明就知道了——在声明时使用了partial这个关键字。使用partial关键字,可以把一个类分拆在多处定义,只要各部分代码不冲突即可。显然,由XAML解析器生成的LedgerView类在声明时也使用了partial关键字,这样由XAML解析成的类和C#文件里定义的部分就合二为一了。正是由于这种partial机制,我们可以把类的逻辑代码留在.cs文件里,用C#语言来实现,而把那些与声明及布局UI元素相关的代码分离出去,实现UI与逻辑分离。并且,用于绘制UI的代码(如声明控件类型的字段、设置它们的外观和布局等)也不必再使用C#语言,使用XAML和XAML编辑工具就能轻松搞定。
至此,对文章开头处的简单XAML程序应该了然于胸了。
二、 XAML中为对象属性赋值的语法
XAML是一种声明性语言,XAML编译器会为每个标签创建一个与之对应的对象。
声明性语言
声明式编程是一种编程范式,即构建计算机程序的结构和元素的一种风格,它表达了计算的逻辑而没有描述其控制流程。
对象创建出来后要对它的属性进行必要的初始化才有使用意义。因为XAML语言不能编写程序的运行逻辑,所以一份XAML文档中除了使用标签声明对象就是初始化对象的属性了。
XAML中为对象赋值共有两种方法:
- 使用字符串进行简单赋值
- 使用属性元素(Property Element)进行复杂赋值
我们以一个<Rectangle>标签的Fill为例来介绍这两种方法。
2.1. 使用标签的Attribute为对象进行赋值
前面已经知道,一个标签的Attribute里有一部分与对象的Property互相对应,<Rectangle>标签的Fill这个Attribute就是这样——它与Rectangle类对象的Fill属性对应。在MSDN文档库里可以查到,Rectangle.Fill的类型是Brush。Brush是一个抽象类,凡是以Brush为基类的类都可作为Fill属性的值。Brush的派生类有很多:
- SolidColorBrush:单色画刷
- LinearGradientBrush:线性渐变画刷
- RadialGradientBrush:径向渐变画刷
- ImageBrush:位图画刷
- DrawingBrush:矢量图画刷
- VisualBrush:可视画刷
下面例子中使用单色画刷和线性渐变画刷两种。
我们先学习使用字符串对Attribute进行简单赋值。假设我们的Rectangle只需要填充成单一的蓝色,那么只需要简单地写成:
<Window x:Class="WpfApplicationTree.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window" Height="188" Width="300">
<Grid VerticalAlignment="Center" HorizontalAlignment="Center">
<Rectangle x:Name="rectangle" Width="200" Height="120" Fill="Blue"/>
</Grid>
</Window>
运行效果如下,
可以看到,Blue这个字符串最终被翻译成了一个SolidColorBrush对象并赋值给了rectangle对象。换成C#代码是这样的:
// ...
SolidColorBrush sBrush = new SolidColorBrush();
sBrush.Color = Colors.Blue;
this.rectangle.Fill = sBrush;
// ...
需要注意的是,通过这种Attribute=Value语法赋值时,由于XAML的语法限制,Value只可能是一个字符串值。这就引出了两个问题:
- 如果一个类能使用XAML语言进行声明,并允许它的Property与XAML标签的Attribute互相映射,那就需要为这些Property准备适当的转换机制。
- 由于Value是个字符串,所以其格式复杂程度有限,尽管可以在转换机制里包含一定的按格式解析字符串的功能以便转换成较复杂的目标对象,但这会让最终的XAML使用者头疼不已。因为他们不得不在没有编码辅助的情况下写一个格式复杂的字符串以满足赋值要求。
第一个问题的解决方案是使用TypeConverter类的派生类,在派生类里重写TypeConverter的一些方法,第二个问题的解决方案是使用属性元素(Property Element)。
题外话
使用WPF时,我有时会想在Attribute中能写下简单的计算逻辑,比如:Height=“100+1”、IsEnable="!true"等
但是通过上面内容可知,由于它是字符串赋值,特征内的值就是一串字符串,所以要达到目标效果显然得解析字符串,那么各种计算逻辑也是先通过字符串解析成表达式再运算的,而XAML自带的解析器并没有提供这样直接解析的功能(虽然可以通过converter的方式)。因此有时想实现一些简单计算,还是挺麻烦的,这是XAML的一个我认为不太方便的点。
🔺2.2. 使用TypeConverter类将XAML标签的Attribute与对象的Property进行映射
首先,我们准备一个类:
public class Human
{
public string Name {get; set;}
public Human Child {get; set;}
}
这个类有两个属性:
- string类型的Name。
- Human类型的Child。
现在我的期望是,如果在XAML里这样写:
<Window.Resources>
<local:Human x:Key="human" Child="ABC"/>
</Window.Resources>
则能够为Human实例的Child属性赋一个Human类型的值,并且Child.Name就是这个字符串的值。
我们先看看直接写行不行。在UI上添加一个按钮button1,并且在它的Click事件处理器里写上:
private void button1_Click(object sender, RoutedEventArgs e)
{
Human h = (Human)this.FindResource("human");
MessageBox.Show(h.Child.Name);
}
编译没有问题,但在单击按钮之后程序抛出异常:
为什么会出现上述异常呢?
原因很简单,Human的Child属性是Human类型,而XAML代码中的ABC是个字符串,编译器不知道如何把一个字符串实例转换成一个Human实例。
那我们应该怎么做呢?
办法是使用TypeConverter和TypeConverterAttribute这两个类。
首先,我们要从TypeConverter类派生出自己的类,并重写它的一个ConvertFrom方法。这个方法有一个参数名为value,这个值就是在XAML文档里为它设置的值,我们要做的就是把这个值“翻译”成合适类型的值赋给对象的属性:
public class StringToHumanTypeConverter : TypeConverter
{
public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
{
if (value is string)
{
Human h = new Human();
h.Name = value as string;
return h;
}
return base.ConvertFrom(context, culture, value);
}
}
有了这个类还不够,还要使用TypeConverterAttribute这个特征类把StringToHumanTypeConverter这个类“粘贴”到作为目标的Human类上。
[TypeConverterAttribute(typeof(StringToHumanTypeConverter))]
public class Human
{
public string Name { get; set; }
public Human Child { get; set; }
}
因为特征类在使用的时候可以省略Attribute这个词,所以也可以写成:
[TypeConverter(typeof(StringToHumanTypeConverter))]
但这样写,需要注意写在方括号里的是TypeConverterAttribute而不是TypeConverter。
完成之后,再次单击按钮,我们想要的结果就出来了。
注意
TypeConverter类的使用远远不是只重载一个ConverterFrom方法那么简单。为了配合这个方法的运行,还需要重载其他几个方法。实际开发中,经常会用到一些封装过的Converter类,定义后在XAML的Resources中实例化,在绑定时使用。
2.3. 属性元素
在XAML中,非空标签均具有自己的内容(Content)。标签的内容指的就是夹在起始标签和结束标签之间的一些子级标签,每个子级标签都是父级标签内容的一个元素(Element),简称为父级标签的一个元素。顾名思义,属性元素指的是某个标签的一个元素对应这个标签的一个属性,即以元素的形式来表达一个实例的属性。代码描述为:
<ClassName>
<ClassName.PropertyName>
<!--以对象形式为属性赋值-->
</ClassName.PropertyName>
</ClassName>
这样,在这个标签的内部就可以使用对象(而不再局限于简单的字符串)进行赋值了。
如果把上面的例子用属性标签式语法改写一下,XAML代码将是这样:
<Grid VerticalAlignment="Center" HorizontalAlignment="Center">
<Rectangle x:Name="rectangle" Width="200" Height="120"/>
<Rectangle.Fill>
<SolidColorBrush Color="Blue"/>
</Rectangle.Fill>
</Rectangle>
</Grid>
效果和先前代码一样。所以,对于简单赋值而言属性元素语法并没有什么优势,反而让代码看起来有点冗长。但遇到属性是复杂对象时这种语法的优势就体现出来了,如使用线性渐变画刷来填充这个矩形:
<Grid>
<Rectangle x:Name="rectangle">
<Rectangle.Fill>
<LinearGradientBrush>
<LinearGradientBrush.StartPoint>
<Point X="0" Y="0"/>
</LinearGradientBrush.StartPoint>
<LinearGradientBrush.EndPoint>
<Point X="1" Y="1"/>
</LinearGradientBrush.EndPoint>
<LinearGradientBrush.GradientStops>
<GradientStopCollection>
<GradientStop Offset="0.2" Color="LightBlue"/>
<GradientStop Offset="0.7" Color="Blue"/>
<GradientStop Offset="1.0" Color="DarkBlue"/>
</GradientStopCollection>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
</Grid>
LinearGradientBrush的GradientStops属性是一个GradientStop对象的集合(GradientStopCollection),即一系列的矢量渐变填充点。在这些填充点之间,系统会自动进行插值运算、计算出过渡色彩。填充矢量的方向是StartPoint和EndPoint两个属性(类型为Point)的连线方向,矩形的左上角为(0,0)、右下角(1,1)。这段代码中,针对这三个属性都使用了属性标签式赋值方法。
上面代码为了突出属性元素语法,我将所有属性都展开成属性元素,结果是代码的可读性一落千丈。经过优化,代码变成这样:
<Rectangle x:Name="rectangle">
<Rectangle.Fill>
<LinearGradientBrush>
<LinearGradientBrush.GradientStops>
<GradientStopCollection>
<GradientStop Offset="0.2" Color="LightBlue"/>
<GradientStop Offset="0.7" Color="Blue"/>
<GradientStop Offset="1.0" Color="DarkBlue"/>
</GradientStopCollection>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
注意
其实上面所谓优化了的代码,也就是去掉StartPoint、EndPoint的元素(因为它们的默认值分别是(0,0)、(1,1))。
一般来讲,在保证了基本可读性的情况下,代码还是精简一点比较好。二话不说,上来一大段代码,谁看了都犯迷糊。
这边有几个简化XAML的技巧:
- 能使用Attribute=Value形式赋值的就不使用属性元素。
- 充分使用默认值,取出冗余:StartPoint=“0, 0” EndPoint="1,1"是默认值,可以省略。
- 充分利用XAML的简写方式:XAML的简写方式有很多,需要在实际应用中慢慢积累(其实这是熟练度问题了)。如本例,LinearGradientBrush.GradientStops的数据类型是GradientStopCollection,如果严格按照语法来写,这个属性元素的内容应该是一个<GradientStopCollection>标签,实际上,XAML允许你省略这个标签而把集合元素直接写在属性元素的内容里,控件的“内容属性”也有类似简写。
最后是一个小例子来结束这一小节:
<Grid>
<Ellipse Width="120" Height="120">
<Ellipse.Fill>
<RadialGradientBrush GradientOrigin="0.25 0.25" RadiusX="0.75" RadiusY="0.75">
<RadialGradientBrush.GradientStops>
<GradientStopCollection>
<GradientStop Offset="0" Color="White"/>
<GradientStop Offset="0.65" Color="Black"/>
<GradientStop Offset="0.8" Color="Gray"/>
</GradientStopCollection>
</RadialGradientBrush.GradientStops>
</RadialGradientBrush>
</Ellipse.Fill>
</Ellipse>
</Grid>
这是一个径向渐变画刷的例子,效果如下:
千万不要以为这是在VS里一行一行敲出来的XAML——这段代码的大部分内容是用Blend通过绘图的形式自动生成的。由Blend生成的代码里会包含一些冗余的细节。常见的细节包括:
- 值过于精确:比如0.79103108323这样的值,一般可以简化为0.8以提高可读性。
- 默认值被显式地写出:比如为StackPanel显式地写出Orientation=“Vertical”,一般删掉即可。
- 专门用于Blend绘图的标记:比如用于锁定图形的标记。根据实际需要决定保留还是删除。
一般情况下,对于复杂的绘图和动画创作,应该现在Blend里进行操作,然后回到VS里进行微调,在保证不影响效果的情况下尽可能地提高代码的可读性和可维护性。
Blend for Visual Studio工具,操作界面如下,和VS差不多,但是在编辑界面上可以图形化操作,拖拽一些图形,下方的代码框中会生成相应的代码,也算是一个生产力工具吧。
⭐2.4. 标记扩展
仔细观察XAML中为对象属性赋值的语法,你会发现大多数赋值都是为属性生成一个新对象。但有时候需要把同一个对象赋值给两个对象的属性,还有时需要给对象的属性赋一个null值,WPF甚至允许将一个对象的属性值依赖在其他对象的某个属性上。当需要为对象的属性进行这些特殊类型的赋值时就需要使用标记扩展了。
注意
所谓标记扩展,实际上是一种特殊的Attribute=Value语法,其特殊的地方在于Value字符串是由一对花括号及其括起来的内容组成,XAML编译器会对这样的内容做出解析,生成相应的对象。
因为本节内容重在讲述XAML的语法,所以不必过分追究下面代码的编程细节,只需要关注标记扩展的语法即可。在下面代码中,将使用Binding类的实例将TextBox的Text属性依赖在Slider的Value上,这样,当Slider滑块滑动时TextBox就会显示Slider当前的值。
<StackPanel>
<TextBox Text="{Binding ElementName=slider1,Path=Value,Mode=OneWay}" Margin="5"/>
<Slider x:Name="slider1" Margin="5"/>
</StackPanel>
其中,Text="{Binding ElementName=slider1, Path=Value, Mode=OneWay}"
这句话就是标记扩展了。我们分析一下这句话代码:
- 当编译器看到这句代码时就会把花括号里的内容解析成相应的对象。
- 对象的数据类型名是紧邻左花括号的字符串。
- 对象的属性由一串以逗号连接的子字符串负责初始化(注意,属性值不再加引号)。
初学者常常会认为这个语法比较难记,其实这个语法与C# 3.0中的对象初始化语法非常接近。如果使用C# 3.0的语法来创建一个Binding类的实例,最佳的语法应该是:
Binding binding = new Binding() { Source = slider1, Mode = BindingMode.OneWay };
C# 3.0中对象初始化器也是这样,使用一对花括号包围一组由逗号分隔的子字符串,这些子字符串用来初始化对象的属性。只是XAML的标签扩展把对象的数据类型也搬到括号里面来了。
标记扩展也会是对属性的赋值,所以完全可以使用属性标签的形式来替换标记扩展,只是简洁性使然没人这么做罢了。下面是使用属性标签替换标记扩展后的代码:
<StackPanel>
<TextBox Margin="5">
<TextBox.Text>
<Binding ElementName="slider1" Path="Value" Mode="OneWay"/>
</TextBox.Text>
</TextBox>
<Slider x:Name="slider1" Margin="5"/>
</StackPanel>
这样写的弊端是使代码量增加、阅读不便,但也有一个好处:老版本的VS(2008)没有对标记扩展提供智能语法提示,而使用属性标签是支持智能提示的。
尽管标记扩展的语法简洁方便,但并不是所有对象都能用标记扩展的语法来写,只有MarkupExtension类的派生类(直接或间接)才能使用标记扩展语法来创建对象。MarkupExtension的直接派生类并不多,它们是:
- System.Windows.ColorConvertedBitmapExtension
- System.Windows.Data.BindingBase
- System.Windows.Data.RelativeSource
- System.Windows.DynamicResourceExtension
- System.Windows.Markup.ArrayExtension
- System.Windows.Markup.NullExtension
- System.Windows.Markup.StaticExtension
- System.Windows.Markup.TypeExtension
- System.Windows.ResourceKey
- System.Windows.StaticResourceExtension
- System.Windows.TemplateBindingExtension
- System.Windows.ThemeDictionaryExtension
注意
最后,使用标记扩展时还需要注意以下几点:
- 标记扩展是可以嵌套的,例如
Text="{Binding Source={StaticResource myDataSource}, Path=PersonName}"
是正确的语法。- 标记扩展具有一些简写语法,例如
"{Binding Value}"
与"{Binding Path=Value}"
是等价的、"{StaticResource myString}"
与"{StaticResource ResourceKey=myString}"
是等价的。两种写法中,前者称为固定位置参数(Positional Parameter),后者称为具名参数(Named Parameters)。固定位置参数实际上就是标记扩展类的构造器的参数,其位置由构造器的参数列表决定。- 标记扩展类的类名均以单词Extension为后缀,在XAML使用它们时Extension后缀可以省略不写,比如写
Text="{x:Static ...}"
与写Text="{x:StaticExtension}"
是等价的。
三、 事件处理器与代码后置
前面说过,当一个XAML标签对应着一个对象时,这个标签的一部分Attribute会对应这个对象的Property。除了这部分对应着对象Property的Attribute外,还有一部分Attribute对应着对象的事件(Event)。<Button>标签有一个名为Click的Attribute,它对应的就是Button类的Click事件。
在.NET事件处理机制中,可以为对象的某个事件指定一个能与该事件匹配的成员函数,当这个事件发生时,.NET运行时会去调用这个函数,即表示对这个事件的响应和处理。 因此,我们把这个函数称为“事件处理器”(Event Handler)。WPF支持在XAML里为对象的事件指定事件处理器,方法是使用事件处理器函数名为对应事件的Attribute进行赋值:
<ClassName EventName="EventHandlerName"/>
当我们为一个XAML标签的事件性Attribute进行赋值时,XAML编辑器会自动为我们生成相应的事件处理器。事件处理器是使用C#语言编写的函数。以<button>为例,当为Click赋值时,会看到提示。
若此时按下Enter键,VS会自动为我们生成一个事件处理器,并把它的名字(函数名)赋值给Click。此时的XAML代码是:
<Button x:Name="button1" Click="button1_Click"/>
在button1_Click上右击,在弹出菜单中选择 转到定义 ,就可跳转到由VS自动生成的事件处理器中。
事件处理器的函数声明与用于声明Button.Click事件的委托保持类型和参数上的一致,它的名字已经被拷贝到XAML代码中。
private void Button_Click(object sender, RoutedEventArgs e)
{
}
如果把<Button x:Name="button1" Click="button1_Click"/>
翻译成C#代码,基本上是这样:
Button button1 = new Button();
button1.Click += new RoutedEventHandler(button1_Click);
我们知道,C#语言编写的代码应该用于处理程序的逻辑,需要让它与表示UI的XAML代码分开。这些C#函数会放在哪里呢?由于C#支持partial类,XAML标签又可以使用x:Class特征指定将由XAML代码解析生成的类与哪个类合并,因此,我们完全可以把用于实现程序逻辑的C#代码放在一个文件里,把用于描述程序UI的XAML代码放在另一个文件里,并且让事件性Attribute充当 XAML与C#之间沟通的纽带——设计师用XAML为程序创建漂亮的UI并展现给客户;程序员用C#编写程序的逻辑、从后台支持前面的UI——这种将逻辑代码与UI代码分离、隐藏在UI代码后面的形式叫做“代码后置”(Code-Behind)。(虽然在国内,很多时候C#的前后端代码都是一个人做的;这边说的在分工层面没有太大意义)
注意
之所以能实现代码后置功能,是因为.NET支持partial类并能将解析XAML所生成的代码与x:Class所指定的类进行合并,有两点需要注意的是:
- 不只是事件处理器,一切用于实现程序逻辑的代码都要放在后置的C#文件中。
- 默认情况下,VS为每个XAML文件生成的后置代码文件名为“XAML文件全名.cs”,比如XAML文件名为MyWindow.xaml,那么它的后置代码文件名为MyWindow.xaml.cs。这样做是为了方便管理文件,但并不是必须的,只要XAML解析器能找到x:Class所指定的类,无论你的文件叫什么名字都可以。
⭐四、导入程序集和引用其中的名称空间
大多数情况下,根据架构设计一个程序会被分成若干个相对独立的模块来编写,每个模块可以独立编译、进行版本升级。模块与模块之间有时会存在一些依赖关系,即有些模块需要“借用”其他模块中的功能。.NET的模块称为程序集(Assembly)。一般情况下,使用VS创建的是解决方案(Solution),一个解决方案就是一个完整的程序。解决方案中会包含若干个项目(Project),每个项目是可以独立编译的,它的编译结果就是一个程序集。常见的程序集是以.exe为扩展名的可执行程序或者是以.dll为扩展名的动态链接库,大多数情况下,我们说“引用其他程序集”时,说的就是动态链接库。因为.NET编程接口以类和类级别的单元为主(Win32 API以函数为主),所以我们又常把引用程序集说成是引用类库。
类库中的类一般都会安置在合适的名称空间中,名称空间的作用是避免同名类的冲突。比如一个程序中引用了LibA.dll和LibB.dll两个类库,这两个类库中都有一个叫Converter的类,如果没有名称空间来限定的话,编译器将分不清程序员打算使用哪个类。如果LibA.dll中的Converter放在一个名为Microsoft的名称空间里,LibB.dll中的Converter放在名为Google名称空间里,程序员就可以通过Microsoft.Converter和Google.Converter来区分这两个类了。
注意
想在自己的程序里引用类库,需要分三步来做:
- 编写类库项目并编译得到.dll文件或者获得别人编译的.dll文件。
- 将类库项目或者.dll文件引用进自己的项目。
- 在C#和XAML中引用类库中的名称空间。
作为常识,编写和引用类库项目在此不再赘述。我们只看如何在XAML里引用类库中的名称空间和类。需要记住一点:把类库引用到项目中是引用其中名称空间的物理基础,无论是C#还是XAML都是这样。一旦将一个类库引用进程序,就可以引用其中的名称空间。
假设我的类库程序集名为MyLibrary.dll,其中包含Common和Controls两个名称空间,而且已经把这个程序集引用进WPF项目,那么在XAML中引用这两个名称空间的语法是:
xmlns:映射名="clr-namespace:类库中名称空间的名字;assembly=类库文件名"
对于MyLibrary.dll里的两个名称空间,XAML中的引用会是:
xmlns:common="clr-namespace:Common;assembly=MyLibrary"
xmlns:controls="clr-namespace:Controls;assembly=MyLibrary"
让我们分析一下XAML引用名称空间的语法。
- xmlns是用于在XAML中声明名称空间的Attribute,它从XML语言继承而来,是XML Namespace的缩写。
- 冒号后的映射名是可选的,但由于可以不加映射名的默认名称空间已经被WPF的主要名称空间占用,所以所引用的名称空间都需要加上这个映射名。映射名可以根据喜好自由选择,但团队内部最好使用一致的命名。一个建议就是使用类库中名称空间的原名或者缩写。
- 引号中的字符串值确定了你要引用的是哪个类库以及类库中的哪个名称空间。这个字符串看起来挺麻烦,但幸好XAML编辑器会帮我们自动填充它。
一旦我们将类库中的名称空间引用进XAML文档,我们就可以使用这些名称空间里的类。语法格式是:
<映射名:类名>...</映射名:类名>
例如使用Common和Controls中的类,代码是这样:
<common:MessagePanel x:Name="window1"/>
<controls:LedButton x:Name="button1"/>
附加一点额外的小知识。我们发现,XAML中引用名称科技hi安的语法与C#不太一样。最大的差别是XAML需要为被引用的名称空间添加一个映射名,用这个映射名来代表被引用的名称空间。其实,C#也可以这样使用名称空间,只是不常用罢了。比如,在C#中引用Common和Controls名称空间时可以这样写:
using Cmn = Common;
using Ctl = Controls;
这种写法在名称较长的名称空间中有同名类时比较有用。
五、XAML的注释
XAML的注释语法继承自XML。语法使:
<!--需要被注释掉的内容-->
注意
有几点要注意:
- XAML注释只能出现在标签的内容区域,即只能出现在开始标签和结束标签之间。
- XAML注释不能用于注释标签的Attribute。
- XAML注释不能嵌套。
六、小结
至此,我们已经大致了解了XAML的基本语法。要注意的是,XAML是一种很灵活的语言,特别是一些用于简化代码的缩写写法。这些看上去奇怪的写法基本上无法系统地用章节来描述,只能靠实际使用中慢慢积累。不过,不用担心,一般情况下比较复杂的代码都能通过前面学过的语法解释清楚。