在WPF中,由于引入了XAML语言。因此在界面设计方面,一般使用XAML语言,而在业务逻辑上用C#这样的后台代码,XAML和后台代码既可以配合得丝丝入扣,又可以将界面设计和业务逻辑分离。这就好比两仪剑法和两仪刀法,一正一反。但均是从八卦中化出,再回归八卦,可以说是殊途同归。
4.1 从C#到XAML
XAML与C#不同,但是与HTML和XML一样是一种声明式语言。下面是一段完整的XAML代码示例:
<!--开始标签(Start Tag)-->
<Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" Width="300" Height="200"><!--属性-->
<!--两者中间的内容-->
<Button.Content>
Hello XAML
</Button.Content>
</Button>
<!--结束标签(End Tag)-->
XAML由开始标签、结束标签及二者中间的内容组成,开始标签中包括一个命名空间(xmlns)和两个属性说明(Width与Height)。
下面是一个更为复杂的XAML文件的例子:
首先看显示效果:
<Window Title="XAML窗口" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" Width="300" Height="200">
<DockPanel>
<Button DockPanel.Dock="Left" Background="AliceBlue" Margin="0 5 0 10" Content="Hello XAML"/>
<Button DockPanel.Dock="Right">
<Button.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
<GradientStop Color="Yellow" Offset="0.0"/>
<GradientStop Color="Red" Offset="0.25"/>
<GradientStop Color="Blue" Offset="0.75"/>
<GradientStop Color="LimeGreen" Offset="1.0"/>
</LinearGradientBrush>
</Button.Background>
Hello XAML
</Button>
</DockPanel>
</Window>
其中描述包含一个面板(DockPanel)的窗口(Window),在面板的左右侧各有一个按钮。需要注意的是,**窗口**不会像前面的按钮一样直接显示出来,**需要运行程序后才会弹出窗口**。
上面这段XAML代码相当于以下的C#代码:
public class MyWindow : Window
{
[STAThread]
public static void Main()
{
MyWindow win = new MyWindow();
win.Show();
Application app = new Application();
app.Run();
}
public MyWindow()
{
InitializeComponent();
}
private void InitializeComponent()
{
this.Title = "XAML窗口";
this.Width = 300;
this.Height = 200;
DockPanel panel = new DockPanel();
Button btn = new Button();
btn.Content = "Hello XAML";
btn.Background = new SolidColorBrush(Color.AliceBlue);
btn.Margin = new Thickness(0, 5, 0, 10);
DockPanel.SetDock(btn, Dock.Left);
panel.Children.Add(btn);
Button btn2 = new Button();
btn2.Content = "Hello XAML";
LinearGradientBrush brush = new LinearGradientBrush();
brush.StartPoint = new Point(0, 0);
brush.EndPoint = new Point(1, 1);
brush.GradientStops.Add(new GradientStop(Colors.Yellow, 0));
brush.GradientStops.Add(new GradientStop(Colors.Red, 0.25));
brush.GradientStops.Add(new GradientStop(Colors.Blue, 0.75));
brush.GradientStops.Add(new GradientStop(Colors.LimeGreen, 1));
btn2.Background = brush;
DockPanel.SetDock(btn2, Dock.Right);
panel.Children.Add(btn2);
this.Content = panel;
}
}
不难发现,XAML代码在描述界面上比C#简洁得多。
XAML文件有两个重要组成部分:一是有完整开始和结束标签的要素,如 Window、DockPanel 和 Button等,称为“元素”(Element);二是依附于元素的要素,如 Width、Height 和 Background,称为“属性”(Attribute)。
在C#代码中创建一个对象并为其赋值等都会使用 using关键字声明其命名空间,在XAML中也是如此。在Window元素中的 xmlns=http://schemas.microsoft.com/winfx/2006/xaml/presentation 就是声明的命名空间,该命名空间貌似一个网站。但是它不是,它仅仅是一个命名空间的标示而已。
在C#中通过 new 关键字创建一个 Button 对象,而在XAML中是通过一个开始标签和结束标签来实现的。还有种更简洁的写法是用一个反斜线“/”取代结束标签,称为“empty-element语法”,如下所示:
<Button DockPanel.Dock="Left"
Background="AliceBlue" Margin="0 5 0 10" Content="Hello XAML"/>
新建完对象后,接下来的事情就是要为该对象设置属性。在C#里设置属性是一件非常简单的事情,double 类型的属性就给它赋予 double 类型的值,string类型的属性就给它赋予 string 类型的值等。但是**在XAML里**,无论你的属性是 double 还是 string,你**能给属性赋的值统统是字符串**。下表中列出了一些典型的属性赋值的例子。
类 | 属性 | C# | XAML |
---|---|---|---|
Window | Title | this.Title = “XAML窗口”; | Title=”XAML窗口” |
Window | Width | this.Width = 300; | Width=”300” |
Button | DockPanel.Dock | DockPanel.SetDock(btn, Dock.Left); | DockPanel.Dock=”Left” |
Button | Background | btn.Background = new SolidColorBrush(Colors.AliceBlue); | Background=”AliceBlue” |
LinearGradientBrush brush = new LinearGradientBrush(); brush.StartPoint = new Point(0, 0); brush.EndPoint = new Point(1, 1); brush.GradientStops.Add(new GradientStop(Colors.Yellow, 0)); …… btn2.Background = brush; |
…… |
||
Button | Content | btn.Content = “Hello XAML”; | Content=”Hello XAML” |
其中仅有 Window 的 Title 属性是字符串,在 C# 和在 XAML 中均直接将值设置成字符串。此外 Window 的 Width 类型也是一种基本型 double,在XAML中给该类型的属性赋值也非常简单,它们均属于“简单属性”。DockPanel.Dock 不是 Button 的属性,在XAML中将其值设置为 Left,表示该按钮在 DockPanel 的左侧。在C#中将其转换为一种方法,这种属性称为“附加属性”。
Button 的 Background 属性是一个画刷类型,第1个按钮的该属性在C#中创建了一个画刷对象,然后将该对象赋给属性。在XAML中直接用一个“AliceBlue”字符串为其赋值,使得更为简洁。这正是类型转换器起到的作用,本章的第4,5节类型转换器里一探究竟;第2个按钮的背景色是一个渐变画刷。Background 属性没有写在元素的标签中,而是如同一个子元素写在 Button 的开始和结束标签之间。前面设置 Background 属性的方法称为“Attribute语法”,后一种称为“Property-Element语法”。“Property-Element语法”得名是因为这个时候的 Background 属性设定更像是一个元素设定的方法,但是由于本质上还是一个属性,因此是Property-Element。
Button 的 Content 属性是一个特殊的属性,其类型为 Object。它不仅仅是一个属性,更确切的说是一个嵌套的元素。从理论上来说它可以设置为任何类型的对象。
此外在C#中还有很多情况是一个属性会去引用一个对象,或者将一个属性赋值成空。在XAML里该如何实现呢?(4.6节)事件的处理函数如何写,如何使用XAML构建一个应用程序,或者混合使用XAML和C#如何构建应用程序?(在4.7节分别使用XAML和C#构建应用程序——刀还是刀,剑依旧是剑和第4.8节使用XAML和C#构建应用程序——刀剑合璧里寻求答案)。
4.2 命名空间及其映射
4.2.1 WPF的命名空间
前面我们已经看到在 XAML 中声明一个命名空间是一个URL,即 http://schemas.microsoft.com/winfx/2006/xaml/presentation。
这是WPF命名空间的标识。XAML文件规定必须至少指定一个XML的命名空间。为了使该命名空间作用于整个文件,通常会将其放置在根元素中,如下所示:
<Page xmlns="http://schema.microsoft.com/winfx/2006/xaml/presentation">
<Button Width="300" Height="200">
<Button.Content>
Hello XAML
</Button.Content>
</Button>
</Page>
在 WPF 中只有以下4种元素可以作为根元素。
(1)Window:代表一个窗口。
(2)Page:类似一个网站的页面。
(3)Application:代表一个应用程序(参见第8章)。
(4)ResourceDictionary:代表一个逻辑资源的集合(参见第12章)。
上面XAML代码中的根元素不是Button吗?该文件在 IE 浏览器和 XamlPad 中都可以正常显示,这是为什么?
实际上这种松散的 XAML 文件会和一个 PresentationHost.exe 程序关联,只要双击该文件,就会执行 PresentationHost。该程序发现没有根元素,则建立一个 Page 类型的对象,然后将其设置为 Page 的 Content 属性。看上去根元素是 Button,但实际上还是 Page。任何继承自 FrameworkElement 的元素都可以看上去作为根元素,但是只有 Window 对象不可以,因为它不能作为某个元素的子元素。
我们知道WPF中不同的控件分属于不同的命名空间,如 Window 类型属于 System.Windows 命名空间,而 Button 属于 System.Windows.Controls 命名空间。如果在C#代码中用到这些控件,则必须连续使用 using 语句,直到包括所有用到的命名空间,如下所示:
using System.Windows;
using System.Windows.Controls;
using ......
而在WPF中一个URL标识就可以全部涵盖所有用到的控件的命名空间,原因是其在 PresentationFramework 和 PresentationCore 这样的程序集中使用 XmlnsDefinition 将 Xml 和 CLR 命名空间关联起来。下面是通过 Reflector 与和 System.Windows 及 System.Windows.Controls 相关联。XAML解析器会检查应用程序所加载的所有组件的 XmlnsDefinition 属性,以此确定对应的 CLR 命名空间。
4.2.2 XAML的命名空间
第二个常用的命名空间是 XAML 的命名空间,如果需要使用 XAML 专用的元素和属性(事实上一定会使用),那么必须声明该声明空间。习惯上 XAML 的声明空间被声明成 x 前缀——xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml",一般使用 VS生成的默认的XAML文件都会包含 WPF 和 XAML 命名空间的声明,如下所示:
其中 Window、Grid 和 Button 属于 WPF 命名空间中的元素,x:Class 属于 XAML 命名空间中的属性。由于 WPF 命名空间是默认空间,因此 Window、Grid 和 Button 没有声明前缀。
4.2.3 其他命名空间
1、使用系统类
除去前面两个命名空间,如果XAML文件要使用其他 .NET 对象或者应用程序或者其他程序集中的自定义对象,也要声明命名空间。如下所示:
如果希望明确地将 Button 的 Content 属性设置为 String 类型(尽管这样做是画蛇添足),由于 String类型属于程序集 mscorlib.dll 且其命名空间为 System,所以在声明后才能使用,如下所示:
注意通过声明xmlns : s="clr-namespace : System; assembly=mscorlib"关联 System 命名空间和前缀 s,引号内的字符串开始是 clr-namespace,用来指定命名空间。assembly 用来指定程序集名称。
2、使用自定义类
有时需要自定义类,然后在 XAML 文件中使用,其中有如下两种情况:
(1)使用本地程序的自定义类
例如,在本地程序中定义一个 Book 类,如下所示:
namespace mumu_customnamespace
{
public class Book
{
public Book() { }
public string Name { get; set; }
public double Price { get; set; }
}
}
如果需要在XAML文件中使用该类,必须做如下声明:
xmlns : local=”clr-namespace : mumu_customnamespace”
由于Book类属于本地应用程序,因此声明中忽略了设置 assembly,如下所示:
xmlns:local=”clr-namespace:mumu_customnamespace” Title=”Window1” Height=”300” Width=”300”>
但是运行程序,按钮上面只会显示该类的类型。显然这不是我们想要的效果,解决办法就是重载 ToString 函数,如下所示:
public override string ToString()
{
string str = Name + "售价为:" + Price + "元";
return str;
}
再次运行,结果如下:
自定义类必须有一个默认的无参数的构造函数,比如下面的 Book 类,程序可能运行时会因无法实例化该类而报异常:
public class Book
{
**public Book(string name, double price){ }**
……
}
但是编译器有时也可能会自动地添加一个无参数的构造函数,因此这样的问题算是一类不容易发现的 Bug。我们**<font style="color:#F5222D;">在自定义类时最好显式地添加无参数的构造函数</font>**以避免此类 Bug。
(2)使用外部程序的自定义类
使用外部程序的自定义类需要设置 assembly,如新建一个类型为 Class Library,名为“mumu_customlib”的项目。然后将刚才的 Book 类添加到其中。
现在一个解决方案中有两个项目,其中 mumu_customnamespace 是一个本地应用程序;mumu_customlib 是一个外部程序集。首先在本地应用程序中添加对 mumu_customlib 的引用,然后在XAML文件中声明并使用 Book 类,如下代码所示:
xmlns:customlib=”clr-namespace:mumu_customlib; assembly=mumu_customlib”** Title=”Window1” Height=”300” Width=”300”>
4.3 简单属性和附加属性
4.3.1 简单属性
前面讨论的 Title 和 Width 属性均为简单属性,为其赋值时一定要将值放在双引号之内。XAML解析器会根据属性的类型执行隐式转换,将字符串转换为相应的 CLR 对象。
其中枚举类型的属性设置会略有不同,在C#中设置为一个枚举类型的值通常是“类型 .值”。而在XAML中则省略前面的“类型”。在如下代码中为 SolidColorBrush 的 Color 属性直接赋值,没有前面的类型 Colors。
C#
SolidColorBrush solidbrush = new SolidColorBrush( );
solidbrush.Color = Colors.AliceBlue;
XAML
在C#中还有一种枚举类型的值可以通过“或”运算符(|)组合而成,如下所示:
public enum CarOptions
{
SunRoof = 0x01;
Spoiler = 0x02;
FogLights = 0x04;
TintedWindows = 0x08;
}
class FlagTest
{
static void Main()
{
CarOptions options = CarOptions.SunRoof | CarOptions.FogLights;
}
}
此类枚举值称为“flagwise枚举值”,不能组合的枚举值称为“nonflag枚举值”。在WPF中前者非常少见,下面代码中可能会用到该枚举值。Glyphs 对象可以用来描述文字,其 StyleSimulations 属性是一个 flagwise 枚举类型。在这里我们将字体设置为加粗和斜体,值中间用“,”取代了C#中的“|”。
<Glyphs
FontUri=”C:\WINDOWS\Fonts\TIMES.TTF”
FontRenderingEmSize=”100”
StyleSimulations=”BoldSimulation,ItalicSimulation”
UnicodeString=”Hello XAML!”
Fill=”Black”
OriginX=”100”
OriginY=”200”
/>
即使是该属性,WPF中也提供了一个 BoldItalicSimulation 枚举值,从而省略了枚举值之间的组合。
4.3.2 附加属性
附加属性是可以用于多个控件,但是在另外一个类中定义的属性。在WPF中该类属性常用在布局中,如前面例子中的 DockPanel.Dock=”Left”。附加属性的命名方式是“定义类型 .属性”,这样可以让XAML解析器将其与普通属性区分开。附加属性的设置可以使用 Attribute 和 Property-Element 语法,使用后者时类型必须是包含该属性的类型,如下代码所示:
<Button>
<!--这里是DockPanel,而不是Button-->
<DockPanel.Dock>
Right
</DockPanel.Dock>
Hello XAML
</Button>
4.4 Content属性
在前面的例子中可以看到 XAML 文件好像是一棵大的嵌套树,Window包含 StackPanel、DockPanel 和 Button,其中 Content属性起到了重要的作用。很多 WPF 类中都有这样一个特殊的属性,即 Content(内容)属性,我们在前面已经看到 Button 和 Content 属性放置的是一个字符串。
<Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
Width="300" Height="200">
<Button.Content>
Hello XAML
</Button.Content>
</Button>
<Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
Width="300" Height="200" Content="Hello XAML">
</Button>
<Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
Width="300" Height="200">
Hello XAML
</Button>
Content属性值可以放在 Button 元素中间的任何位置,如下面代码中的“Hello XAML”放置在 Button 元素的末尾处,同样也可以放置在标记1和2处。
<Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<!--1 Hello XAML-->
<Button.Width>
300
</Button.Width>
<!--2 Hello XAML-->
<Button.Height>
200
</Button.Height>
Hello XAML
</Button>
但是 Content 不能分开放置,不允许在 Content 的值中插入其他标签。以下的写法是错误的:
例外情况是 TextBlock 的 Content 属性中可以放置一些加粗或者斜体标签,如下所示:
<TextBlock xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<Italic>Hi everyOne,</Italic>This is <Bold>XAML</Bold>
</TextBlock>
Content属性不需要使用 Property-Element 语法即可直接成为 Button 元素的子类,**这是因为当用户定义一个类时使用 ContentProperty 来说明某个属性是 Content 属性**,这样WPF就知道该属性可以不需要使用 Property-Element 语法。如 Button 类,通过 Reflector 可以查看到其基类 ContentControl 的标识,如下所示:
同样可以为前面的 Book 类添加一个Text属性,并且将其指定为 Content 属性。然后在 ToString 函数中添加 Text 的内容,如下所示:
[ContentProperty("Text")]
public class Book
{
public string Text{ get; set; }
public override string ToString()
{
string str = Name + "售价为:" + Price + "元" + "\n" + Text;
return str;
}
...
}
在 XAML 文件中也可以按照内容属性语法来设置 Book 的 Text 属性值。如:
<Button FontSize="14">
<customlib:Book Name="葵花宝典" Price="0.1">
欲练此功 必先自宫
</customlib:Book>
</Button>
WPF中到底有哪些类有 Content 属性,它们的 Content 属性的名字又是什么?
从上图中可以看出不仅仅是名称为 Content 属性才是真正的 Content属性,而 Panel 的 Children 及 MenuBase 的 Items 属性也是 Content 属性。这两种属性和 Content属性不同的是它们是一个集合属性,可以放置一个或多个对象。
4.5 类型转换器
4.5.1 功能
XAML解析器通过类型转换器跨越字符串值和非字符串值的鸿沟,在XAML中输入的字符串通过类型转换器将这些字符串转换为相应的 CLR 对象。这就是类型转化器所起到的作用,如下图所示:
所有的类型转换器都派生自 TypeConverter。TypeConverter 提供的4个重要的方法是 CanConvertTo、CanConvertFrom、ConvertTo 和 ConvertFrom。
ConvertFrom 方法将 XAML 中的**字符串转换为相应的 **CLR 对象;ConvertTo 方法将 **CLR 对象转换为相应的字符串**;CanConvertFrom 用来检查能否从字符串转换为相应的 CLR 对象;CanConvertTo 检查 CLR 对象能否转换为相应的字符串,如果可以,则返回 true,否则返回 false。
从 TypeConverter 派生的转换器类有 100 多个,这里不一一列举。比较常用的比如 BrushConverter 就可以实现从字符串转换成相应的画刷类型。但是 XAML 是如何知道何种属性使用什么转换器呢?XAML 解析器实际上通过两个步骤来查找类型转换器:
(1)检查属性声明查找 TypeConverter 特性,如窗口的 Width 和 Height 属性(派生自 FrameworkElement)前面都会声明一个类型转换器。下图是通过 Reflector 查看到的 FrameworkElement 源码片段:
(2)如果在属性声明中没有 TypeConverter 特性,XAML解析器会检查对应数据类型的类的声明。如按钮的 Background 属性类型是 Brush,并在 Brush类头部就声明了一个 BrushConverter 转换器,这样在设置 Background 属性时 XAML 解析器会自动应用该转换器。下图是通过 Reflector 查看到的 Brush源码片段:
4.5.2 自定义类型转换器
我们还是以自定义的类 Book,说明如何使 Book 的 Price 属性稍作改进能够支持人民币,还能支持美元。如:
<local:Book Name="葵花宝典" Price="0.1"/>
我们认为葵花宝典的价格还是 0.1 元,如果写成:
<local:Book Name="葵花宝典" Price="$0.1"/>
则认为葵花宝典的价格是 0.8 元(假定 1 美元 = 8 元)。
为 Price 定义一个新的类型为 MoneyType,它只是简单的封装了一个 double 类型的变量。同时提供了一个静态的 Parse 方法将一个字符串正确转换为 MoneyType 类型的对象,如下所示:
[TypeConverter(typeof(MoneyConverter))]
public class MoneyType
{
private double _value;
public MoneyType()
{
_value = 0;
}
public MoneyType(double value)
{
_value = value;
}
public override string ToString()
{
return _value.ToString();
}
public static MoneyType Parse(string value)
{
string str = (value as string).Trim();
if(str[0] == '$')
{
string newprice = str.Remove(0, 1);
double price = double.Parse(newprice);
return new MoneyType(price * 8);
}
else
{
double price = double.Parse(str);
return new MoneyType(price);
}
}
}
在第一行中为这个类提供了一个 MoneyConverter 类型的转换器。下面是其实现。为了使这个类看起来比较完整,将实现其 4 个方法。事实上关键的是 ConvertFrom 方法,通过 MoneyType 的静态方法 Parse 将字符串转换为正确的 MoneyType 对象,如下代码所示:
public class MoneyConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
if(sourceType == typeof(string))
return true;
return base.CanConvertFrom(context, sourceType);
}
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
if(destinationType == typeof(string))
return true;
return base.CanConvertTo(context, destinationType);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
if(value.GetType() != typeof(string))
return base.ConvertFrom(context, culture, value);
return MoneyType.Parse((string)value);
}
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
if(destinationType == typeof(string))
return base.ConvertTo(context, culture, value, destinationType);
return value.ToString();
}
}
在原来的XAML文件中 Price 的值前面加上一个“$”符号,**<font style="color:#E8323C;">同时,将Price的类型从 double改成 MoneyType</font>**,再次运行程序,结果如下:
4.6 标记扩展
现在绝大多数的属性 XAML 都可以工作得很好了,但是依旧会有几种情况 XAML 难以胜任。
(1)将一个属性赋值为 null。
(2)将一个属性赋值给一个静态变量,如将按钮的 Background 赋值为一个预定义画刷,参看如下代码(在C#中将Background属性设置为一个静态变量):
Button btn = new Burron();
btn.Content = “Hello XAML”;
btn.Background = SystemColors.ActiveCaptionBrush;
……
以上这几种情况,我们就需要使用标记扩展了。类型转换器悄无声息的实现了类型转换,而**标记扩展则是通过 XAML 的显式的、一致的语法调用实现**。标记扩展比类型转化器更为强大,和类型转换器一样,标记扩展也可以通过自定义来实现 XAML 的语义扩展。
在 XAML 中只要属性值被一对花括号{}括起,XAML 解析器就会认为这是一个标记扩展,而不是一个普通的字符串。前面的两种情况都可以用标记扩展的方法来解决,将一个按钮的 Background 属性赋值为空,表示这个按钮的背景颜色为透明色。{x:Null}是 XAML 中提供的一种标记扩展,表示一个空值,如下代码所示:
<Button Name="btn" Content="MyButton" Click="btn_Click" **Background="{x:Null}"**>
XAML 中提供了一个{x:Static}的标记扩展,可以引用一个类的静态变量,如下所示:
<Button Name="btn" Content="MyButton" Click="btn_Click">
<Button.Background>
<x:Static Member="SystemColors.ActiveCaptionBrush"/>
</Button.Background>
</Button>
有时候,我们只想显示一个字符串,但是不巧字符串里就有一对花括号({}),如下代码所示,这个时候XAML解析器会固执地认为这是一个扩展标记。由于它又无法找到 HelloXAML 这样的标记扩展,所以无法编译通过。
<TextBlock Text="{HelloText}"/>
解决这个问题地方法是在该字符串前面添加一个空的花括号,以指示XAML 它只是一个普通的字符串,而不是一个扩展标记,如下代码所示:
<TextBlock Text="{}{HelloText}"/>
4.7 分别使用XAML和C#构建应用程序——刀还是刀,剑还是剑
4.7.1 XAML——反两仪刀法
XAML语言,我们称之为标记式(Markup)语言。它的这种层次结构天生地适合描述WPF界面。XAML既适合机器阅读,又适合人类阅读。
XAML还有一种其他代码语言(C#或者VB)难以比拟的优点,即甚至不需要编译,可以在 IE 或者在 XamlPad 中直接浏览。这种文件称为“松散XAML文件”。可以容易地将其从桌面应用移植到Web应用。
尽管我们一直在WPF范畴内讨论XAML,但是XAML与WPF不相互依赖,WPF完全可以只用代码语言来实现应用程序。XAML也可以用在以下方面:
(1)WPF XAML:本书所讨论的XAML
(2)XPS XAML:WPF XAML 的一部分,它定义了一个表示格式化的电子文档的XML。
(3)Silverlight XAML:Silverlight 是一个跨平台的浏览器插件,它是WPF的一个子集。可以使用XAML来描述Silverlight 的二维图形、动画,以及音频和视频以创建富互联网应用。
(4)WF XAML:描述 Window工作流基础内容的元素,可以参阅相关书籍。
XAML 毕竟是一种相对简单的声明式语言,不适用于应用程序的业务逻辑实现。下面代码为一个仅仅依靠 XAML 实现的应用程序,该项目由 App.xaml 和 Window1.xaml文件组成。
<Application
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
StartupUri="Window1.xaml">
</Application>
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
Title="AppByOnlyXAML" Height="200" Width="600">
<StackPanel Width="500">
<Button Content="MyButton"/>
<Ellipse Stroke="Red" Height="60" StrokeThickness="3"/>
</StackPanel>
</Window>
应用程序的入口是 App.xaml文件,XAML 文件的根元素之一,即 Application。该文件的 BuildAction 必须设置为 ApplicationDefinition(右击App.xaml文件,选择快捷菜单中的Properties选项,然后在打开的 Properties 对话框中设置)。Application 元素的 StartupUri 属性设置需要启动的窗口文件,在这里是 Window1.xaml文件,如下图所示:
Window1.xaml 文件的 Build Action 选项设置为 Page,该窗口中包含一个面板StackPanel。其中放置一个按钮和一个椭圆形,运行结果如下图所示:
如果单击按钮令椭圆的边界色由红色变成蓝色,则难以处理。XAML 毕竟是一种简单的声明式的语言,对于这样的应用程序业务逻辑显得力不从心,因此需要更为强大的C#语言——正两仪剑法。
4.7.2 C#——正两仪剑法
C#强大到任何XAML写的应用程序都可以轻易地通过C#实现,我们通过代码方式将这个 StackPanel 作为一个窗口的 Content 属性显示。如下代码所示,函数 InitializeComponent主要用来描述界面设置。虽然有的时候我们会抱怨 XAML 过于冗长,但是在描述界面方面,它确实比用代码的方式要简洁和清晰很多。
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Shapes;
using System.Windows.Media;
namespace mumu_appbyonlycode
{
public class WindowByOnlyCode : Window
{
public WindowByOnlyCode()
{
InitializeComponent();
}
private Ellipse elip;
public void InitializeCompoment()
{
this.Width = 600;
this.Height = 200;
this.Title = "AppBuOnlyCode";
StackPanel panel = new StackPanel();
panel.Width = 500;
Button btn = new Button();
btn.Content = "MyButton";
panel.Children.Add(btn);
elip = new Ellipse();
elip.Stroke = new SolidColorBrush(Colors.Red);
elip.Height = 60;
elip.StrokeThickness = 3;
panel.Children.Add(elip);
this.Content = panel;
}
[STAThread]
public static void Main()
{
Application app = new Application();
app.Run(new WindowByOnlyCode());
}
}
}
程序运行结果同上节,只是修改了窗口的标题,如下所示:
现在可以方便地实现上一节的业务逻辑,在 InitializeComponent 函数中为 Button 的 Click 事件注册事件处理器 btn_Click,然后在其中将椭圆的颜色修改成蓝色,如下代码所示:
public void InitializeComponent()
{
......
btn.Click += new RoutedEventHandler(btn_Click);
}
void btn_Click(object sender, RoutedEventArgs e)
{
elip.Stroke = new SolidColorBrush(Colors.Blue);
}
4.8 使用XAML和C#构建应用程序——刀剑合璧
XAML用来描述界面,C#用来实现业务逻辑。
4.8.1 第一次刀剑合璧
一个最为直接的想法是编写一个程序,解析前面例子中XAML的字符串,通过反射机制最终将这些字符串变成一个对象。这样的想法虽然直接,但是会是一种失败的“合璧”,会呈几何倍数地增大工作量。如果WPF提供了这样的解析类,则情况将不同。
WPF中提供了这样的解析类,在System.Windowa.Markup命名空间中包含一个 XamlReader 类,它通过一个静态的 Load 方法将 XAML 字符串解析为一个对应的对象。但是该方法不能直接接收字符串作为参数,而且需要一个 XmlReader对象作为输入参数,可以通过 StringReader 作为中介将字符串转换为一个 XmlTextReader 对象(XmlTextReader 派生自 XmlReader)。我们按照这样的思路,在前面的例子中第一次混合使用 XAML和C#来构建应用程序,代码如下所示:
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Shapes;
using System.Windows.Media;
using System.Windows.Markuo;
using System.Xml;
using System.IO;
namespace mumu_appbyxamlandcode1
{
public class WindowByXAMLAndCode : Window
{
public WindowByXAMLAndCode()
{
InitializeComponent();
}
public void InitializeComponent()
{
this.Width = 600;
this.Height = 200;
this.Title = "AppByXAMLAndCode";
string strXaml = "<StackPanel xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation' Width='500'>" +
"<Button Content='MyButton'/>" + "<Ellipse Stroke='Red' Height='60' StrokeThickness='3'/>" + "</StackPanel>";
StringReader strReader = new StringReader(strXaml);
XmlTextReader xmlreader = new XmlTextReader(strReader);
StackPanel obj = (StackPanel)XamlReader.Load(xmlreader);
Content = obj;
}
[STAThread]
public static void Main()
{
Application app = new Application();
app.Run(new WindowByXAMLAndCode());
}
}
}
由于涉及 StringReader、XmlTextReader 和 XamlReader 几个新类,因此需要增加一个 System.Xml.dll 的引用,同时需要增加 System.Windows.Markup、System.Xml 和 Syatem.IO 命名空间。除了改变窗口的标题,程序运行结果和前面一节相同。
当然也可以不从字符串,而直接从一个松散XAML文件解析对象,如将前面的字符串写成一个松散XAML文件(mumu_stackpanel.xaml)后添加到项目中。注意 mumu_stackpanel.xaml文件的 Build Action选项(生成操作)一定要设置为 Resource,
如下代码所示:
public void InitializeComponent()
{
this.Width = 600;
this.Height = 200;
this.Title = "AppByXAMLAndCode";
Uri uri = new Uri("pack://application:,,,/mumu_stackpanel.xaml");
Stream stream = Application.GetResourceStream(uri).Stream;
StackPanel obj = (StackPanel)XamlReader.Load(stream);
Content = obj;
}
如果为 Button 添加事件,则需要为 mumu_stackpanel.xaml 添加 Name 属性,如下代码所示:
<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" Width="500">
<Button Name="btn" Content="MyButton"/>
<Ellipse Name="elip" Stroke="Red" Height="60" StrokeThickness="3"/>
</StackPanel>
在 InitializeComponent 函数中通过 FindName 方法找到按钮和椭圆对象,将椭圆对象保存在事先定义的成员变量中,然后为按钮对象注册 Click 事件处理函数。在该函数中通过保存的椭圆对象来改变边界颜色,如下代码所示:
public class WindowByXAMLAndCode : Window
{
private Ellipse elip;
public void InitializeComponent()
{
this.Width = 600;
this.Height = 200;
this.Title = "AppByXAMLAndCode";
Uri uri = new Uri("pack://application:,,,/mumu_stackpanel.xaml");
Stream stream = Application.GetResourceStream(uri).Stream;
StackPanel obj = (StackPanel)XamlReader.Load(stream);
Content = obj;
elip = obj.FindName("elip") as Ellipse;
Button btn = obj.FindName("btn") as Button;
btn.Click += new RoutedEventHandler(btn_Click);
}
void btn_Click(object sender, RoutedEventArgs e)
{
if(elip != null)
{
elip.Stroke = new SolidColorBrush(Colors.Blue);
}
}
......
}
4.8.2 完美的刀剑合璧
1、Markup + Code + Behind
WPF提供了完美的刀剑合璧,看同样的例子。WPF的解决方案提供了 4 个文件(App.xaml 和 App.xaml.cs,MainWindow.xaml 和 MainWindow.xaml.cs),两两成对,如下图所示:
App.xaml 为应用程序的入口;App.xaml.cs 是其后台代码,用于实现业务逻辑,二者通过 x:Class 关联。在 XAML 文件中 x:Class 指定的类名必须和对应的 .cs 文件中的类名一致,在 .cs文件中类的声明必须加上关键字 partial。
MainWindow.xaml 为应用程序的主窗口,其中描述该窗口的界面组成。而对应的 .cs文件实现的是**事件响应处理函数,二者之间的关联和前面类似,但是注意必须在 MainWindow 的构造函数中调用 InitializeComponent方法**。
同样需要为按钮添加 Click 事件处理函数,在 MainWindow.xaml 文件的按钮标签中加上 Click=”btn_Click”。该函数的实现放在 MainWindow.xaml.cs 文件中,这就是 WPF中典型的 Markup + Code + Behind的做法。
4个文件的内容分别如下所示:
<Application x:Class="mumu_appbyxamlandcode2.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml">
<Application.Resources>
</Application.Resources>
</Application>
namespace mumu_appbyxamlandcode2
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
}
}
<Window x:Class="mumu_appbyxamlandcode2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="AppByXAMLAndCode2" Height="200" Width="600">
<StackPanel Width="500">
<Button Name="btn" Content="MyButton" Click="btn_Click"/>
<Ellipse Name="elip" Stroke="Red" Height="60" StrokeThickness="3"/>
</StackPanel>
</Window>
namespace mumu_appbyxamlandcode2
{
/// <summary>
/// Interaction logic for Window1.xaml
/// </summary>
public partial class MainWindow : Window
{
InitializeComponent();
}
private void btn_Click(object sender, RoutedEventArgs e)
{
if(elip != null)
{
elip.Stroke = new SolidColorBrush(Colors.Blue);
}
}
}
2、工作原理
我们看看WPF是如何将XAML文件和代码文件关联起来的:工程目录下 obj 子目录中的 Release 或者 Debug 子目录(取决于编译状态是 Debug 还是 Release)中后缀名为 .g.cs 的两个文件是App 和 MainWindow类的另外一部分。g(generated)指这些文件是自动产生的,两个文件的内容代码如下所示:
namespace mumu_appbyxamlandcode2
{
/// <summary>
/// App
/// </summary>
public partial class App : System.Window.Application
{
/// <summary>
/// InitializeComponent
/// </summary>
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
public void InitializeComponent()
{
#line 4 "..\..\App.xaml"
this.StartupUri = new System.Uri("MainWindow.xaml", System.UriKind.Relative);
#line default
#line hidden
}
/// <summary>
/// Application Entry Point.
/// </summary>
[System.STAThreadAttribute()]
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
//此处才是真正的函数入口
public static void Main()
{
mumu_appbyxamlandcode2.App app = new mumu_appbyxamlandcode2.App();
app.InitializeComponent();
app.Run();
}
}
}

