第 3 部分: XAML 标记扩展

XAML 标记扩展在 XAML 中构成了一项重要功能,允许将属性设置为从其他源间接引用的对象或值。 XAML 标记扩展对于共享对象和引用整个应用程序中使用的常量尤其重要,但它们在数据绑定中的作用最大。

XAML 标记扩展

一般情况下,使用 XAML 将对象的属性设置为显式值,例如字符串、数字、枚举成员或转换为后台值的字符串。

但有时,属性需要引用在其他位置定义的值,或可能需要在运行时通过代码进行少量处理才能引用的值。 为此,可使用 XAML 标记扩展

这些 XAML 标记扩展不是 XML 的扩展。 XAML 是标准的 XML。 它们会调用“扩展”,因为它们由实现 IMarkupExtension 的类中的代码提供支持。 可以编写自己的自定义标记扩展。

在许多情况下,XAML 标记扩展在 XAML 文件中是即时可识别的,因为它们显示为由大括号分隔的特性设置:{ 和 },但有时标记扩展会以传统元素的形式出现在标记中。

共享资源

某些 XAML 页面包含多个视图,这些视图的属性设置为相同的值。 例如,这些 Button 对象的许多属性设置都相同:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="XamlSamples.SharedResourcesPage"
             Title="Shared Resources Page">

    <StackLayout>
        <Button Text="Do this!"
                HorizontalOptions="Center"
                VerticalOptions="CenterAndExpand"
                BorderWidth="3"
                Rotation="-15"
                TextColor="Red"
                FontSize="24" />

        <Button Text="Do that!"
                HorizontalOptions="Center"
                VerticalOptions="CenterAndExpand"
                BorderWidth="3"
                Rotation="-15"
                TextColor="Red"
                FontSize="24" />

        <Button Text="Do the other thing!"
                HorizontalOptions="Center"
                VerticalOptions="CenterAndExpand"
                BorderWidth="3"
                Rotation="-15"
                TextColor="Red"
                FontSize="24" />

    </StackLayout>
</ContentPage>

如果需要更改其中一个属性,你可能更愿意只更改一次,而不是更改三次。 如果这是代码,你可能会使用常量和静态只读对象来帮助保持此类值一致和便于修改。

在 XAML 中,一种常用的解决方案是将此类值或对象存储在资源字典中。 VisualElement 类定义一个名为 Resources 的属性,其类型为 ResourceDictionary,是一种键为 string 类型、值为 object 类型的字典。 可以将对象放入此字典,然后通过标记引用它们,全部操作都在 XAML 中完成。

若要在页面上使用资源字典,请包含一对 Resources 属性元素标记。 将这些放在页面顶部是最方便的:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="XamlSamples.SharedResourcesPage"
             Title="Shared Resources Page">

    <ContentPage.Resources>

    </ContentPage.Resources>
    ...
</ContentPage>

还需要显式包含 ResourceDictionary 标记:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="XamlSamples.SharedResourcesPage"
             Title="Shared Resources Page">

    <ContentPage.Resources>
        <ResourceDictionary>

        </ResourceDictionary>
    </ContentPage.Resources>
    ...
</ContentPage>

现在,可以将各种类型的对象和值添加到资源字典。 这些类型必须可实例化。 例如,它们不能是抽象类。 这些类型还必须具有公共无参数构造函数。 每个项都需要使用 x:Key 特性指定的字典键。 例如:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="XamlSamples.SharedResourcesPage"
             Title="Shared Resources Page">

    <ContentPage.Resources>
        <ResourceDictionary>
            <LayoutOptions x:Key="horzOptions"
                           Alignment="Center" />

            <LayoutOptions x:Key="vertOptions"
                           Alignment="Center"
                           Expands="True" />
        </ResourceDictionary>
    </ContentPage.Resources>
    ...
</ContentPage>

这两个项是结构类型 LayoutOptions 的值,每个项都有一个唯一键和一个或两个属性集。 在代码和标记中,使用 LayoutOptions 的静态字段更为常见,但在此处设置属性更为方便。

现在,必须将这些按钮的 HorizontalOptionsVerticalOptions 属性设置为这些资源,使用 StaticResource XAML 标记扩展完成:

<Button Text="Do this!"
        HorizontalOptions="{StaticResource horzOptions}"
        VerticalOptions="{StaticResource vertOptions}"
        BorderWidth="3"
        Rotation="-15"
        TextColor="Red"
        FontSize="24" />

StaticResource 标记扩展始终用大括号分隔,并包含字典键。

名称 StaticResource 将其与 DynamicResource 区分开来,Xamarin.Forms 也支持该名称。 DynamicResource 适用于与运行时可能会更改的值关联的字典键,而在构造页面上的元素时,StaticResource 仅会访问字典中的元素一次。

对于 BorderWidth 属性,需要在字典中存储双精度浮点数。 XAML 可轻松为 x:Doublex:Int32 等常见数据类型定义标记:

<ContentPage.Resources>
    <ResourceDictionary>
        <LayoutOptions x:Key="horzOptions"
                       Alignment="Center" />

        <LayoutOptions x:Key="vertOptions"
                       Alignment="Center"
                       Expands="True" />

        <x:Double x:Key="borderWidth">
            3
        </x:Double>
    </ResourceDictionary>
</ContentPage.Resources>

无需将其放在三行上。 此旋转角度的此字典条目仅占用一行:

<ContentPage.Resources>
    <ResourceDictionary>
        <LayoutOptions x:Key="horzOptions"
                       Alignment="Center" />

        <LayoutOptions x:Key="vertOptions"
                       Alignment="Center"
                       Expands="True" />

         <x:Double x:Key="borderWidth">
            3
         </x:Double>

        <x:Double x:Key="rotationAngle">-15</x:Double>
    </ResourceDictionary>
</ContentPage.Resources>

可以使用与 LayoutOptions 值相同的方式引用这两个资源:

<Button Text="Do this!"
        HorizontalOptions="{StaticResource horzOptions}"
        VerticalOptions="{StaticResource vertOptions}"
        BorderWidth="{StaticResource borderWidth}"
        Rotation="{StaticResource rotationAngle}"
        TextColor="Red"
        FontSize="24" />

对于 Color 类型的资源,可以使用与直接分配这些类型的特性时所用相同的字符串表示形式。 创建资源时将调用类型转换器。 下面是 Color 类型的资源:

<Color x:Key="textColor">Red</Color>

通常,程序肌哦将 FontSize 属性设置为 NamedSize 枚举的成员,例如 LargeFontSizeConverter 类会在后台工作,以使用 Device.GetNamedSized 方法将其转换为依赖于平台的值。 但是,定义字号资源时,使用数值更有意义,此处显示为 x:Double 类型:

<x:Double x:Key="fontSize">24</x:Double>

现在,除 Text 之外的所有属性都由资源设置定义:

<Button Text="Do this!"
        HorizontalOptions="{StaticResource horzOptions}"
        VerticalOptions="{StaticResource vertOptions}"
        BorderWidth="{StaticResource borderWidth}"
        Rotation="{StaticResource rotationAngle}"
        TextColor="{StaticResource textColor}"
        FontSize="{StaticResource fontSize}" />

还可以在资源字典中使用 OnPlatform 来定义平台的不同值。 下面介绍了 OnPlatform 对象如何成为不同文本颜色的资源字典的一部分:

<OnPlatform x:Key="textColor"
            x:TypeArguments="Color">
    <On Platform="iOS" Value="Red" />
    <On Platform="Android" Value="Aqua" />
    <On Platform="UWP" Value="#80FF80" />
</OnPlatform>

请注意,OnPlatform 会同时获取 x:Key 特性,因为它是字典中的对象,还有 x:TypeArguments 特性,因为它是泛型类。 初始化对象时,iOSAndroidUWP 特性将转换为 Color 值。

