使用 XAML 在 .NET MAUI 应用中创建 UI

内容纲要

介绍

.NET MAUI 使您能够使用 C# 代码动态创建应用的用户界面。但是,有时静态定义 UI 更合适、更有效。可扩展应用程序标记语言 (XAML) 提供了一种在编译时对 UI 进行布局的方法。UI 的 XAML 说明还提供了一定程度的文档,使你能够快速掌握向用户呈现 UI 的方式,而无需深入研究应用程序代码。

假设您作为移动开发人员在一家电力公司工作。您负责改进工程师在访问客户场所时使用的公司移动应用程序。目前,应用的 UI 是使用 C# 代码生成的。但是,你已开始注意到管理 UI 更新变得越来越困难。这种困难是由于应用程序变得越来越复杂。理解核心行为逻辑变得越来越困难,因为它与UI代码混合在一起。

你希望找到一个将 UI 和行为完全分离的解决方案。UI和行为的分离将使您的设计专家专注于他们最擅长的事情,并且您将有时间专注于编写应用程序的行为。NET MAUI 允许您使用 XAML 定义 UI。XAML 使用户界面 (UI) 和行为完全分离。XAML 还使使用设计专家和设计工具变得更加容易。在本模块中,你将了解如何创建一个 .NET MAUI 应用,该应用使用 XAML 而不是 C# 代码定义其页面和控件。通过在 XAML 上创建 UI,可以将所有 UI 代码与行为代码分开,以便更轻松地管理这两者。

学习目标

在本模块中,您将学习:

  • 与在 C 语言中为 .NET MAUI 应用定义 UI 相比,使用 XAML 的好处#
  • 如何使用 XAML 创建页和控件,并设置其属性
  • 如何处理 UI 事件并在 XAML 中连接它们
  • 如何创建和使用 XAML 标记扩展
  • 如何在 XAML 标记中设置特定于平台的值

先决条件

  • 安装了 .NET MAUI 工作负载的 Visual Studio 2022
  • 熟悉 C# 和 .NET

使用 XAML 的好处

XAML 是一种标记语言,可用于生成 UI 而不是 C# 代码。使用 XAML,可以拆分 UI 和行为代码,使两者更易于管理。

在本单元中,你将把使用 XAML 与使用 C# 代码定义 UI 布局进行比较。你将了解使用 XAML 作为标记语言来定义 UI 的一些好处。

什么是标记语言?

标记语言是一种计算机语言,可用于在文档中引入各种元素。您可以使用预定义的标签来描述元素。标记在使用文档的域的上下文中具有特定含义。

例如,可以使用超文本标记语言 (HTML) 创建可在 Web 浏览器中显示的网页。您无需了解以下示例中使用的所有标记。重要的是要看到,此代码描述了一个包含文本“Hello World!”作为其内容的文档。

<!DOCTYPE html>
<html>
    <body>
        <p>Hello <b>World</b>!</p>
    </body>
</html>

您可能已经使用过标记语言。您可能已使用 HTML 创建了网页,或者可能已修改 Visual Studio project.csproj 文件中的可扩展标记语言 (XML) 定义。此文件由 Microsoft 生成工具进行分析和处理。

包含标记语言的文件通常由其他软件工具进行处理和解释。标记的这种解释性正是 XAML 的工作方式。但是,解释它的软件工具有助于生成应用 UI。

什么是 XAML?

XAML 是由 Microsoft 创建的声明性标记语言。XAML 旨在简化在应用程序中创建 UI 的过程。

您创建的 XAML 文档包含以声明方式描述应用程序 UI 元素的元素。请记住,XAML 中的这些元素直接表示对象的实例化。在 XAML 中定义元素后,可以在代码隐藏文件中访问该元素,并使用 C# 代码定义行为。

.NET MAUI XAML 和 Microsoft XAML 之间的区别

XAML 基于 Microsoft 2009 XAML 规范。但是,该规范仅定义了语言的语法。与 Windows Presentation Foundation (WPF)、Universal Windows Platform (UWP) 和 WinUI 3 一样,它们都使用 XAML,你在 XAML 中声明的元素将发生更改。

XAML 首次出现在 2006 年,与 WPF 一起出现。如果你已经使用 Microsoft XAML 一段时间了,则 XAML 语法应该看起来很熟悉。

XAML 的 .NET MAUI 风格与其他 UI 工具使用的 XAML 之间存在一些关键区别。结构和概念是相似的。但是,类和属性的某些名称是不同的。

使用 .NET MAUI XAML 创建 UI

查看 XAML 的实际效果的最佳方法是查看现有 C# 编码的 ContentPage 页类型的示例。然后,可以将其与具有使用 XAML 定义的相同 UI 的另一个页面进行比较。

假设你的应用中已对以下代码进行了编码。ContentPage

namespace MauiCode;

public partial class MainPage : ContentPage
{
    Button loginButton;
    VerticalStackLayout layout;

    public MainPage()
    {
        this.BackgroundColor = Color.FromArgb("512bdf");

        layout = new VerticalStackLayout
        {
            Margin = new Thickness(15, 15, 15, 15),
            Padding = new Thickness(30, 60, 30, 30),
            Children =
            {
                new Label { Text = "Please log in", FontSize = 30, TextColor = Color.FromRgb(255, 255, 100) },
                new Label { Text = "Username", TextColor = Color.FromRgb(255, 255, 255) },
                new Entry (),
                new Label { Text = "Password", TextColor = Color.FromRgb(255, 255, 255) },
                new Entry { IsPassword = true }
            }
        };

        loginButton = new Button { Text = "Login", BackgroundColor = Color.FromRgb(0, 148, 255) };
        layout.Children.Add(loginButton);

        Content = layout;

        loginButton.Clicked += (sender, e) =>
        {
            Debug.WriteLine("Clicked !");
        };
    }
}

