用ASP.NET Core编写Web应用程序时,除了需要知道C#之外,还需要了解HTML、CSS和JavaScript。创建Windows应用程序时,除了C#之外,还需要了解XAML。XAML不仅用于创建Windows应用程序,还用于Windows Presentation Foundation(WPF)、Windows WorkFlow Foundation(WCF)和Xamarin的跨平台应用程序。
可以用XAML完成的工作都可以用C#实现,每个XAML元素都用一个类表示,因此可以从C#中访问。那么,为什么还需要XAML?XAML通常用于描述对象及其属性,可以描述很深的层析结构。例如,Page包含一个Grid控件,Grid控件包含一个StackPanel和其他控件,StackPanel包含按钮和文本框控件。XAML便于描述这种层析结构,并通过XML特性或元素分配对象的属性。
XAML允许以声明的方式编写代码,而C#主要是一种命令式编程语言。XAML支持声明式定义。在命令式编程语言(如C#)中,用C#代码定义一个for循环,编译器就使用中间语言(IL)代码创建一个for循环。在声明性编程语言中,声明应该做什么,而不是如何完成。
注意:
虽然C#不是纯粹的命令式编程语言,但使用LINQ时,也是在以声明方式编写语法。Entity Framework Core(EF Core)的LINQ提供程序将LINQ查询转换为SQL语句。
XAML是一个XML语法,但它定义了XML的几个增强特性。XAML仍然是有效的XML。但是一些增强特性有特殊的意义和特殊的功能,例如,在XML特性中使用花括号,对于XML,这仍然只是一个字符串,因此是有效的XML。对于XAML,这是一个标记扩展。
在有效使用XAML之前,需要了解这门语言的一些重要特性。本章介绍了如下XAML特性:
依赖属性:从外部看起来,依赖属性像正常属性。然而,它们需要更少的存储空间,实现了变更通知。 路由事件:从外部看起来,路由事件像正常的.NET事件。然而,通过添加和删除访问器来使用自定义事件实现方式,就允许冒泡和隧道。事件从外部控件进入内部控件称为隧道,从内部控件进入外部控件称为冒泡。 附加属性:通过附加属性,可以给其他控件添加属性。例如,按钮控件没有属性用于把它自己定位在Grid控件的特性行和列上。在XAML中,看起来有这样一个属性。 标记扩展:编写XML特性需要的编码比编写XML元素少。然而,XML特性只能是字符串;使用XML元素可以编写更强大的语法。为了减少需要编写的代码量,标记扩展允许在特性中编写强大的语法。
1. XAML标准
WPF、UWP和Xamarin对XAML元素使用(部分仍然使用)不同的语法。例如,对于WPF和UWP,按钮有Content属性,而Xamarin的按钮有Text属性。在WPF和UWP中,可以使用StackPanle来排列多个元素。在Xamarin中,类似的控件是StackLayout。
为了更容易地在不同的UI技术堆栈之间切换,定义了XAML标准。有关标准的实际状态,请参见 https://github.com/Microsoft/xaml-standard/ 。
2. 将元素映射到类
在每个XAML元素的后面都有一个具有属性、方法和事件爱你的类。如前所述,可以使用C#代码或使用XAML创建UI元素。下面看一个例子。使用以下代码片段,定义了一个包含按钮控件的StakcPanel。使用XAML特性,按钮分配了Content属性和Click事件。Content属性只包含一个简单的字符串,而Click事件引用了方法OnButtonClick的地址。XML特性x:Name用于向按钮控件声明一个名词,该名称可以在XAML和C#代码隐藏文件中使用:
在页面顶部,可以看到带有XML特性x:Class的Page袁术。这定义了类的名称,在该类中,XAML编译器生成了部分代码。使用Visual Studio中的代码隐藏文件,可以看到这个类中能修改的部分:
代码隐藏文件包含类MainPage的一部分(XAML编译器没有生成这个部分)。在构造函数中,调用方法InitializeComponent。InitializeComponet的实现是由XAML编译器创建的。该方法加载XAML文件,并将其转换为XAML文件中的根元素指定的对象。OnButtonClick方法是之前在XAML代码中创建的按钮的Click事件处理程序。这个实现打开了一个MessageDialog:
public MainPage() { this.InitializeComponent(); } private async void OnButtonClick(object sender, RoutedEventArgs e) { await new MessageDialog("button 1 clicked").ShowAsync(); }
现在,在C#代码的Button类中创建一个新对象,并将其添加到现有的StackPanel中。在下面的代码片段中,修改了MainPage的构造函数,以创建一个新按钮,设置Content属性,并为Click事件分配一个Lambda表达式。最后,新创建的按钮添加到StackPanel的Children中:
public MainPage() { this.InitializeComponent(); var button2 = new Button { Content = "created dynamically" }; button2.Click += async (sender,e) => { await new MessageDialog("button 2 clicked").ShowAsync(); }; stackPanel1.Children.Add(button2); }
如前所述,XAML只是处理对象、属性和事件的另一种方式。下一节将展示XAML在用户界面上的优势。
3. 通过XAML使用定制的.NET类
要在XAML代码中使用自定义的.NET类,可以使用简单的POCO类,对类定义没有特殊要求。只需要将.NET名称空间添加到XAML声明中。为了演示这一点,下面设计一个具有FirstName和LastName属性的简单Person类:
public class Person { public string FirstName { get; set; } public string LastName { get; set; } public override string ToString() => $"{FirstName} {LastName}"; }
在XAML中定义了一个名为datalib的XML的名称空间别名,它映射到程序集DataLib中的.NET名称空间DataLib。有了这个别名,现在就可以把别名作为元素前缀,来使用这个名称空间中的所有类。
在XAML代码中添加一个列表框,其中包含Person类型的项。使用XAML特性,可以设置属性FirstName和LastName的值。运行应用程序时,ToString()方法的输出显示在列表框中:
注意:
WPF和Xamarin在别名声明中使用 clr-namespace 而不是 using 。原因是,使用UWP的XAML既不基于.NET,也不局限于.NET。可以使用本机C++和XAML。因此clr(公共语言运行库)就不适合了。
4. 将属性用作特性
在前面的XAML示例中,类的属性用XML特性来设置。要在XAML中设置属性,只要属性的类型可以表示为字符串,或者可以把字符串转换为属性类型,就可以把属性设置为特性。下面的代码片段用XML特性设置了Button元素的Content和Background属性。
在上面的代码片段中,因为Content属性的类型是object,所以可以接受字符串。Background属性的类型是Brush,字符串转换为派生自Brush的SolidColorBrush类型。
5. 将属性用作元素
总是可以使用元素语法给属性提供值。Button类的Background属性可以用属性(如LinearGradientBrush),如下面的示例所示。
使用元素代替特性,可以把比较复杂的画笔应用于Background属性(LinearGradientBrush),如下面的示例所示。
注意:
当设置示例中的内容时,Content特性和Button.Content元素都不用于编写内容;相反,内容会直接写入为Button元素的子元素值。这是因为在Button类的基类ContentControl中,ContentProperty特性通过[ContentProperty("Content")]应用,这个特性把Content属性标记为ContentProperty。这样,XAML元素的直接子元素就应用于Content属性。
3. 依赖属性
XAML使用依赖属性完成数据绑定、动画、属性变更通知、样式化等。依赖属性存在的原因是什么、假设创建一个类,它有100个int类型的属性,这个类在一个表单上实例化乐100次。需要多少内存?因为int的大小是4个字节,所以结果是4*100*100=40 000字节。刚才看到的是一个XAML元素的属性?由于继承层次结构非常大,一个XAML元素定义了数以百计的属性。属性类型不是简单的int,而是更复杂的类型。这样的属性会消耗大量的内存。然而,通常只改变其中一些属性的值,大部分的属性保持对所有实例都相同的默认值。这个难题可以用依赖属性解决。使用依赖属性,对象内存不是分配给每个属性和实例。依赖属性系统管理一个包含所有属性的字典,只有值发生了改变才分配内存。否则,默认值就下所有实例之间共享。
依赖属性也内置了属性变更通知的支持。对于普通属性,需要为属性变更通知实现INotifyPropertyChanged接口。这种变更机制是通过依赖属性内置的。对于数据绑定,绑定到.NET属性源上的UI元素必须是依赖属性。现在,详细讨论依赖属性。
从外部来看,依赖属性像是正常的.NET属性。但是,正常的.NET属性通常还定义了由该属性的get和set访问器访问的数据成员。
private int _value; public int Value { get => _value; set => _value = value; }
依赖属性不是这样。依赖属性通常也有get和set访问器。它们与普通属性是相同的。但在get和set访问器的实现代码中,调用了GetValue()和SetValue()方法。GetValue()和SetValue()方法是基类DependencyObject的成员,依赖对象需要使用这个类——它们必须在DependencyObject的派生类中实现。
有了依赖属性,数据成员就放在由基类管理的内部集合中,仅在值发生变化时分配数据。对于没有变化的值,数据可以在不同的实例或基类之间共享。GetValue()和SetValue()方法需要一个DependencyObject参数。这个参数由类的一个静态成员定义,该静态成员与属性同名,并在该属性名的后面追加Property术语。对于Value属性,静态成员的名称是ValueProperty。DependencyProperty.Register()是一个辅助方法,可在依赖属性系统中注册属性。在下面的代码片段中,使用Register()方法和4个参数定义了属性名、属性的类型和拥有者的类型(即MyDependencyObject类),使用PerpertyMetadata指定了默认值。
public class MyDependencyObject:DependencyObject { public int Value { get => (int)GetValue(valueProperty); set => SetValue(valueProperty,value); } public static readonly DependencyProperty valueProperty = DependencyProperty.Register("Value",typeof(int), typeof(MyDependencyObject),new PropertyMetadata(0)); }
7. 创建依赖属性
下面的示例定义的不是一个依赖属性,而是3个依赖属性。MyDependencyObject类定义了依赖属性Value、Minimum和Maximum。所有这些属性都是用DependencyProperty.Register()方法注册的依赖属性。GetValue()和SetValue()方法是基类DependencyObject的成员。对于Minimum和Maximum属性,定义了默认值,用DependencyProperty.Register()方法设置该默认值,可以把第4个参数设置为PropertyMetadata。使用带一个参数PropertyMetadata的构造函数,把Minimum属性设置为0,把Maximum属性设置为100。
public class MyDependencyObject : DependencyObject { public int Value { get => (int)GetValue(ValueProperty); set => SetValue(ValueProperty, value); } public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(nameof(Value), typeof(int), typeof(MyDependencyObject), new PropertyMetadata(0)); public int Minimum { get => (int)GetValue(MinimumPropety); set => SetValue(MinimumPropety, value); } public static readonly DependencyProperty MinimumPropety = DependencyProperty.Register(nameof(Minimum), typeof(int), typeof(MyDependencyObject), new PropertyMetadata(0)); public int Maximum { get => (int)GetValue(MaximumProperty); set => SetValue(MaximumProperty, value); } public static readonly DependencyProperty MaximumProperty = DependencyProperty.Register(nameof(Maximum), typeof(int), typeof(MyDependencyObject), new PropertyMetadata(100)); }
注意:
在get和set属性访问器的实现代码中,只能调用GetValue()和SetValue()方法。使用依赖属性,可以通过GetValue()和SetValue()方法从外部访问属性的值。UWP也是这样作的。因此,强类型化的属性访问器可能根本就不会被调用,包含它们仅为了方便在自定义代码中使用正常的属性语法。
8. 值变更回调和事件
为了获得值变更的信息,依赖属性还支持值变更回调。在属性发生变化时调用的DependencyProperty.Register()方法中,可以添加一个DependencyPropertyChanged事件处理程序。在示例代码中,把OnValueChanged()处理程序方法赋予PropertyMetadata对象的PropertyChangedCallback属性。在OnValueChanged()方法中,可以用DependencyPropertyChangedEventArgs()参数访问属性的新旧值。
public int Value { get => (int)GetValue(ValueProperty); set => SetValue(ValueProperty, value); } public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(nameof(Value), typeof(int), typeof(MyDependencyObject), new PropertyMetadata(0,OnValueChanged)); private static void OnValueChanged(DependencyObject obj,DependencyPropertyChangedEventArgs e) { int oldValue = (int)e.OldValue; int newValue = (int)e.NewValue; }
9. 路由事件
使用默认实现的事件,当触发事件时,将调用直接连接到事件的处理程序。使用UI技术时,对事件处理有不同的需求。在一些事件中,应该可以创建一个带有容器控件的处理程序,并对来自子控件的事件做出反应。这可以通过为.NET事件创建自定义实现代码来实现,如add和remove访问器所示。
UWP提供了路由事件。示例应用程序定义的用户界面包含一个复选框,如果选中它,就停止路由;一个按钮控件,其Tapped事件设置为OnTappedButton处理程序方法;一个网格,其Tapped事件设置为OnTappedGrid处理程序。Tapped事件是Universal Windows应用程序的一个路由事件。这个事件可以用鼠标、触摸屏和笔设备触发:
OnTappedXX处理程序方法把状态信息写入一个TextBlock,来显示处理程序方法和事件初始源的控件:
private void OnTappedButton(object sender, TappedRoutedEventArgs e) { ShowStatus(nameof(OnTappedButton), e); e.Handled = CheckStopRouting.IsChecked == true; } private void OnTappedGrid(object sender, TappedRoutedEventArgs e) { ShowStatus(nameof(OnTappedGrid), e); e.Handled = CheckStopRouting.IsChecked == true; } private void OnCleanStatus(object sender, RoutedEventArgs e) { textStatus.Text = string.Empty; } private void ShowStatus(string status, RoutedEventArgs e) { textStatus.Text += $"{status} {e.OriginalSource.GetType().Name}"; textStatus.Text += "\r\n"; }
运行应用程序,在网格内单击按钮的外部,就会看到处理的OnTappedGrid事件,并把Grid(实际运行结果,为TextBlock)控件作为触发事件的源:
单击按钮的中间,会看到事件被路由。第一个调用的处理程序是OnTappedButton,其后是OnTappedGrid:
同样有趣的是,事件源不是按钮,而是TextBlock。原因在于,这个按钮使用TextBlock设置样式,来包含按钮的文本。 如果单击按钮内的其他位置,还可以看到Grid或ContentPresenter是原始事件源。Grid和ContentPresenter是创建按钮的其他控件。
在单击按钮之前,选中复选框CheckStopRouting,可以看到事件不在路由, 因为事件参数的Handled属性被设置为true:
OnTappedButton TextBock
在事件的Microsoft API文档中,可以在文档的备注部分看到事件类型是否路由。在Universal Windows应用程序中,Tapped、Drag和Drop、KeyUp和KeyDown、Pointer、Focus、Manipulation事件是路由事件。
10 附加属性
依赖属性是可用于特性类型的属性。而通过附加属性,可以为其他类型定义属性。一些容器控件为其子控件定义了附加属性;例如,如果使用DockPanel控件,就可以为其子控件使用Dock属性。Grid控件定义了Row和Column属性。
下面的代码片段说明了附加属性在XAML中的情况。Button类没有Grid.Dock属性,但它是从Grid控件附加的。
附加属性的定义与依赖属性非常类似,如下面的示例所示。定义附加属性的类必须派生自基类DependencyObject,并定义一个普通的属性,其中get和set访问器调用基类的GetValue()和SetValue()方法。这些都是类似之处。接着不调用DependencyProperty类的Register()方法,而是调用RegisterAttached()方法。RegisterAttached()方法注册一个附加属性,现在它可用于每个元素。
public class MyAttachedPropertyProvider : DependencyObject { //public string MySample //{ // get => (string)GetValue(MySampleProperty); // set => SetValue(MySampleProperty, value); //} public static readonly DependencyProperty MySampleProperty = DependencyProperty.RegisterAttached("MySample", typeof(string), typeof(MyAttachedPropertyProvider), new PropertyMetadata(string.Empty)); public static void SetMySample(UIElement element, string value) => element.SetValue(MySampleProperty, value); public static string GetMySample(UIElement element) => (string)element.GetValue(MySampleProperty); }
注意:
似乎Grid.Row属性只能添加到Grid控件中的元素。实际上,附加属性可以添加到任何元素上,但无法使用这个属性值。Grid控件能够识别这个属性,并从其子元素中读取它,以安排其子元素。它不从子元素的子元素中读取。
在XAML代码中,附加属性现在可以附加到任何元素上。第二个Button控件button2为自身附加了属性MyAttachedPropertyProvider.MySample,其值指定为42。
在代码隐藏中执行相同的操作时,必须调用MyAttachedPropertyProvider类的静态方法SetMySample()。不能扩展Button类,使其包含某个属性。SetMySample()方法获取一个应该由属性及其值扩展的UIElement实例。在如下的代码片段中,把该属性附加到button2中,将其值设置为 sample value。
public MainPage() { this.InitializeComponent(); MyAttachedPropertyProvider.SetMySample(button1,"sample value"); }
为了读取分配给元素的附加属性,可以使用VisualTreeHelper迭代层次结构中的每个元素,并试图读取其附加属性。VisualTreeHelper用于在运行期间读取元素的可视化树。GetChildrenCount方法返回子元素的数量。为了访问子元素,可以使用GetChild方法,通过第二个参数传递一个元素的索引,该方法返回元素。只有当元素的类型是FrameworkElement(或派生于它),且用Func参数传递的谓词返回true时,该方法的实现代码才返回元素。
private IEnumerable GetChildren(FrameworkElement element, Func pred) { int childrenCount = VisualTreeHelper.GetChildrenCount(element); for (int i = 0; i < childrenCount; i++) { var child = VisualTreeHelper.GetChild(element, i) as FrameworkElement; if (child != null && pred(child)) { yield return child; } } }
GitChildren方法现在在页面的构造函数中用于把带有附加属性的所有元素添加到ListBox控件中:
public MainPage() { this.InitializeComponent(); MyAttachedPropertyProvider.SetMySample(button1, "sample value"); foreach (var item in GetChildren(stackPanel, e => MyAttachedPropertyProvider.GetMySample(e) != string.Empty)) { listBox.Items.Add($"{item.Name}: {MyAttachedPropertyProvider.GetMySample(item)}"); } }
运行应用程序(WPF或UWP应用程序)时,会看到列表框中的两个按钮控件与下述值:
11. 标记扩展
通过标记扩展,可以扩展XAML的元素或特性语法。如果XML特性包含花括号,就表示这是标记扩展的一个符号。特性的标记扩展常常用作简写记号,而不再使用元素。
这种标记扩展的示例是StaticResourceExtension,它可查找资源。下面是带有gradientBrush1键的线性渐变笔刷的资源:
使用StaticResourceExtension,通过特性语法来设置Button的Background属性,就可以引用这个资源。特性语法通过花括号和没有Extension后缀的扩展类名来定义。
Windows应用程序不支持可用于WPF的所有标记扩展,只支持其中的一些StaticResource和ThemeResource,绑定标记扩展Binding和x:Bind在本章的"数据绑定"一节讨论。从Fall Creators Update(Windows 10 构建号 16299)开始,也支持自定义标记扩展的创建,如下所述。
12. 自定义标记扩展
自定义标记扩展允许在XAML代码的花括号中添加自己的特性。可以创建自定义绑定、基于条件的评估或简单的计算器,如下一个示例所示。
Calculator标记扩展允许使用加、减、乘、除操作计算两个值。标记扩展非常简单:类名包含Extension后缀,它派生自基类MarkupExtension,重写了方法ProviderValue。使用ProviderValue,标记扩展返回分配给属性的值或对象(在其中定义的标记) 。返回值的类型由MarkupExtensionReturnType特性定义。下面的代码片段显示了Calculator标记扩展的实现。这个扩展定义了可以设置的三个属性:X、Y的属性,以及应用于X和Y的Operation。Operation用一个枚举定义。在ProviderValue方法的实现中,对X和Y应用一个操作,返回结果:
public enum Operation { Add, Subtract, Multiply, Divide } [MarkupExtensionReturnType(ReturnType = typeof(string))] public class CalculatorExtension : MarkupExtension { public double X { get; set; } public double Y { get; set; } public Operation Operation { get; set; } protected override object ProvideValue() { double result = 0; switch (Operation) { case Operation.Add: result = X + Y; break; case Operation.Subtract: result = X - Y; break; case Operation.Multiply: result = X * Y; break; case Operation.Divide: result = X / Y; break; default: break; } return result.ToString(); } }
现在,Calculator标记扩展可以与XML特性语法一起使用。在这里,初始化标记扩展,以设置属性。返回的字符串应用于TextBlock的Text属性:
使用标记扩展语法,不使用名称Extension。这个后缀会自动应用。当然,如果CalculatorExtension类仅仅用于将它实例化为Text属性的子元素,并设置扩展的属性,这就是不同的:
运行应用程序时,在所使用的两个操作中返回值42。
13. 条件XAML
如果需要支持Windows 10的多个构建版本,但是仍然使用更新的特性,比如自定义标记扩展,就需要编写自适应代码,这样,在旧版本的Windows上调用新API时,不会导致应用程序崩溃。使用自适应代码时,需要在调用API之前验证它是否可用,并为旧版本的Windows提供替代方法。这样的条件代码也可以用XAML实现,只要支持的最小构建版本号是15063。条件XAML不支持旧版本。
条件XAML是通过指定XAML名称空间来实现的,这些名称空间只能根据特定的条件提供。例如,只有给Windows.Foundation.UniversalApiContract传递版本5的IsApiContractPresent返回true,才能使用名称空间别名contract5。否则,XAML编译器会忽略这个名称空间别名,不使用这个别名创建元素或特性。名称空间之后的问号 "?" 定义了别名有效的条件:
xmlns:contract5="http://schemas.microsoft.com/winfx/2006/xaml/presentation?IsApiContractPresent(Windows.Foundation.UniversalApiContract, 5)"
下表中的API可以与条件XAML一起使用。使用IsApiContractPresent方法,可以检查特定版本号的特定API是否可用。如果指定的控件类型可用,则IsTypePresent返回true;如果控件类型的特定属性可用,则IsPropertyPresent返回true。逆方法也用所有这些方法来实现。逆方法返回反布尔值。例如,如果IsApiContractPresent返回true,则方法IsApiContractNotPresent返回false。那么,为什么逆方法是必需的,而不能仅仅使用C#的 ! 操作符?原因是XAML没有这样的操作符。
现在,名称空间别名可以在http://schemas.microsoft.com/winfx/2006/xaml/presentation名称空间中使用元素和特性,例如,TextBlock的Text属性:
使用C#代码,可以使用名称空间Windows.Foundation.Metadata的ApiInformation类中定义的IsApiContractPresent方法来实现类似的功能:
if (ApiInformation.IsApiContractPresent( "Windows.Foundation.UniversalApiContract",5)) { text1.Text = "contract 5 present"; }
在XAML代码中,不能多次设置Text属性,而必须确保该属性只为特定的版本设置了一次。否则,将得到Text属性定义多次的错误。这就是为什么定义了可以用来设置别名名称空间的逆方法。
可以根据可用的协定,设置多个名称空间:
xmlns:contract5="http://schemas.microsoft.com/winfx/2006/xaml/presentation?IsApiContractPresent(Windows.Foundation.UniversalApiContract, 5)" xmlns:notcontract5="http://schemas.microsoft.com/winfx/2006/xaml/presentation?IsApiContractNotPresent(Windows.Foundation.UniversalApiContract, 5)"
现在使用不同的名称空间别名来设置TextBlock的Text属性。使用这些别名定义,可以确保只定义其中一个:
在哪里可以找到API协定的名称?每个Windows Runtime API都记录在https://docs.microsoft.com/uwp/api 中,引入API时,文档列出了API协定和版本号。Windows.Foundation.UniversalApiContract本身就是一个引用其他协定的参考协定。在文件夹%ProgramFiles(x86)\Windows Kits\10\Reference中可以找到所有协定。使用ildasm工具可以读取包含协定信息的.md文件。