下面是访问六个共享值的三个按钮的最终完整 XAML 文件:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="XamlSamples.SharedResourcesPage"
             Title="Shared Resources Page">

    <ContentPage.Resources>
        <ResourceDictionary>
            <LayoutOptions x:Key="horzOptions"
                           Alignment="Center" />

            <LayoutOptions x:Key="vertOptions"
                           Alignment="Center"
                           Expands="True" />

            <x:Double x:Key="borderWidth">3</x:Double>

            <x:Double x:Key="rotationAngle">-15</x:Double>

            <OnPlatform x:Key="textColor"
                        x:TypeArguments="Color">
                <On Platform="iOS" Value="Red" />
                <On Platform="Android" Value="Aqua" />
                <On Platform="UWP" Value="#80FF80" />
            </OnPlatform>

            <x:Double x:Key="fontSize">24</x:Double>
        </ResourceDictionary>
    </ContentPage.Resources>

    <StackLayout>
        <Button Text="Do this!"
                HorizontalOptions="{StaticResource horzOptions}"
                VerticalOptions="{StaticResource vertOptions}"
                BorderWidth="{StaticResource borderWidth}"
                Rotation="{StaticResource rotationAngle}"
                TextColor="{StaticResource textColor}"
                FontSize="{StaticResource fontSize}" />

        <Button Text="Do that!"
                HorizontalOptions="{StaticResource horzOptions}"
                VerticalOptions="{StaticResource vertOptions}"
                BorderWidth="{StaticResource borderWidth}"
                Rotation="{StaticResource rotationAngle}"
                TextColor="{StaticResource textColor}"
                FontSize="{StaticResource fontSize}" />

        <Button Text="Do the other thing!"
                HorizontalOptions="{StaticResource horzOptions}"
                VerticalOptions="{StaticResource vertOptions}"
                BorderWidth="{StaticResource borderWidth}"
                Rotation="{StaticResource rotationAngle}"
                TextColor="{StaticResource textColor}"
                FontSize="{StaticResource fontSize}" />

    </StackLayout>
</ContentPage>

屏幕截图验证一致的样式设置和依赖于平台的样式:

样式控件

虽然在页面顶部定义 Resources 集合最为常见,但请记住,Resources 属性由 VisualElement 定义,并且可以在页面上的其他元素上具有 Resources 集合。 例如,请尝试添加一个到此示例中的 StackLayout

<StackLayout>
    <StackLayout.Resources>
        <ResourceDictionary>
            <Color x:Key="textColor">Blue</Color>
        </ResourceDictionary>
    </StackLayout.Resources>
    ...
</StackLayout>

你会发现按钮的文本颜色现在为蓝色。 基本上,每当 XAML 分析程序遇到 StaticResource 标记扩展时,它会搜索可视化树并使用它遇到的第一个包含该键的 ResourceDictionary

资源字典中存储的最常见对象类型之一是 Xamarin.FormsStyle,用于定义属性设置的集合。 样式在样式一文中进行了讨论。

有时,不熟悉 XAML 的开发人员想知道他们是否可以将视觉元素(如 LabelButton)放在 ResourceDictionary 中。 虽然这是肯定可能的,但它其实没有太大意义。 ResourceDictionary 的目的是共享对象。 无法共享视觉对象元素。 同一实例不能在单个页面上出现两次。

x:Static 标记扩展

尽管他们的名字相似,但 x:StaticStaticResource 是非常不同的。 StaticResource 从资源字典返回对象,而 x:Static 访问下列其中一项:

  • 公共静态字段
  • 公共静态属性
  • 公共常数字段
  • 枚举成员。

定义资源字典的 XAML 实现支持 StaticResource 标记扩展,而 x:Static 是 XAML 的固有部分,如 x 前缀所示。

下面是一些示例,演示 x:Static 如何显式引用静态字段和枚举成员:

<Label Text="Hello, XAML!"
       VerticalOptions="{x:Static LayoutOptions.Start}"
       HorizontalTextAlignment="{x:Static TextAlignment.Center}"
       TextColor="{x:Static Color.Aqua}" />

到目前为止,这并不令人印象深刻。 但是,x:Static 标记扩展也可以从自己的代码引用静态字段或属性。 例如,下面是一个 AppConstants 类,其中包含一些静态字段,你可能希望在应用程序中的多个页面上使用它们:

using System;
using Xamarin.Forms;

namespace XamlSamples
{
    static class AppConstants
    {
        public static readonly Thickness PagePadding;

        public static readonly Font TitleFont;

        public static readonly Color BackgroundColor = Color.Aqua;

        public static readonly Color ForegroundColor = Color.Brown;