该页面包含一个布局容器、两个标签、两个条目和一个按钮。该代码还处理按钮的 Clicked 事件。页面中的元素也只设置了几个设计属性。在运行时,在 Android 设备上,页面如下所示:

尽管该页面具有简单的设计,但它是同一文件中行为和设计的混合体。

使用 XAML 定义的相同页面布局如下所示:

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MauiXaml.Page1"
             BackgroundColor="#512bdf">

    <VerticalStackLayout Margin="15" Padding="30, 60, 30, 30">
        <Label Text="Please log in" FontSize="30" TextColor="AntiqueWhite"/>
        <Label Text="Username" TextColor="White" />
        <Entry />
        <Label Text="Password" TextColor="White" />
        <Entry IsPassword="True" />
        <Button Text="Log in" BackgroundColor="#0094FF" Clicked="LoginButton_Clicked" />
    </StackLayout>
</ContentPage>

初始化页并为代码隐藏文件中控件的事件实现事件处理程序的 C# 代码如下所示:ClickedLoginButton

namespace MauiXaml;

public partial class Page1 : ContentPage, IPage
{
    public Page1()
    {
        InitializeComponent();
    }

    void LoginButton_Clicked(object sender, EventArgs e)
    {
        Debug.WriteLine("Clicked !");
    }
}

备注

页构造函数中的方法读取页的 XAML 说明,加载该页上的各种控件,并设置其属性。仅当使用 XAML 标记定义页时,才调用此方法。前面的示例演示如何使用 C# 代码创建 UI,但未调用 。InitializeComponentInitializeComponent

这种结构允许设计和行为的分离。UI 的整个声明都包含在单个专用源文件中。它与 UI 行为是分开的。此外,XAML 标记还为尝试了解应用程序外观的开发人员提供了更清晰的信息。

使用 XAML 的好处

使用 XAML 可以将行为逻辑与 UI 设计分开。这种分离有助于你独立构建每个部分,并使整个应用在增长时更易于管理。

此方法还使专业 UI 设计人员能够使用 XAML 编辑工具与更新 UI 逻辑的开发人员分开更新 UI 的外观。

.NET MAUI XAML 中的类型和属性

XAML 是一种声明性标记语言。它的设计理念是简化创建 UI 的过程。XAML 中的元素直接表示在代码隐藏文件中访问的对象的实例化。

在本单元中,你将了解如何使用 XAML 中可用的类型,以及如何设置和读取这些类型的属性。

类型定义在哪里?

.NET MAUI 实现一个 XAML 分析器,该分析器分析已声明的 XAML 元素,并将每个元素实例化为 .NET 类型。.NET MAUI 分析器理解的 XAML 方言特定于 .NET MAUI,尽管它类似于 Windows Presentation Foundation 等其他框架使用的 XAML。

实现由 XAML 代码标识的项的 .NET 类型由多个 .NET 程序集中的代码实现。其中许多程序集都作为 .NET MAUI 模板的一部分包含在内。还可以通过将相应的程序集作为项目的一部分加载来利用其他自定义类型。许多程序集都可用作 NuGet 包。MAUI 应用程序使用的大多数常见类型都在 Microsoft.Maui.Dependencies 和 Microsoft.Maui.Extensions 包中。

每种类型的类型都在命名空间中定义。在 XAML 代码中,指定引用的类型的命名空间。大多数 MAUI 控件都位于 Microsoft.Maui.Controls 命名空间中,而 Microsoft.Maui 命名空间定义实用程序类型,如 ,而 Microsoft.Maui.Graphics 命名空间包括通用类型,如 .以这种方式引入类型的选项突出显示了 XAML 的可扩展性。XAML 允许你创建应用的 UI,并可以自由地包含 .NET MAUI XAML 元素、.NET 类型和自定义类型。在大多数情况下,你无需担心这些命名空间,因为它们是使用 C# 的隐式 usings 功能引入的,该功能会自动将它们添加到应用范围。ThicknessColor

如何在 XAML 中实例化类型

使用 XAML 生成 UI 的第一步是实例化 UI 控件类型。在 XAML 中,可以使用对象元素语法创建指定类型的对象。对象元素语法是用于声明元素的标准、格式正确的 XML 语法。例如,如果要创建具有特定颜色的标签,则 XAML 元素将类似于以下代码:

<Label TextColor="AntiqueWhite"/>

此 XAML 元素将由 .NET MAUI XAML 分析器分析,以实例化内存中的对象。实际上,分析的 XAML 标签与以下 C# 代码相同:

var myLabel = new Label
{
  TextColor = Color.FromRgb(255, 255, 100)
};

什么是 XAML 命名空间?

请记住,要使 XAML 分析器成功分析页中控件的 XAML 定义,它必须有权访问实现该控件并定义其属性的代码。可用于 .NET MAUI 页的控件是在作为 Microsoft.Maui NuGet 包的一部分安装的程序集集合中实现的。这些控件位于这些程序集的 .NET 命名空间中。在 C# 代码中,使用指令将命名空间引入作用域。在 XAML 页中,使用页的属性引用命名空间。下面的代码显示了在上一单元中创建的 XAML 页所使用的命名空间:usingxmlns

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             ...>

    ...
</ContentPage>

