在WPF中,由于引入了XAML语言。因此在界面设计方面,一般使用XAML语言,而在业务逻辑上用C#这样的后台代码,XAML和后台代码既可以配合得丝丝入扣,又可以将界面设计和业务逻辑分离。这就好比两仪剑法和两仪刀法,一正一反。但均是从八卦中化出,再回归八卦,可以说是殊途同归。

4.1 从C#到XAML

XAML与C#不同,但是与HTML和XML一样是一种声明式语言。下面是一段完整的XAML代码示例:

  1. <!--开始标签(Start Tag)-->
  2. <Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" Width="300" Height="200"><!--属性-->
  3. <!--两者中间的内容-->
  4. <Button.Content>
  5. Hello XAML
  6. </Button.Content>
  7. </Button>
  8. <!--结束标签(End Tag)-->
  1. XAML由开始标签、结束标签及二者中间的内容组成,开始标签中包括一个命名空间(xmlns)和两个属性说明(WidthHeight)。

下面是一个更为复杂的XAML文件的例子:

首先看显示效果:

第4章 XAML——反两仪刀法和正两仪剑法 - 图1第4章 XAML——反两仪刀法和正两仪剑法 - 图2

  1. <Window Title="XAML窗口" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" Width="300" Height="200">
  2. <DockPanel>
  3. <Button DockPanel.Dock="Left" Background="AliceBlue" Margin="0 5 0 10" Content="Hello XAML"/>
  4. <Button DockPanel.Dock="Right">
  5. <Button.Background>
  6. <LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
  7. <GradientStop Color="Yellow" Offset="0.0"/>
  8. <GradientStop Color="Red" Offset="0.25"/>
  9. <GradientStop Color="Blue" Offset="0.75"/>
  10. <GradientStop Color="LimeGreen" Offset="1.0"/>
  11. </LinearGradientBrush>
  12. </Button.Background>
  13. Hello XAML
  14. </Button>
  15. </DockPanel>
  16. </Window>
  1. 其中描述包含一个面板(DockPanel)的窗口(Window),在面板的左右侧各有一个按钮。需要注意的是,**窗口**不会像前面的按钮一样直接显示出来,**需要运行程序后才会弹出窗口**。

上面这段XAML代码相当于以下的C#代码:

  1. public class MyWindow : Window
  2. {
  3. [STAThread]
  4. public static void Main()
  5. {
  6. MyWindow win = new MyWindow();
  7. win.Show();
  8. Application app = new Application();
  9. app.Run();
  10. }
  11. public MyWindow()
  12. {
  13. InitializeComponent();
  14. }
  15. private void InitializeComponent()
  16. {
  17. this.Title = "XAML窗口";
  18. this.Width = 300;
  19. this.Height = 200;
  20. DockPanel panel = new DockPanel();
  21. Button btn = new Button();
  22. btn.Content = "Hello XAML";
  23. btn.Background = new SolidColorBrush(Color.AliceBlue);
  24. btn.Margin = new Thickness(0, 5, 0, 10);
  25. DockPanel.SetDock(btn, Dock.Left);
  26. panel.Children.Add(btn);
  27. Button btn2 = new Button();
  28. btn2.Content = "Hello XAML";
  29. LinearGradientBrush brush = new LinearGradientBrush();
  30. brush.StartPoint = new Point(0, 0);
  31. brush.EndPoint = new Point(1, 1);
  32. brush.GradientStops.Add(new GradientStop(Colors.Yellow, 0));
  33. brush.GradientStops.Add(new GradientStop(Colors.Red, 0.25));
  34. brush.GradientStops.Add(new GradientStop(Colors.Blue, 0.75));
  35. brush.GradientStops.Add(new GradientStop(Colors.LimeGreen, 1));
  36. btn2.Background = brush;
  37. DockPanel.SetDock(btn2, Dock.Right);
  38. panel.Children.Add(btn2);
  39. this.Content = panel;
  40. }
  41. }
  1. 不难发现,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语法”,如下所示:

  1. <Button DockPanel.Dock="Left"
  2. Background="AliceBlue" Margin="0 5 0 10" Content="Hello XAML"/>
  1. 新建完对象后,接下来的事情就是要为该对象设置属性。在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的命名空间。为了使该命名空间作用于整个文件,通常会将其放置在根元素中,如下所示:

  1. <Page xmlns="http://schema.microsoft.com/winfx/2006/xaml/presentation">
  2. <Button Width="300" Height="200">
  3. <Button.Content>
  4. Hello XAML
  5. </Button.Content>
  6. </Button>
  7. </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 对象不可以,因为它不能作为某个元素的子元素。

  1. 我们知道WPF中不同的控件分属于不同的命名空间,如 Window 类型属于 System.Windows 命名空间,而 Button 属于 System.Windows.Controls 命名空间。如果在C#代码中用到这些控件,则必须连续使用 using 语句,直到包括所有用到的命名空间,如下所示:
  1. using System.Windows;
  2. using System.Windows.Controls;
  3. using ......
  1. 而在WPF中一个URL标识就可以全部涵盖所有用到的控件的命名空间,原因是其在 PresentationFramework PresentationCore 这样的程序集中使用 XmlnsDefinition Xml CLR 命名空间关联起来。下面是通过 Reflector 与和 System.Windows System.Windows.Controls 相关联。XAML解析器会检查应用程序所加载的所有组件的 XmlnsDefinition 属性,以此确定对应的 CLR 命名空间。