我们可以看到代码①处才是真正的函数入口,App类仍从 Main函数开始,如下所示:
MainWindow.g.cs
在代码②和代码③处会发现 btn 和 elip 的字段声明,XAML 的名称和 XAML 文件中按钮和椭圆的 Name 属性一致。 btn 和 elip 会在 Connect 方法中(⑥)获得该窗口的按钮和椭圆对象的实例,并实现事件处理函数的注册。Connect 方法是IComponentConnector 必须实现的一个接口方法,在MainWindow 的 InitializeComponent 函数中(⑤)会根据初始的 xaml文件(这里是 MainWindow.xaml),将其转换为当前的应用对象。该函数调用 LoadComponent后将一个 bool 类型的私有变量设置为 true,以保证在程序的生命周期内请求的资源只加载一次。加载的资源是一个BAML文件(这里是MainWindow.baml),它和生成的 .g.cs 文件放在同一个目录下。
BAML 文件是一个二进制形式的XAML文件,它会作为一个二进制资源嵌入到程序集中,可以通过Reflector 工具查到,如下图所示。该文件比原来的 XAML文件小得多,这样会显著提高程序运行时的性能。
4.8.3 还有一种方法——在XAML中嵌入代码
其实 XAML和C#还有一种混合使用的方法,就是在XAML中嵌入C#代码,如下所示:
嵌入的程序代码需要使用 x:Code 元素以及 x:Code 中的CDATA(Character data,字符数据)节,它实际是XML中的规定。即必须以“<![CDATA[”开头,以“]]>”结尾。如果中间出现“]]>”这样的字符则会出现问题,如代码 array1[array2[i]]>5。这样的情况虽然罕见,但是一定要小心。如果遇到,则需要在“>”号中多加一个空格。
这样的方式看起来很方便,但是实际上很不灵活。为了嵌入代码该文件必须要使用 x:Class 关键字。此外 x:Code 中也不支持队命名空间的引用,即不能使用 using 这样的关键字,一个“松散”XAML文件变得只能编译执行。