第一个命名空间是页面的默认命名空间。命名空间的这种 URI 形式是典型的 XML 形式,看起来与 C# 中可能熟悉的名称有所不同。但是,此 URI 只是 Microsoft.Maui NuGet 包中的程序集定义的一个或多个命名空间的别名,因此在页面开头指定此命名空间会将所有 .NET MAUI 类型和控件纳入范围。如果省略此命名空间,将无法使用 、 、 或 等控件。http://schemas.microsoft.com/dotnet/2021/mauiButtonLabelEntryStackLayout

第二个命名空间 引用包含各种 .NET 内部类型(如字符串、数值和属性)的程序集。在上面的 XAML 代码中,为此命名空间分配了别名 x。在此页的 XAML 代码中,可以通过在此命名空间中以 x: 作为前缀来引用这些类型。例如,每个 XAML 页都编译为一个类,并指定使用该页的 x:Class 属性生成的类的名称:http://schemas.microsoft.com/winfx/2009/xaml

<ContentPage ...
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MauiXaml.Page1"
            ...>

    ...
</ContentPage>

可以通过 XAML 命名空间在 XAML 代码中引用自己的程序集中的类型。例如,如果要在项目中名为 Utils 的命名空间中定义 XAML 代码中使用的类型和方法,则可以将 Utils 命名空间添加到页面中,如下所示。在此示例中,您可以通过在此命名空间中使用别名 mycode 作为前缀来访问这些类型。

<ContentPage ...
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:mycode="clr-namespace:Utils"
            ...>

    ...
</ContentPage>

备注

您将在本模块的后面部分看到此技术的更多示例。

如何在 XAML 中指定属性值

在 XML 中,可以使用属性来描述或提供有关元素的信息。在 XAML 中,使用特性来设置基础类型的属性。例如,请考虑以下 C# 代码:

var label = new Label { Text = "Username", TextColor = Color.Black };

此语句创建一个新对象并设置 and 属性。若要在 XAML 中设置属性,请使用特性。相应的 XAML 代码如下所示:LabelTextTextColor

<Label Text="Username" TextColor="Black" />

您可能会注意到,XAML 代码中与 C# 代码不同的一件事是属性的值。例如,在 C# 代码中,使用属性的类型。但是,在 XAML 定义中,使用字符串值进行设置。这是因为使用字符串是可用于 XML 属性值的唯一有效操作。因此,需要有一种方法将每个字符串值转换为其正确的类型。在 XAML 中,通过使用类型转换器进行此转换。ColorTextColorTextColor

什么是类型转换器?

类型转换器用于将指定为字符串值的 XML 特性转换为其正确的类型。为了更好地理解这个概念,请考虑以下示例:

<Label Text="Username" TextColor="Black" FontSize="42" FontAttributes="Bold,Italic" />

此代码创建 并设置其 、 、 和属性。LabelTextTextColorFontSizeFontAttributes

从第一个属性开始。文本已经是一个字符串,这意味着 XAML 页不需要类型转换器。接下来,使用该类型,因此 XAML 需要类型转换器将字符串转换为相应的值。该属性是一个整数,因此 XAML 需要类型转换器将字符串分析为整数。最后,是复杂对象的一个示例。您可以将这些值组合为逗号分隔的字符串“粗体,斜体”。逗号分隔的字符串被视为基于 [Flags] 的枚举,相应的类型转换器会将值的按位应用于属性。TextTextColorColorColorFontSizeFontAttributesOR

.NET MAUI 具有适用于大多数内置类的类型转换器,它将自动使用这些类型转换器。但是,如果不存在特定的转换器,则可以编写自己的转换器并将其与类型相关联,以使其在 XAML 中可用。

复杂类型赋值

类型转换器非常适合简单的属性设置;但是,在某些情况下,您需要创建具有自己的属性值的完整对象。此问题的解决方案是将属性赋值更改为使用基于元素的语法。此语法称为属性元素窗体。此语法涉及将属性 setter 分解为父子形式,其中属性以 Type.PropertyName 形式的元素标记表示。假设你想要将手势识别器分配给标签,以便应用用户可以点击该标签。手势识别器是具有自己的属性的复杂对象。通常,需要分配这些属性以确保功能正常:

<TapGestureRecognizer NumberOfTapsRequired="2" />

如果需要将此值分配给 ,可以按如下方式编写 XAML:Label

<Label Text="Username" TextColor="Black" FontSize="42" FontAttributes="Bold,Italic">
    <Label.GestureRecognizers>
        <TapGestureRecognizer NumberOfTapsRequired="2" />
    </Label.GestureRecognizers>
</Label>

该类型具有一个名为 的属性。通过使用“属性元素”窗体,可以将 添加到 的手势列表中。LabelGestureRecognizersTapGestureRecognizerLabel

默认内容属性

某些 .NET MAUI 控件具有默认内容属性。content 属性允许您指定控件上属性的值,而无需在 XAML 中显式声明它。请看以下 XAML 片段:

<VerticalStackLayout>
    <VerticalStackLayout.Children>
        <Label Text="Please log in" />
    </VerticalStackLayout.Children>
</VerticalStackLayout>

此代码创建一个并添加一个作为子元素。由于通常将子级添加到 中,因此其属性是默认内容属性。这意味着您可以添加子项,而无需显式指定 ,如下所示:VerticalStackLayoutLabelVerticalStackLayoutChildrenChildren

<VerticalStackLayout>
    <Label Text="Please log in" />
</VerticalStackLayout>

XAML 中的事件处理

创建 XAML UI 后,可以添加代码来响应用户访问页面时发生的交互。.NET MAUI 通过标准 .NET 事件通知用户输入和交互的应用。

在本单元中,您将学习如何处理这些事件并执行用户预期的操作。

命名 XAML 页中的元素