第4章 XAML——反两仪刀法和正两仪剑法 - 图3

4.2.2 XAML的命名空间

第二个常用的命名空间是 XAML 的命名空间,如果需要使用 XAML 专用的元素和属性(事实上一定会使用),那么必须声明该声明空间。习惯上 XAML 的声明空间被声明成 x 前缀——xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml",一般使用 VS生成的默认的XAML文件都会包含 WPF 和 XAML 命名空间的声明,如下所示:

第4章 XAML——反两仪刀法和正两仪剑法 - 图4

其中 Window、Grid 和 Button 属于 WPF 命名空间中的元素,x:Class 属于 XAML 命名空间中的属性。由于 WPF 命名空间是默认空间,因此 Window、Grid 和 Button 没有声明前缀

第4章 XAML——反两仪刀法和正两仪剑法 - 图5

4.2.3 其他命名空间

1、使用系统类

除去前面两个命名空间,如果XAML文件要使用其他 .NET 对象或者应用程序或者其他程序集中的自定义对象,也要声明命名空间。如下所示:

第4章 XAML——反两仪刀法和正两仪剑法 - 图6

如果希望明确地将 Button 的 Content 属性设置为 String 类型(尽管这样做是画蛇添足),由于 String类型属于程序集 mscorlib.dll 且其命名空间为 System,所以在声明后才能使用,如下所示:

  1. 注意通过声明xmlns : s="clr-namespace : System; assembly=mscorlib"关联 System 命名空间和前缀 s,引号内的字符串开始是 clr-namespace,用来指定命名空间。assembly 用来指定程序集名称。

2、使用自定义类

有时需要自定义类,然后在 XAML 文件中使用,其中有如下两种情况:

(1)使用本地程序的自定义类

例如,在本地程序中定义一个 Book 类,如下所示:

  1. namespace mumu_customnamespace
  2. {
  3. public class Book
  4. {
  5. public Book() { }
  6. public string Name { get; set; }
  7. public double Price { get; set; }
  8. }
  9. }
  1. 如果需要在XAML文件中使用该类,必须做如下声明:

xmlns : local=”clr-namespace : mumu_customnamespace”

由于Book类属于本地应用程序,因此声明中忽略了设置 assembly,如下所示:

xmlns:local=”clr-namespace:mumu_customnamespace” Title=”Window1” Height=”300” Width=”300”>
  1. 但是运行程序,按钮上面只会显示该类的类型。显然这不是我们想要的效果,解决办法就是重载 ToString 函数,如下所示:
  1. public override string ToString()
  2. {
  3. string str = Name + "售价为:" + Price + "元";
  4. return str;
  5. }
  1. 再次运行,结果如下:

第4章 XAML——反两仪刀法和正两仪剑法 - 图7

自定义类必须有一个默认的无参数的构造函数,比如下面的 Book 类,程序可能运行时会因无法实例化该类而报异常:

public class Book

{

**public Book(string name, double price){ }**

  1. ……

}

  1. 但是编译器有时也可能会自动地添加一个无参数的构造函数,因此这样的问题算是一类不容易发现的 Bug。我们**<font style="color:#F5222D;">在自定义类时最好显式地添加无参数的构造函数</font>**以避免此类 Bug

(2)使用外部程序的自定义类

使用外部程序的自定义类需要设置 assembly,如新建一个类型为 Class Library,名为“mumu_customlib”的项目。然后将刚才的 Book 类添加到其中。

第4章 XAML——反两仪刀法和正两仪剑法 - 图8

现在一个解决方案中有两个项目,其中 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

  1. C#中还有一种枚举类型的值可以通过“或”运算符(|)组合而成,如下所示:
  1. public enum CarOptions
  2. {
  3. SunRoof = 0x01;
  4. Spoiler = 0x02;
  5. FogLights = 0x04;
  6. TintedWindows = 0x08;
  7. }
  8. class FlagTest
  9. {
  10. static void Main()
  11. {
  12. CarOptions options = CarOptions.SunRoof | CarOptions.FogLights;
  13. }
  14. }
  1. 此类枚举值称为“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”

/>

第4章 XAML——反两仪刀法和正两仪剑法 - 图9

即使是该属性,WPF中也提供了一个 BoldItalicSimulation 枚举值,从而省略了枚举值之间的组合。

4.3.2 附加属性

附加属性是可以用于多个控件,但是在另外一个类中定义的属性。在WPF中该类属性常用在布局中,如前面例子中的 DockPanel.Dock=”Left”。附加属性的命名方式是“定义类型 .属性”,这样可以让XAML解析器将其与普通属性区分开。附加属性的设置可以使用 Attribute 和 Property-Element 语法,使用后者时类型必须是包含该属性的类型,如下代码所示:

  1. <Button>
  2. <!--这里是DockPanel,而不是Button-->
  3. <DockPanel.Dock>
  4. Right
  5. </DockPanel.Dock>
  6. Hello XAML
  7. </Button>

4.4 Content属性

在前面的例子中可以看到 XAML 文件好像是一棵大的嵌套树,Window包含 StackPanel、DockPanel 和 Button,其中 Content属性起到了重要的作用。很多 WPF 类中都有这样一个特殊的属性,即 Content(内容)属性,我们在前面已经看到 Button 和 Content 属性放置的是一个字符串。

  1. <Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  2. Width="300" Height="200">
  3. <Button.Content>
  4. Hello XAML
  5. </Button.Content>
  6. </Button>
  1. <Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  2. Width="300" Height="200" Content="Hello XAML">
  3. </Button>
  1. <Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  2. Width="300" Height="200">
  3. Hello XAML
  4. </Button>