        static AppConstants()
        {
            switch (Device.RuntimePlatform)
            {
                case Device.iOS:
                    PagePadding = new Thickness(5, 20, 5, 0);
                    TitleFont = Font.SystemFontOfSize(35, FontAttributes.Bold);
                    break;

                case Device.Android:
                    PagePadding = new Thickness(5, 0, 5, 0);
                    TitleFont = Font.SystemFontOfSize(40, FontAttributes.Bold);
                    break;

                case Device.UWP:
                    PagePadding = new Thickness(5, 0, 5, 0);
                    TitleFont = Font.SystemFontOfSize(50, FontAttributes.Bold);
                    break;
            }
        }
    }
}

若要在 XAML 文件中引用此类的静态字段,则需要一些方法来指示此文件所在的 XAML 文件中的位置。 需要使用 XML 命名空间声明执行此操作。

回想一下,作为标准 Xamarin.Forms XAML 模板的一部分创建的 XAML 文件包含两个 XML 命名空间声明:一个用于访问 Xamarin.Forms 类,另一个用于引用 XAML 内部的标记和特性:

xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"

需要其他 XML 命名空间声明才能访问其他类。 每个附加的 XML 命名空间声明定义一个新前缀。 若要访问共享应用程序 .NET Standard 库(如 AppConstants)的本地类,XAML 程序员通常会使用前缀 local。 命名空间声明必须指示 CLR(公共语言运行时)命名空间名称,也称为 .NET 命名空间名称,该名称显示在 C# namespace 定义或 using 指令中:

xmlns:local="clr-namespace:XamlSamples"

还可以在 .NET Standard 库引用的任何程序集中为 .NET 命名空间定义 XML 命名空间声明。 例如,下面是标准 .NET System 命名空间的 sys 前缀,该命名空间位于 netstandard 程序集中。 由于这是另一个程序集,因此还必须指定程序集名称,在本例中为 netstandard

xmlns:sys="clr-namespace:System;assembly=netstandard"

请注意,关键字 clr-namespace 后跟冒号,然后是 .NET 命名空间名称,后跟分号、关键字 assembly、等号和程序集名称。

是的,冒号在 clr-namespace 后面,但等号在 assembly 后面。 语法是故意以这种方式定义的:大多数 XML 命名空间声明引用一个以 URI 方案名称(如 http)开头的 URI,而 URI 方案名称后面总是跟着一个冒号。 此字符串的 clr-namespace 部分旨在模拟该约定。

这两个命名空间声明都包含在 StaticConstantsPage 示例中。 请注意,BoxView 维度设置为 Math.PIMath.E,但按比例缩放为 100:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:XamlSamples"
             xmlns:sys="clr-namespace:System;assembly=netstandard"
             x:Class="XamlSamples.StaticConstantsPage"
             Title="Static Constants Page"
             Padding="{x:Static local:AppConstants.PagePadding}">

    <StackLayout>
       <Label Text="Hello, XAML!"
              TextColor="{x:Static local:AppConstants.BackgroundColor}"
              BackgroundColor="{x:Static local:AppConstants.ForegroundColor}"
              Font="{x:Static local:AppConstants.TitleFont}"
              HorizontalOptions="Center" />

      <BoxView WidthRequest="{x:Static sys:Math.PI}"
               HeightRequest="{x:Static sys:Math.E}"
               Color="{x:Static local:AppConstants.ForegroundColor}"
               HorizontalOptions="Center"
               VerticalOptions="CenterAndExpand"
               Scale="100" />
    </StackLayout>
</ContentPage>

相对于屏幕的结果 BoxView 的大小取决于平台:

使用 x:Static 标记扩展的控件

其他标准标记扩展

有几种标记扩展是 XAML 固有的,并在 Xamarin.Forms XAML 文件中受支持。 其中有些并不常用,但在需要时却是必不可少的:

  • 如果属性默认具有非 null 值,但你想要将其设置为 null,则可将其设置为 {x:Null} 标记扩展。
  • 如果属性为 Type 类型,可使用标记扩展 {x:Type someClass} 将其分配给 Type 对象。
  • 可使用 x:Array 标记扩展在 XAML 中定义数组。 该标记扩展具有一个名为 Type 的必需特性,用于指示数组中的元素类型。
  • 第 4 部分将讨论 Binding 标记扩展。数据绑定基础知识
  • 相对绑定中讨论了 RelativeSource 标记扩展。

ConstraintExpression 标记扩展

标记扩展可以具有属性,但却不像 XML 特性那样设置。 在标记扩展中,属性在设置时用逗号分隔,大括号内不用引号。