事件处理代码经常需要引用页面上的特定控件及其属性。可以为每个控件分配唯一的名称。为此,请使用 XAML 属性 。该属性执行两项操作:x:Namex:Name

  • 它将一个私有字段添加到映射到此元素的生成的代码隐藏文件中。在代码中使用此字段可与可视元素进行交互,以设置运行时属性和处理事件。
  • XAML 通过此名称知道该元素。可以从同一 XAML 文件中定义的其他元素中引用这些元素。

命名元素时,不能使用任何任意字符串。分配给该属性的值用于在代码中创建字段。相反,它必须符合变量的命名约定。该名称还必须是唯一的,因为它已编译到代码隐藏定义中。x:Name

提供元素的名称后,可以在代码隐藏文件中与该元素进行交互。以下 XAML 片段定义了一个控件。它被命名为 CounterLabel(此示例取自 .NET MAUI 模板生成的默认应用):Label

    <Label Text="Current count: 0"
        ...
        x:Name="CounterLabel"
        ... />

在此页的代码隐藏中,可以通过字段引用此控件,并修改其属性:CounterLabel

count++;
CounterLabel.Text = $"Current count: {count}";

重要

在运行页面的方法之前,不会初始化该字段。此方法是 XAML 分析和对象实例化过程的一部分。在此调用之后,放置与 XAML 中定义的元素交互的任何代码。此规则的例外是类本身。在执行方法之前,可以访问类上的所有属性。但是,如果在 XAML 中对此类设置了任何属性,则这些属性值将覆盖在执行 之前可能已设置的任何值。InitializeComponentContentPageInitializeComponentInitializeComponent

使用属性连接事件

许多控件公开与这些控件可以响应的事件对应的属性,例如按钮的事件。不同的控件支持不同的事件集。例如,控件可以响应 、 和事件,而控件具有诸如 .可以在页的 XAML 标记中初始化事件属性,并指定触发事件时要运行的方法的名称。事件方法必须满足以下签名要求:ClickedButtonClickedPressedReleasedEntryTextChanged

  • 它不能返回值;该方法必须是 。void
  • 它必须采用两个参数;指示触发事件的对象(称为发送方)的引用,以及包含发送方传递给事件处理程序的任何参数的参数。objectEventArgs
  • 事件处理程序应为 。这不是强制执行的,但是如果将事件处理程序设置为公共事件处理程序,则外部世界可以访问它,并且可以由正在触发的预期事件以外的操作调用。private
  • 如果事件处理程序需要运行异步操作,则可以使用。async

下面的示例演示了来自 .NET MAUI 模板的示例应用中按钮的事件处理程序的定义。方法的名称遵循标准约定;跟控件的名称(按钮名为 Counter)和事件的名称(单击)。此约定不强制执行,但很好的做法:Clicked

private void OnCounterClicked(object sender, EventArgs e)
{
    ...
}

关注点分离

在 XAML 中连接事件很方便,但它将控件的行为与 UI 定义混合在一起。许多开发人员更喜欢保持这些内容不同,并对命名元素执行代码后面的所有事件处理程序订阅。更容易看到连接的内容以及行为映射到的位置。它还使得在没有意识到的情况下删除 XAML 中的处理程序来意外破坏代码变得更加困难。已删除的处理程序不会被编译器捕获,并且仅当代码未正确执行该行为时才会将自身显示为问题。

是选择使用 XAML 还是使用代码来连接事件处理程序,这是个人选择的问题。

若要在代码中连接事件处理程序,请使用运算符订阅事件。通常在调用之后,在页的构造函数中执行此操作:+=InitializeComponent

public partial class MainPage : ContentPage, IPage
{
    public MainPage()
    {
        InitializeComponent();
        Counter.Clicked += OnCounterClicked;
    }

    ...

    private void OnCounterClicked(object sender, EventArgs e)
    {
        ...
    }
}

备注

可以使用此方法为同一事件订阅多个事件处理方法。每个事件处理方法都将在事件发生时运行,尽管您不应假设它们将以任何特定顺序执行,因此不要在它们之间引入任何依赖关系。

同样,您可以通过在应用程序后面的应用程序中使用运算符从事件中取消订阅事件处理程序来删除事件处理程序:-=

Counter.Clicked -= OnCounterClicked;

练习:创建第一个 XAML 页面

电力公司工程师定期拜访客户,以维修电器并执行其他电气维护任务。该应用程序的一部分使工程师能够记录有关访问的信息。它显示一个简单的编辑器,工程师可以在其中输入详细信息并保存它们。

在 Android 上,应用如下所示:

系统要求您向此页面添加一些其他功能。在开始之前,您需要了解页面的创建方式,以便查看源代码。您注意到 UI 已完全使用 C# 代码创建。虽然此方法有效,但它将处理布局的代码与控制 UI 工作方式的代码混合在一起。您意识到,不久之后,应用程序两个方面就有可能被锁定在一起,这使得未来的维护变得困难,并且随着添加更多功能,可能会使应用程序变得更加脆弱。你决定通过从应用中提取定义布局的 C# 代码并将其替换为 XAML 页,将 UI 设计与 UI 逻辑分开。

查看现有应用

在计算机上本地克隆此练习的 GitHub 存储库 https://github.com/microsoftdocs/mslearn-dotnetmaui-create-user-interface-xaml

移动到存储库的本地副本中的 exercise1 文件夹。

在此文件夹中打开 Notes.sln Visual Studio 解决方案文件。

“解决方案资源管理器”窗口中,展开“注释”项目,然后打开 MainPage.xaml.cs 文件。

查看此文件中定义的 MainPage 类。构造函数包含以下用于创建 UI 的代码:

public MainPage()
{
    var notesHeading = new Label() { Text = "Notes", HorizontalOptions = LayoutOptions.Center, FontAttributes = FontAttributes.Bold };

    editor = new Editor() { Placeholder = "Enter your note", HeightRequest = 100 };
    editor.Text = File.Exists(_fileName) ? File.ReadAllText(_fileName) : string.Empty;

    var buttonsGrid = new Grid() { HeightRequest = 40.0 };
    buttonsGrid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(1.0, GridUnitType.Auto) });
    buttonsGrid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(30.0, GridUnitType.Absolute) });
    buttonsGrid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(1.0, GridUnitType.Auto) });

    var saveButton = new Button() { WidthRequest = 100, Text = "Save" };
    saveButton.Clicked += OnSaveButtonClicked;
    Grid.SetColumn(saveButton, 0);
    buttonsGrid.Children.Add(saveButton);

    var deleteButton = new Button() { WidthRequest = 100, Text = "Delete" };
    deleteButton.Clicked += OnDeleteButtonClicked;
    Grid.SetColumn(deleteButton, 2);
    buttonsGrid.Children.Add(deleteButton);

    var stackLayout = new VerticalStackLayout 
    { 
        Padding = new Thickness(30, 60, 30, 30),
        Children = { notesHeading, editor, buttonsGrid }
    };

    this.Content = stackLayout;
}

UI 包括一个包含 、 和 一个包含三列的 。第一列包含 saveButton 控件,第二列包含间隔符,第三列包含 deleteButton 控件。VerticalStackLayoutLabelEditorGrid

下图说明了 UI 的结构:

请注意,MainPage 类还包含按钮的事件处理方法,以及一些初始化控件的代码。此代码与 UI 定义没有区别。Editor

在 Windows 上构建并运行应用,只是为了查看它的外观。完成后返回到Visual Studio。

创建 UI 的 XAML 版本

打开 MainPage.xaml 文件。此页面中的标记表示一个空的 MAUI 内容页面:

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="Notes.MainPage">

</ContentPage>

向内容页添加控件:VerticalStackLayout

<ContentPage ...>
    <VerticalStackLayout Margin="30,60,30,30">

    </VerticalStackLayout>
</ContentPage>

将控件添加到 .设置此控件的“文本”、“水平文本对齐”和“字体属性”属性,如下所示:LabelVerticalStackLayout

<ContentPage ...>
    <VerticalStackLayout ...>
        <Label Text="Notes"
               HorizontalOptions="Center"
               FontAttributes="Bold" />
    </VerticalStackLayout>
</ContentPage>

将控件添加到 :EditorVerticalStackLayout

<ContentPage ...>
    <VerticalStackLayout ...>
        <Label .../>

        <Editor x:Name="editor"
                Placeholder="Enter your note"
                HeightRequest="100" />
    </VerticalStackLayout>
</ContentPage>

将子级添加到 .这应该有三列;第一个和第三个会自动调整大小,而第二个宽度为 30:GridVerticalStackLayoutGrid

<ContentPage ...>
    <VerticalStackLayout ...>
        <Label .../>

        <Editor .../>

        <Grid ColumnDefinitions="Auto, 30, Auto">

        </Grid>
    </VerticalStackLayout>
</ContentPage>

将 a 添加到子项 的第一列。这是保存按钮:ButtonGrid

<ContentPage ...>
    <VerticalStackLayout ...>
        <Label .../>

        <Editor .../>

        <Grid ...>                    
            <Button Grid.Column="0"
                    Text="Save" 
                    WidthRequest="100"
                    Clicked="OnSaveButtonClicked" />
        </Grid>
    </VerticalStackLayout>
</ContentPage>

将另一个添加到子项 的第三列。这是“删除”按钮:ButtonGrid

<ContentPage ...>
    <VerticalStackLayout ...>
        <Label .../>

        <Editor .../>

        <Grid ...>                    
            <Button ... />

            <Button Grid.Column="2"
                    Text="Delete" 
                     WidthRequest="100"
                    Clicked="OnDeleteButtonClicked" />
        </Grid>
    </VerticalStackLayout>
</ContentPage>

从代码隐藏文件中删除布局代码

“解决方案资源管理器”窗口中,展开 MainPage.xaml 节点,然后打开 MainPage.xaml.cs 文件。

从 MainPage 类中删除编辑器字段。

在 MainPage.xaml.cs 文件中的 MainPage 构造函数中,删除创建用户界面元素的所有代码,并将其替换为对 InitializeComponent 方法的调用。添加代码,用于检查用于存储注释的文件是否存在,如果存在,则读取内容并填充 Editor 控件的“文本”字段。构造函数应如下所示:

public partial class MainPage : ContentPage
{
    string _fileName = Path.Combine(FileSystem.AppDataDirectory, "notes.txt");

    public MainPage()
    {
        InitializeComponent();

        if (File.Exists(_fileName))
        {
            editor.Text = File.ReadAllText(_fileName);
        }
    }

    ...
}

在“生成”菜单上,选择“重新生成解决方案”。验证应用生成时是否没有任何错误。

运行应用程序。它应该像以前一样工作。

如果您有时间,请使用 Android 模拟器部署并运行应用。应用 UI 应类似于本练习开始时的图像中显示的 UI。

XAML 标记扩展

大部分 XAML 定义将在编译时结算。您通常知道元素应放置在何处,将使用哪些颜色和字体,以及应将哪些文本值分配给属性。

但是,有时需要将属性值设置为无法在编译时确定的值。这些值仅在程序运行时才知道。在这些情况下,可以创建一个对象,该对象将在运行时向 XAML 提供值。XAML 支持为此目的的标记扩展