Content属性值可以放在 Button 元素中间的任何位置,如下面代码中的“Hello XAML”放置在 Button 元素的末尾处,同样也可以放置在标记1和2处。

  1. <Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
  2. <!--1 Hello XAML-->
  3. <Button.Width>
  4. 300
  5. </Button.Width>
  6. <!--2 Hello XAML-->
  7. <Button.Height>
  8. 200
  9. </Button.Height>
  10. Hello XAML
  11. </Button>
  1. 但是 Content 不能分开放置,不允许在 Content 的值中插入其他标签。以下的写法是错误的:
  1. 例外情况是 TextBlock Content 属性中可以放置一些加粗或者斜体标签,如下所示:
  1. <TextBlock xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
  2. <Italic>Hi everyOne,</Italic>This is <Bold>XAML</Bold>
  3. </TextBlock>
  1. Content属性不需要使用 Property-Element 语法即可直接成为 Button 元素的子类,**这是因为当用户定义一个类时使用 ContentProperty 来说明某个属性是 Content 属性**,这样WPF就知道该属性可以不需要使用 Property-Element 语法。如 Button 类,通过 Reflector 可以查看到其基类 ContentControl 的标识,如下所示:

第4章 XAML——反两仪刀法和正两仪剑法 - 图10

同样可以为前面的 Book 类添加一个Text属性,并且将其指定为 Content 属性。然后在 ToString 函数中添加 Text 的内容,如下所示:

  1. [ContentProperty("Text")]
  2. public class Book
  3. {
  4. public string Text{ get; set; }
  5. public override string ToString()
  6. {
  7. string str = Name + "售价为:" + Price + "元" + "\n" + Text;
  8. return str;
  9. }
  10. ...
  11. }
  1. XAML 文件中也可以按照内容属性语法来设置 Book Text 属性值。如:
  1. <Button FontSize="14">
  2. <customlib:Book Name="葵花宝典" Price="0.1">
  3. 欲练此功 必先自宫
  4. </customlib:Book>
  5. </Button>
  1. WPF中到底有哪些类有 Content 属性,它们的 Content 属性的名字又是什么?

第4章 XAML——反两仪刀法和正两仪剑法 - 图11

从上图中可以看出不仅仅是名称为 Content 属性才是真正的 Content属性,而 Panel 的 Children 及 MenuBase 的 Items 属性也是 Content 属性。这两种属性和 Content属性不同的是它们是一个集合属性,可以放置一个或多个对象。

4.5 类型转换器

4.5.1 功能

XAML解析器通过类型转换器跨越字符串值和非字符串值的鸿沟,在XAML中输入的字符串通过类型转换器将这些字符串转换为相应的 CLR 对象。这就是类型转化器所起到的作用,如下图所示:

第4章 XAML——反两仪刀法和正两仪剑法 - 图12

所有的类型转换器都派生自 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 源码片段:

第4章 XAML——反两仪刀法和正两仪剑法 - 图13

(2)如果在属性声明中没有 TypeConverter 特性,XAML解析器会检查对应数据类型的类的声明。如按钮的 Background 属性类型是 Brush,并在 Brush类头部就声明了一个 BrushConverter 转换器,这样在设置 Background 属性时 XAML 解析器会自动应用该转换器。下图是通过 Reflector 查看到的 Brush源码片段:

第4章 XAML——反两仪刀法和正两仪剑法 - 图14

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 类型的对象,如下所示:

  1. [TypeConverter(typeof(MoneyConverter))]
  2. public class MoneyType
  3. {
  4. private double _value;
  5. public MoneyType()
  6. {
  7. _value = 0;
  8. }
  9. public MoneyType(double value)
  10. {
  11. _value = value;
  12. }
  13. public override string ToString()
  14. {
  15. return _value.ToString();
  16. }
  17. public static MoneyType Parse(string value)
  18. {
  19. string str = (value as string).Trim();
  20. if(str[0] == '$')
  21. {
  22. string newprice = str.Remove(0, 1);
  23. double price = double.Parse(newprice);
  24. return new MoneyType(price * 8);
  25. }
  26. else
  27. {
  28. double price = double.Parse(str);
  29. return new MoneyType(price);
  30. }
  31. }
  32. }
  1. 在第一行中为这个类提供了一个 MoneyConverter 类型的转换器。下面是其实现。为了使这个类看起来比较完整,将实现其 4 个方法。事实上关键的是 ConvertFrom 方法,通过 MoneyType 的静态方法 Parse 将字符串转换为正确的 MoneyType 对象,如下代码所示:
  1. public class MoneyConverter : TypeConverter
  2. {
  3. public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
  4. {
  5. if(sourceType == typeof(string))
  6. return true;
  7. return base.CanConvertFrom(context, sourceType);
  8. }
  9. public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
  10. {
  11. if(destinationType == typeof(string))
  12. return true;
  13. return base.CanConvertTo(context, destinationType);
  14. }
  15. public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
  16. {
  17. if(value.GetType() != typeof(string))
  18. return base.ConvertFrom(context, culture, value);
  19. return MoneyType.Parse((string)value);
  20. }
  21. public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
  22. {
  23. if(destinationType == typeof(string))
  24. return base.ConvertTo(context, culture, value, destinationType);
  25. return value.ToString();
  26. }
  27. }
  1. 在原来的XAML文件中 Price 的值前面加上一个“$”符号,**<font style="color:#E8323C;">同时,将Price的类型从 double改成 MoneyType</font>**,再次运行程序,结果如下:

第4章 XAML——反两仪刀法和正两仪剑法 - 图15

4.6 标记扩展

现在绝大多数的属性 XAML 都可以工作得很好了,但是依旧会有几种情况 XAML 难以胜任。

(1)将一个属性赋值为 null。