这可以通过名为 ConstraintExpression 的 Xamarin.Forms 标记扩展进行说明,该扩展与 RelativeLayout 类一起使用。 可以将子视图的位置或大小指定为常量,或者相对于父视图或其他命名视图。 ConstraintExpression 的语法允许使用 Factor 乘以另一个视图的属性,再加上 Constant 来设置视图的位置或大小。 任何与代码相比更复杂的内容。

下面是一个示例:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="XamlSamples.RelativeLayoutPage"
             Title="RelativeLayout Page">

    <RelativeLayout>

        <!-- Upper left -->
        <BoxView Color="Red"
                 RelativeLayout.XConstraint=
                     "{ConstraintExpression Type=Constant,
                                            Constant=0}"
                 RelativeLayout.YConstraint=
                     "{ConstraintExpression Type=Constant,
                                            Constant=0}" />
        <!-- Upper right -->
        <BoxView Color="Green"
                 RelativeLayout.XConstraint=
                     "{ConstraintExpression Type=RelativeToParent,
                                            Property=Width,
                                            Factor=1,
                                            Constant=-40}"
                 RelativeLayout.YConstraint=
                     "{ConstraintExpression Type=Constant,
                                            Constant=0}" />
        <!-- Lower left -->
        <BoxView Color="Blue"
                 RelativeLayout.XConstraint=
                     "{ConstraintExpression Type=Constant,
                                            Constant=0}"
                 RelativeLayout.YConstraint=
                     "{ConstraintExpression Type=RelativeToParent,
                                            Property=Height,
                                            Factor=1,
                                            Constant=-40}" />
        <!-- Lower right -->
        <BoxView Color="Yellow"
                 RelativeLayout.XConstraint=
                     "{ConstraintExpression Type=RelativeToParent,
                                            Property=Width,
                                            Factor=1,
                                            Constant=-40}"
                 RelativeLayout.YConstraint=
                     "{ConstraintExpression Type=RelativeToParent,
                                            Property=Height,
                                            Factor=1,
                                            Constant=-40}" />

        <!-- Centered and 1/3 width and height of parent -->
        <BoxView x:Name="oneThird"
                 Color="Red"
                 RelativeLayout.XConstraint=
                     "{ConstraintExpression Type=RelativeToParent,
                                            Property=Width,
                                            Factor=0.33}"
                 RelativeLayout.YConstraint=
                     "{ConstraintExpression Type=RelativeToParent,
                                            Property=Height,
                                            Factor=0.33}"
                 RelativeLayout.WidthConstraint=
                     "{ConstraintExpression Type=RelativeToParent,
                                            Property=Width,
                                            Factor=0.33}"
                 RelativeLayout.HeightConstraint=
                     "{ConstraintExpression Type=RelativeToParent,
                                            Property=Height,
                                            Factor=0.33}"  />

        <!-- 1/3 width and height of previous -->
        <BoxView Color="Blue"
                 RelativeLayout.XConstraint=
                     "{ConstraintExpression Type=RelativeToView,
                                            ElementName=oneThird,
                                            Property=X}"
                 RelativeLayout.YConstraint=
                     "{ConstraintExpression Type=RelativeToView,
                                            ElementName=oneThird,
                                            Property=Y}"
                 RelativeLayout.WidthConstraint=
                     "{ConstraintExpression Type=RelativeToView,
                                            ElementName=oneThird,
                                            Property=Width,
                                            Factor=0.33}"
                 RelativeLayout.HeightConstraint=
                     "{ConstraintExpression Type=RelativeToView,
                                            ElementName=oneThird,
                                            Property=Height,
                                            Factor=0.33}"  />
    </RelativeLayout>
</ContentPage>

也许你应该从此示例中吸取的最重要教训是标记扩展的语法:标记扩展的大括号内不得出现引号。 在 XAML 文件中键入标记扩展时,自然会想要将属性的值括在引号中。 抵制诱惑!

下面是正在运行的程序:

使用约束的相对布局

总结

此处显示的 XAML 标记扩展为 XAML 文件提供重要支持。 但也许最有价值的 XAML 标记扩展是 Binding,本系列下一部分 第 4 部分对此进行了介绍。数据绑定基础知识