在本单元中,您将学习如何创建和使用标记扩展。

什么是加价扩展?

标记扩展是在 XAML 中用于访问运行时值的类。假设你在 XAML UI 中定义了多个标签,并且你希望在整个应用中将该属性设置为相同的值,以确保所有标签的样式一致。可以使用 XAML 设置该属性,如下面的示例所示。FontSizeFontSize

<Label Text="Hello, World!"
            Grid.Row="0"
            SemanticProperties.HeadingLevel="Level1"
            FontSize="28"
            HorizontalOptions="CenterAndExpand"/>

您可以对每个标签重复此相同的设置,但是如果以后要更改此值,该怎么办?必须找到此属性的每个实例并进行更改。另外,假设您不知道要使用什么值;它可以在运行时根据设备方向,屏幕分辨率或其他考虑因素等因素进行计算。在这些情况下,您需要比硬编码文本更复杂的内容。这就是标记扩展有用的地方。标记扩展使你能够灵活地获取在 XAML 中使用的值。

创建标记扩展

标记扩展是实现 Microsoft.Maui.Controls.Xaml.IMarkupExtension 接口的类。此接口定义了一个名为 ProvideValue 的方法,该方法具有以下签名:

public object ProvideValue(IServiceProvider serviceProvider)
{
    ...
}

此方法的目的是为 XAML 标记提供值。请注意,返回类型为 ,因此该值可以是任何类型,只要它适合使用它的位置。例如,在计算并返回字体大小的标记扩展中,返回类型应为 .objectdouble

serviceProvider 参数包含有关在 XAML 代码中使用标记扩展的位置的上下文信息。在其他信息中,它标识要应用扩展的控件。

属性的标记扩展可以保持简单。在下面的示例中,MainPage 类公开了一个名为 MyFontSize 的字段。GlobalFontSizeExtension 类实现 IMarkupExtension 接口,ProvideValue 方法返回 MyFontSize 变量的值:FontSizedouble

namespace MyMauiApp;

public partial class MainPage : ContentPage
{
    public const double MyFontSize = 28;

    public MainPage()
    {
        InitializeComponent();
        ...
    }
    ...
}

public class GlobalFontSizeExtension : IMarkupExtension
{
    public object ProvideValue(IServiceProvider serviceProvider)
    {
        return MainPage.MyFontSize;
    }
}

备注

MyFontSize 字段必须是 MainPage 类的成员,才能允许以他的方式在 ProvideValue 方法中引用它。良好的做法意味着在这种情况下,变量也应该是一个常量。值为 。staticconststatic

ProvideValue 方法还可以根据设备的方向和外形尺寸对返回的值进行调整。

将标记扩展应用于 XAML 中的控件

若要在 XAML 代码中使用标记扩展,请将包含 GlobalFontSizeExtension 类的命名空间添加到标记中的命名空间列表中。在下面的示例中,此命名空间被赋予别名 mycodeContentPage

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:mycode="clr-namespace:MyMauiApp"
             x:Class="MyMauiApp.MainPage">

您可以使用标记扩展来设置属性,如下所示。请注意,约定是标记扩展的名称中具有后缀扩展。XAML 可识别此后缀,从 XAML 代码调用扩展时无需包含它。在下面的示例中,GlobalFontSizeExtension 类被简单地引用为 GlobalFontSizeFontSize

<Label Text="Hello, World!"
            Grid.Row="0"
            SemanticProperties.HeadingLevel="Level1"
            FontSize="{mycode:GlobalFontSize}"
            HorizontalOptions="CenterAndExpand"/>

你可以在整个 XAML 代码中对需要指定字体大小的任何控件应用相同的标记扩展名。稍后,如果您决定更改字体大小,则只需修改 MainPage 类中 MyFontSize 变量的定义。

静态扩展类

由于 GlobalFontSize 标记扩展很有用,因此您不太可能创建这样的扩展。原因很简单;.NET MAUI 已经提供了一个更通用的扩展,使您能够在代码中引用任何静态值。此扩展名为 StaticExtension,或简称 Static。下面的代码显示了此扩展类的基本大纲:

[ContentProperty ("Member")]
public class StaticExtension : IMarkupExtension
{
    public string Member {get; set;}
    public object ProvideValue (IServiceProvider serviceProvider)
    {
        ...
    }
}

备注

自定义标记扩展的目的是使您能够处理更复杂的情况,而不是简单的静态情况。例如,您可能需要根据设备外形规格动态更改字体大小。

若要在 XAML 代码中使用此类,请在 Member 属性中提供要引用的静态变量的名称,并且 ProvideValue 方法在此变量中返回值。下面的示例演示如何使用它:

<Label Text="Hello, World!"
            Grid.Row="0"
            SemanticProperties.HeadingLevel="Level1"
            FontSize="{x:Static Member=mycode:MainPage.MyFontSize}"
            HorizontalOptions="CenterAndExpand"/>

.NET MAUI 提供了一组其他标记扩展类,您可以使用这些类进行数据绑定、引用动态资源和样式以及处理数据数组等方案。

XAML 中特定于平台的值

应用在每个平台上的视觉体验都会有所不同。你通常需要根据你使用的视觉元素为每个平台微调你的 UI。.NET MAUI 使您能够根据这些设备属性管理应用的布局。

在本单元中,你将了解 .NET MAUI 提供的功能,这些功能允许你根据应用运行的平台调整应用的 UI。

使用设备类