(2)将一个属性赋值给一个静态变量,如将按钮的 Background 赋值为一个预定义画刷,参看如下代码(在C#中将Background属性设置为一个静态变量):

Button btn = new Burron();

btn.Content = “Hello XAML”;

btn.Background = SystemColors.ActiveCaptionBrush;

……

  1. 以上这几种情况,我们就需要使用标记扩展了。类型转换器悄无声息的实现了类型转换,而**标记扩展则是通过 XAML 的显式的、一致的语法调用实现**。标记扩展比类型转化器更为强大,和类型转换器一样,标记扩展也可以通过自定义来实现 XAML 的语义扩展。

在 XAML 中只要属性值被一对花括号{}括起,XAML 解析器就会认为这是一个标记扩展,而不是一个普通的字符串。前面的两种情况都可以用标记扩展的方法来解决,将一个按钮的 Background 属性赋值为空,表示这个按钮的背景颜色为透明色。{x:Null}是 XAML 中提供的一种标记扩展,表示一个空值,如下代码所示:

<Button Name="btn" Content="MyButton" Click="btn_Click" **Background="{x:Null}"**>

XAML 中提供了一个{x:Static}的标记扩展,可以引用一个类的静态变量,如下所示:

  1. <Button Name="btn" Content="MyButton" Click="btn_Click">
  2. <Button.Background>
  3. <x:Static Member="SystemColors.ActiveCaptionBrush"/>
  4. </Button.Background>
  5. </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文件组成。

  1. <Application
  2. xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3. StartupUri="Window1.xaml">
  4. </Application>
  1. <Window
  2. xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3. Title="AppByOnlyXAML" Height="200" Width="600">
  4. <StackPanel Width="500">
  5. <Button Content="MyButton"/>
  6. <Ellipse Stroke="Red" Height="60" StrokeThickness="3"/>
  7. </StackPanel>
  8. </Window>
  1. 应用程序的入口是 App.xaml文件,XAML 文件的根元素之一,即 Application。该文件的 BuildAction 必须设置为 ApplicationDefinition(右击App.xaml文件,选择快捷菜单中的Properties选项,然后在打开的 Properties 对话框中设置)。Application 元素的 StartupUri 属性设置需要启动的窗口文件,在这里是 Window1.xaml文件,如下图所示:

第4章 XAML——反两仪刀法和正两仪剑法 - 图16

Window1.xaml 文件的 Build Action 选项设置为 Page,该窗口中包含一个面板StackPanel。其中放置一个按钮和一个椭圆形,运行结果如下图所示:

第4章 XAML——反两仪刀法和正两仪剑法 - 图17

如果单击按钮令椭圆的边界色由红色变成蓝色,则难以处理。XAML 毕竟是一种简单的声明式的语言,对于这样的应用程序业务逻辑显得力不从心,因此需要更为强大的C#语言——正两仪剑法。

4.7.2 C#——正两仪剑法

C#强大到任何XAML写的应用程序都可以轻易地通过C#实现,我们通过代码方式将这个 StackPanel 作为一个窗口的 Content 属性显示。如下代码所示,函数 InitializeComponent主要用来描述界面设置。虽然有的时候我们会抱怨 XAML 过于冗长,但是在描述界面方面,它确实比用代码的方式要简洁和清晰很多。

  1. using System;
  2. using System.Windows;
  3. using System.Windows.Controls;
  4. using System.Windows.Shapes;
  5. using System.Windows.Media;
  6. namespace mumu_appbyonlycode
  7. {
  8. public class WindowByOnlyCode : Window
  9. {
  10. public WindowByOnlyCode()
  11. {
  12. InitializeComponent();
  13. }
  14. private Ellipse elip;
  15. public void InitializeCompoment()
  16. {
  17. this.Width = 600;
  18. this.Height = 200;
  19. this.Title = "AppBuOnlyCode";
  20. StackPanel panel = new StackPanel();
  21. panel.Width = 500;
  22. Button btn = new Button();
  23. btn.Content = "MyButton";
  24. panel.Children.Add(btn);
  25. elip = new Ellipse();
  26. elip.Stroke = new SolidColorBrush(Colors.Red);
  27. elip.Height = 60;
  28. elip.StrokeThickness = 3;
  29. panel.Children.Add(elip);
  30. this.Content = panel;
  31. }
  32. [STAThread]
  33. public static void Main()
  34. {
  35. Application app = new Application();
  36. app.Run(new WindowByOnlyCode());
  37. }
  38. }
  39. }
  1. 程序运行结果同上节,只是修改了窗口的标题,如下所示:

第4章 XAML——反两仪刀法和正两仪剑法 - 图18

现在可以方便地实现上一节的业务逻辑,在 InitializeComponent 函数中为 Button 的 Click 事件注册事件处理器 btn_Click,然后在其中将椭圆的颜色修改成蓝色,如下代码所示:

  1. public void InitializeComponent()
  2. {
  3. ......
  4. btn.Click += new RoutedEventHandler(btn_Click);
  5. }
  6. void btn_Click(object sender, RoutedEventArgs e)
  7. {
  8. elip.Stroke = new SolidColorBrush(Colors.Blue);
  9. }

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#来构建应用程序,代码如下所示:

  1. using System;
  2. using System.Windows;
  3. using System.Windows.Controls;
  4. using System.Windows.Shapes;
  5. using System.Windows.Media;
  6. using System.Windows.Markuo;
  7. using System.Xml;
  8. using System.IO;
  9. namespace mumu_appbyxamlandcode1
  10. {
  11. public class WindowByXAMLAndCode : Window
  12. {
  13. public WindowByXAMLAndCode()
  14. {
  15. InitializeComponent();
  16. }
  17. public void InitializeComponent()
  18. {
  19. this.Width = 600;
  20. this.Height = 200;
  21. this.Title = "AppByXAMLAndCode";
  22. string strXaml = "<StackPanel xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation' Width='500'>" +
  23. "<Button Content='MyButton'/>" + "<Ellipse Stroke='Red' Height='60' StrokeThickness='3'/>" + "</StackPanel>";
  24. StringReader strReader = new StringReader(strXaml);
  25. XmlTextReader xmlreader = new XmlTextReader(strReader);
  26. StackPanel obj = (StackPanel)XamlReader.Load(xmlreader);
  27. Content = obj;
  28. }
  29. [STAThread]
  30. public static void Main()
  31. {
  32. Application app = new Application();
  33. app.Run(new WindowByXAMLAndCode());
  34. }
  35. }
  36. }
  1. 由于涉及 StringReaderXmlTextReader XamlReader 几个新类,因此需要增加一个 System.Xml.dll 的引用,同时需要增加 System.Windows.MarkupSystem.Xml Syatem.IO 命名空间。除了改变窗口的标题,程序运行结果和前面一节相同。

当然也可以不从字符串,而直接从一个松散XAML文件解析对象,如将前面的字符串写成一个松散XAML文件(mumu_stackpanel.xaml)后添加到项目中。注意 mumu_stackpanel.xaml文件的 Build Action选项(生成操作)一定要设置为 Resource,第4章 XAML——反两仪刀法和正两仪剑法 - 图19

如下代码所示:

  1. public void InitializeComponent()
  2. {
  3. this.Width = 600;
  4. this.Height = 200;
  5. this.Title = "AppByXAMLAndCode";
  6. Uri uri = new Uri("pack://application:,,,/mumu_stackpanel.xaml");
  7. Stream stream = Application.GetResourceStream(uri).Stream;
  8. StackPanel obj = (StackPanel)XamlReader.Load(stream);
  9. Content = obj;
  10. }
  1. 如果为 Button 添加事件,则需要为 mumu_stackpanel.xaml 添加 Name 属性,如下代码所示:
  1. <StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" Width="500">
  2. <Button Name="btn" Content="MyButton"/>
  3. <Ellipse Name="elip" Stroke="Red" Height="60" StrokeThickness="3"/>
  4. </StackPanel>
  1. InitializeComponent 函数中通过 FindName 方法找到按钮和椭圆对象,将椭圆对象保存在事先定义的成员变量中,然后为按钮对象注册 Click 事件处理函数。在该函数中通过保存的椭圆对象来改变边界颜色,如下代码所示:
  1. public class WindowByXAMLAndCode : Window
  2. {
  3. private Ellipse elip;
  4. public void InitializeComponent()
  5. {
  6. this.Width = 600;
  7. this.Height = 200;
  8. this.Title = "AppByXAMLAndCode";
  9. Uri uri = new Uri("pack://application:,,,/mumu_stackpanel.xaml");
  10. Stream stream = Application.GetResourceStream(uri).Stream;
  11. StackPanel obj = (StackPanel)XamlReader.Load(stream);
  12. Content = obj;
  13. elip = obj.FindName("elip") as Ellipse;
  14. Button btn = obj.FindName("btn") as Button;
  15. btn.Click += new RoutedEventHandler(btn_Click);
  16. }
  17. void btn_Click(object sender, RoutedEventArgs e)
  18. {
  19. if(elip != null)
  20. {
  21. elip.Stroke = new SolidColorBrush(Colors.Blue);
  22. }
  23. }
  24. ......
  25. }

4.8.2 完美的刀剑合璧

1、Markup + Code + Behind

WPF提供了完美的刀剑合璧,看同样的例子。WPF的解决方案提供了 4 个文件(App.xaml 和 App.xaml.cs,MainWindow.xaml 和 MainWindow.xaml.cs),两两成对,如下图所示:

第4章 XAML——反两仪刀法和正两仪剑法 - 图20

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个文件的内容分别如下所示:

  1. <Application x:Class="mumu_appbyxamlandcode2.App"
  2. xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3. xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4. StartupUri="MainWindow.xaml">
  5. <Application.Resources>
  6. </Application.Resources>
  7. </Application>
  1. namespace mumu_appbyxamlandcode2
  2. {
  3. /// <summary>
  4. /// Interaction logic for App.xaml
  5. /// </summary>
  6. public partial class App : Application
  7. {
  8. }
  9. }
  1. <Window x:Class="mumu_appbyxamlandcode2.MainWindow"
  2. xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3. xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4. Title="AppByXAMLAndCode2" Height="200" Width="600">
  5. <StackPanel Width="500">
  6. <Button Name="btn" Content="MyButton" Click="btn_Click"/>
  7. <Ellipse Name="elip" Stroke="Red" Height="60" StrokeThickness="3"/>
  8. </StackPanel>
  9. </Window>
  1. namespace mumu_appbyxamlandcode2
  2. {
  3. /// <summary>
  4. /// Interaction logic for Window1.xaml
  5. /// </summary>
  6. public partial class MainWindow : Window
  7. {
  8. InitializeComponent();
  9. }
  10. private void btn_Click(object sender, RoutedEventArgs e)
  11. {
  12. if(elip != null)
  13. {
  14. elip.Stroke = new SolidColorBrush(Colors.Blue);
  15. }
  16. }
  17. }

2、工作原理

我们看看WPF是如何将XAML文件和代码文件关联起来的:工程目录下 obj 子目录中的 Release 或者 Debug 子目录(取决于编译状态是 Debug 还是 Release)中后缀名为 .g.cs 的两个文件是App 和 MainWindow类的另外一部分。g(generated)指这些文件是自动产生的,两个文件的内容代码如下所示:

  1. namespace mumu_appbyxamlandcode2
  2. {
  3. /// <summary>
  4. /// App
  5. /// </summary>
  6. public partial class App : System.Window.Application
  7. {
  8. /// <summary>
  9. /// InitializeComponent
  10. /// </summary>
  11. [System.Diagnostics.DebuggerNonUserCodeAttribute()]
  12. public void InitializeComponent()
  13. {
  14. #line 4 "..\..\App.xaml"
  15. this.StartupUri = new System.Uri("MainWindow.xaml", System.UriKind.Relative);
  16. #line default
  17. #line hidden
  18. }
  19. /// <summary>
  20. /// Application Entry Point.
  21. /// </summary>
  22. [System.STAThreadAttribute()]
  23. [System.Diagnostics.DebuggerNonUserCodeAttribute()]
  24. //此处才是真正的函数入口
  25. public static void Main()
  26. {
  27. mumu_appbyxamlandcode2.App app = new mumu_appbyxamlandcode2.App();
  28. app.InitializeComponent();
  29. app.Run();
  30. }
  31. }
  32. }
  1. ![](https://cdn.nlark.com/yuque/0/2022/png/26289675/1652681192306-df303429-4587-4a10-bbb6-5799f802c843.png)

我们可以看到代码①处才是真正的函数入口,App类仍从 Main函数开始,如下所示:

MainWindow.g.cs

第4章 XAML——反两仪刀法和正两仪剑法 - 图21 第4章 XAML——反两仪刀法和正两仪剑法 - 图22

在代码②和代码③处会发现 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章 XAML——反两仪刀法和正两仪剑法 - 图23

4.8.3 还有一种方法——在XAML中嵌入代码

其实 XAML和C#还有一种混合使用的方法,就是在XAML中嵌入C#代码,如下所示:

第4章 XAML——反两仪刀法和正两仪剑法 - 图24

嵌入的程序代码需要使用 x:Code 元素以及 x:Code 中的CDATA(Character data,字符数据)节,它实际是XML中的规定。即必须以“<![CDATA[”开头,以“]]>”结尾。如果中间出现“]]>”这样的字符则会出现问题,如代码 array1[array2[i]]>5。这样的情况虽然罕见,但是一定要小心。如果遇到,则需要在“>”号中多加一个空格。

这样的方式看起来很方便,但是实际上很不灵活。为了嵌入代码该文件必须要使用 x:Class 关键字。此外 x:Code 中也不支持队命名空间的引用,即不能使用 using 这样的关键字,一个“松散”XAML文件变得只能编译执行。