该类是一个实用工具类,它为运行应用的设备提供特定于设备的信息。它通过一组属性公开此信息。最重要的属性是 。该属性返回一个字符串,指示当前正在使用的设备类型;“Android”、“iOS”、“WinUI”或“macOS”。DeviceInfoDeviceInfo.Platform

请考虑以下方案作为何时可以使用此功能的示例。.NET MAUI iOS 应用中的默认行为是添加到页面的内容会侵占屏幕顶部的 iOS 状态栏。您想要更改此行为。最简单的解决方案是将页面中的内容向下移动。您在上一练习中创建的 Notes 解决方案通过将控件的属性设置为将内容下移 60 点来解决此问题:MarginVerticalStackLayout

<VerticalStackLayout x:Name="MyStackLayout" Padding="30,60,30,30">
    ...
</VerticalStackLayout>

问题是这个问题仅适用于iOS。在Android和WinUI上将内容向下移动这么多会导致页面顶部的屏幕空间浪费。

您可以查询属性以解决此显示问题。你可以将以下代码添加到应用中的页面构造函数,以展开页面顶部的填充,但仅适用于 iOS:DeviceInfo.Platform

MyStackLayout.Padding = 
    DeviceInfo.Platform == DevicePlatform.iOS
        ? new Thickness(30, 60, 30, 30) // Shift down by 60 points on iOS only
        : new Thickness(30); // Set the default margin to be 30 points

备注

DevicePlatform.iOS是返回字符串值“iOS”的结构。其他受支持的平台具有等效的属性。您应该使用这些属性,而不是与硬编码的字符串进行比较;这是一种很好的做法,如果其中一些字符串值将来发生更改,它将使您的代码适应未来。DevicePlatform

此代码有效,但它位于页面的代码隐藏文件中。填充是用户界面特定的值。可以说,从 XAML 执行此操作比在代码隐藏中执行此操作更合适、更方便。

使用平台上标记扩展

.NET MAUI XAML 提供了标记扩展,使你能够从 XAML 代码中的内部检测运行时平台。将此标记扩展作为设置属性值的 XAML 代码的一部分应用。该扩展要求您提供属性的类型,以及一系列块,您可以在其中根据平台设置属性的值。OnPlatformOn Platform

备注

标记扩展是通用的;它采用类型参数。属性指定的类型可确保使用正确的扩展类型。OnPlatformTypeArguments

您可以按如下方式设置属性。请注意,该属性的类型为:PaddingPaddingThickness

<VerticalStackLayout>
    <VerticalStackLayout.Padding>
        <OnPlatform x:TypeArguments="Thickness">
            <On Platform="iOS" Value="30,60,30,30" />
        </OnPlatform>
    </VerticalStackLayout.Padding>
    <!--XAML for other controls goes here -->
    ...
</VerticalStackLayout>

对于 iOS 以外的平台,填充将保持设置为其默认值“0,0,0,0”。对于 WinUI 和 Android,您可以使用其他块将填充设置为 30 点:On Platfom

<VerticalStackLayout>
    <VerticalStackLayout.Padding>
        <OnPlatform x:TypeArguments="Thickness">
            <On Platform="iOS" Value="30,60,30,30" />
            <On Platform="Android" Value="30" />
            <On Platform="WinUI" Value="30" />
        </OnPlatform>
    </VerticalStackLayout.Padding>
    ...
</VerticalStackLayout>

您可以将此相同技术应用于其他属性。下面的示例将页面上堆栈布局的背景色更改为 iOS 上的银色、Android 上的绿色和 Windows 上的黄色。

<VerticalStackLayout>
    ...
    <VerticalStackLayout.BackgroundColor>
        <OnPlatform x:TypeArguments="Color">
            <On Platform="iOS" Value="Silver" />
            <On Platform="Android" Value="Green" />
            <On Platform="WinUI" Value="Yellow" />
        </OnPlatform>
    </VerticalStackLayout.BackgroundColor>
    ...
</VerticalStackLayout>

此语法有点冗长,但有一个简化的语法可用于扩展。您可以简化设置填充的示例,如下所示:OnPlatform

<VerticalStackLayout Padding="{OnPlatform iOS='30,60,30,30', Default='30'}">
    <!--XAML for other controls goes here -->
</VerticalStackLayout>

您可以指定属性的默认值以及任何特定于平台的值。在此窗体中,类型参数是从应用该特性的属性推断出来的。OnPlatform

若要设置背景色,可以使用此 XAML 片段代替上面的第二个示例:

<VerticalStackLayout BackgroundColor="{OnPlatform WinUI=Yellow, iOS=Silver, Android=Green}">
    ...
</VerticalStackLayout>

练习:向 XAML 页面添加行为

你之前已修改了 Notes 应用,以将 UI 布局从 C# 代码移动到 XAML。您现在已准备好向页面添加以下功能:

  • 支持自定义标签、按钮和编辑器控件的字体颜色和背景色。通过这种方式,可以轻松调整应用,使其更易于需要高对比度 UI 的用户访问。
  • 在 Android 和 iOS 上调整编辑器控件的高度。在 Windows 上运行时,此控件具有足够的宽度,允许用户在滚动之前输入合理数量的文本。在Android手机或iPhone上,较窄的宽度会导致滚动发生得更快,因此提供更多的垂直空间是有益的。

在 XAML 中使用静态资源

您将创建一个静态类来保存应用程序的字体颜色和背景颜色的值。然后,你将使用标记扩展从类中读取这些值,并将它们应用于页面上控件的 XAML 标记。x:Static

在 Visual Studio 中,返回到在上一练习中编辑的“便笺”应用。 

备注

该应用程序的工作副本位于您在上一次练习开始时克隆的锻炼存储库的 exercise2 文件夹中。

“解决方案资源管理器”窗口中,右击“Notes”项目,选择“添加”,然后选择“”。

“添加新项”对话框中,确保选中“”模板。将新类文件命名为“共享资源.cs”,然后选择“添加”:

在 SharedResources.cs 文件中,将指令替换为如下所示的指令,并将 SharedResources 类标记为:usingstatic

namespace Notes;

static class SharedResources
{

}

将字段 FontColor 添加到 SharedResources 类。此字段当前提供与蓝色对应的值,但您可以使用 RGB 值的任何有效组合对其进行修改:static readonly

static class SharedResources
{
    public static readonly Color FontColor = Color.FromRgb(0, 0, 0xFF);
}

添加名为“背景颜色”的第二个字段,并将其设置为您选择的颜色:static readonly

static class SharedResources
{
    ...
    public static readonly Color BackgroundColor = Color.FromRgb(0xFF, 0xF0, 0xAD);
}

打开 MainPage.xaml 文件。

将下面显示的 XML 命名空间声明添加到元素中,在属性之前。此声明将 C# Notes 命名空间中的类引入 XAML 页的作用域:ContentPagex:Class

<ContentPage ...
         xmlns:notes="clr-namespace:Notes"
         x:Class="Notes.MainPage"
         ...>

将以下代码中显示的特性添加到控件中。此标记使用标记扩展来检索 SharedResources 类中字段中的值存储:TextColorLabelx:Staticstatic

<Label Text="Notes"
    HorizontalOptions="Center"
    FontAttributes="Bold" 
    TextColor="{x:Static Member=notes:SharedResources.FontColor}" />

使用标记扩展可以设置 和 控件的属性。主页的已完成标记应如下所示:x:StaticTextColorBackgroundColorEditorButton

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
         xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
         xmlns:notes="clr-namespace:Notes"
         x:Class="Notes.UIPage">

    <VerticalStackLayout Padding="30,60,30,30">
        <Label Text="Notes"
            HorizontalOptions="Center"
            FontAttributes="Bold" 
            TextColor="{x:Static Member=notes:SharedResources.FontColor}" />

        <Editor x:Name="editor"
                Placeholder="Enter your note"
                HeightRequest="100" 
                TextColor="{x:Static Member=notes:SharedResources.FontColor}"/>

        <Grid Grid.Row="2" ColumnDefinitions="Auto,30,Auto">

            <Button Grid.Column="0"
                    Text="Save" 
                    WidthRequest="100"
                    TextColor="{x:Static Member=notes:SharedResources.FontColor}"
                    BackgroundColor="{x:Static Member=notes:SharedResources.BackgroundColor}"
                    Clicked="OnSaveButtonClicked" />

            <Button Grid.Column="2"
                    Text="Delete" 
                    WidthRequest="100"
                    TextColor="{x:Static Member=notes:SharedResources.FontColor}"
                    BackgroundColor="{x:Static Member=notes:SharedResources.BackgroundColor}"
                    Clicked="OnDeleteButtonClicked" />
        </Grid>
    </VerticalStackLayout>

</ContentPage>

备注

此 XAML 代码包含设置 和 属性的标记的重复。XAML 使你能够定义可通过使用 App.xaml 文件中的资源字典跨应用应用的资源。此技术将在后面的模块中介绍。TextColorBackgroundColor

重新生成应用并使用 Windows 运行它。验证颜色是否与您在 SharedResources 类中指定的颜色匹配。如果您有时间,还可以尝试使用 Android 模拟器运行应用:

完成后返回Visual Studio。

添加特定于平台的自定义项

在 Visual Studio 中打开 MainPage.xaml 文件。

找到控件的定义,并修改 HeightRequest 属性的值,如下面的示例所示:Editor

<Editor x:Name="editor"
        ...
        HeightRequest="{OnPlatform 100, Android=500, iOS=500}" 
        .../>

此标记将控件的默认高度设置为 100 个单位,但在 Android 上,它增加到 500 个单位。

重新生成应用并使用 Windows 运行它,然后在 Android 上运行它。应用在每个平台上应如下所示:

总结

编码的 UI 使管理布局和行为变得困难。这种方法通常包括布局和行为逻辑,并导致两者之间的紧密耦合。UI 设计中的更改可能会对代码库的其余部分产生连锁反应。维护一个没有UI和行为完全分离的代码库可能很困难。

.NET MAUI 使您能够使用 XAML 定义 UI。通过这种分离,可以专注于 C# 代码文件中的行为逻辑。UI 设计人员现在可以专注于 UI,而程序员可以专注于代码。

.NET MAUI XAML 使您能够使用标记扩展为每个平台自定义 UI。此方法允许你设计可利用特定于操作系统的 UI 功能但在所有平台上仍具有良好的外观的应用。OnPlatform

在本模块中,你了解了如何使用 XAML 达到最佳效果,以便为跨平台应用设计 UI。具体来说,您了解到:

  • 与在 C 语言中为 .NET MAUI 应用定义 UI 相比,使用 XAML 的好处#
  • 如何使用 XAML 创建页和控件,并设置其属性
  • 如何处理 UI 事件并在 XAML 中连接它们
  • 如何创建和使用 XAML 标记扩展
  • 如何在 XAML 标记中设置特定于平台的值

给TA打赏
共{{data.count}}人
人已打赏
.NET MAUI

自定义 .NET MAUI XAML 页面中的布局

2022-6-30 10:29:31

.NET MAUI

跟我做⼀个高德地图的 iOS / Android MAUI 控件(前言)

2022-7-8 9:47:09

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
今日签到
有新私信 私信列表
搜索