创建 .NET MAUI 应用

本教程旨在演示如何创建仅使用跨平台代码的 .NET 多平台应用 UI (.NET MAUI) 应用。 这意味着,你编写的代码不会特定于 Windows、Android、iOS 或 macOS。 你将创建的应用将是记笔记应用,用户可以在其中创建、保存和加载多个笔记。

本教程中,您将学习如何:

  • 创建 .NET MAUI shell 应用。
  • 在所选平台上运行应用。
  • 使用可扩展应用程序标记语言 (XAML) 定义用户界面,并通过代码与 XAML 元素进行交互。
  • 创建视图并将其绑定到数据。
  • 使用导航在页面之间移动。

你将使用 Visual Studio 2022 创建应用程序,可以使用它输入笔记并将其保存到设备存储中。 最后一个应用程序如下所示:

笔记应用的最终屏幕截图,其中列出了笔记。 笔记应用的最终屏幕截图,其中添加了笔记。

创建项目

在开始本教程之前,必须遵循构建第一个应用文章中的操作。 创建项目时,请使用以下设置:

  • 项目名称

    此属性必须设置为 Notes。 如果项目的名称不同,则从本教程复制和粘贴的代码可能会导致生成错误。

  • 将解决方案和项目放在同一目录中

    取消选中此设置。

在 Visual Studio中将 .NET MAUI 项目的名称设置为“笔记”。

创建项目时,请选择最新的 .NET 版本。

选择目标设备

根据设计,.NET MAUI 应用可在多个操作系统和设备上运行。 你需要选择要用于测试和调试应用的目标。

在 Visual Studio 工具栏中,将“调试目标”设置为要用于调试和测试的设备。 以下步骤演示如何将“调试目标”设置为 Android:

在 Visual Studio 中为 .NET MAUI 应用选择 Android 调试目标。

  1. 选择“调试目标”下拉列表按钮。
  2. 选择“Android 仿真器”项。
  3. 选择仿真器设备。

自定义应用 shell

Visual Studio 创建 .NET MAUI 项目时,将生成四个重要的代码文件。 可以在 Visual Studio 的“解决方案资源管理器”窗格中看到这些文件:

在 Visual Studio 中显示 .NET MAUI 项目文件的解决方案资源管理器。

这些文件有助于配置和运行 .NET MAUI 应用。 每个文件都有不同的用途,如下所述:

  • MauiProgram.cs

    这是启动应用的代码文件。 此文件中的代码充当应用的跨平台入口点,用于配置和启动应用。 模板启动代码指向 App 文件定义的 类。

  • App.xamlApp.xaml.cs

    为了简单起见,这两个文件称为单个文件。 所有 XAML 文件通常都包含两个文件,即 .xaml 文件本身,以及一个相应的代码文件,该文件是“解决方案资源管理器”中的 .xaml 文件的子项。 .xaml 文件包含 XAML 标记,代码文件包含用户创建的用于与 XAML 标记交互的代码。

    App.xaml 文件包含应用范围的 XAML 资源,例如颜色、样式或模板。 App.xaml.cs 文件通常包含用于实例化 Shell 应用程序的代码。 在此项目中,它指向 AppShell 类。

  • AppShell.xamlAppShell.xaml.cs

    此文件定义 AppShell 类,该类用于定义应用的视觉层次结构。

  • MainPage.xamlMainPage.xaml.cs

    这是应用显示的启动页。 MainPage.xaml 文件定义页面的 UI(用户界面)。 MainPage.xaml.cs 包含 XAML 的代码隐藏,如按钮单击事件的代码。

添加“关于”页

你将执行的第一个自定义操作是向项目添加另一个页面。 此页面是一个“关于”页面,它表示有关此应用的信息,例如作者、版本和可能提供更多详细信息的链接。

  1. 在 Visual Studio 的解决方案资源管理器 窗格中,右键单击 Notes 项目 >“添加>新项”

    右键单击 Visual Studio 中的项目并选择“新建项”。

  2. “添加新项”对话框中,在窗口左侧的模板列表中选择 .NET MAUI。 接下来,选择 .NET MAUI ContentPage (XAML) 模板。 将文件命名为 AboutPage.xaml,然后选择“添加”

    向项目添加新的 ContentPage。该 ContentPage 名为 AboutPage.xaml。

  3. AboutPage.xaml 文件将打开新的文档选项卡,显示所有表示页面 UI 的 XAML 标记。 将 XAML 标记替换为以下标记:

    <?xml version="1.0" encoding="utf-8" ?>
    <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
                 xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                 x:Class="Notes.AboutPage">
        <VerticalStackLayout Spacing="10" Margin="10">
            <HorizontalStackLayout Spacing="10">
                <Image Source="dotnet_bot.png"
                       SemanticProperties.Description="The dot net bot waving hello!"
                       HeightRequest="64" />
                <Label FontSize="22" FontAttributes="Bold" Text="Notes" VerticalOptions="End" />
                <Label FontSize="22" Text="v1.0" VerticalOptions="End" />
            </HorizontalStackLayout>
    
            <Label Text="This app is written in XAML and C# with .NET MAUI." />
            <Button Text="Learn more..." Clicked="LearnMore_Clicked" />
        </VerticalStackLayout>
    </ContentPage>
    
  4. 通过按 Ctrl+S 或选择菜单“文件”>“保存 AboutPage.xaml”来保存文件。

让我们将页面上 XAML 控件的关键部分细分一下:

  • <ContentPage>AboutPage 类的根对象。

  • <VerticalStackLayout>ContentPage 的唯一子对象。 ContentPage 只能有一个子对象。 VerticalStackLayout 类型可以有多个子项。 此布局控件将其子控件逐一垂直排列。

  • <HorizontalStackLayout> 的运行方式与 <VerticalStackLayout>相同,只是其子项以水平方式排列。

  • <Image> 显示图像,在本例中,它使用的是每个 .NET MAUI 项目附带的 dotnet_bot.png 图像。

    重要

    添加到项目的文件实际上是 dotnet_bot.svg。 .NET MAUI 基于目标设备将可缩放的向量图形 (SVG) 文件转换为可移植网络图形格式 (PNG) 文件。 因此,将 SVG 文件添加到 .NET MAUI 应用项目时,应从具有 .png 扩展名的 XAML 或 C# 引用该文件。 对 SVG 文件的唯一引用应位于项目文件中。

  • <Label> 控制显示文本。

  • 用户可以按下 <Button> 控件来引发 Clicked 事件。 可运行代码来响应 Clicked 事件。

  • Clicked="LearnMore_Clicked"

    将按钮的 Clicked 事件分配给 LearnMore_Clicked 事件处理程序,将在代码隐藏文件中定义该处理程序。 将在下一步骤中创建此代码。

处理 Clicked 事件

下一步是为按钮的 Clicked 事件添加代码。

  1. 在 Visual Studio 的“解决方案资源管理器”窗格中,展开 AboutPage.xaml 文件以显示其代码隐藏文件 AboutPage.xaml.cs。 然后,双击 AboutPage.xaml.cs 文件,在代码编辑器中打开该文件。

    Visual Studio 中“解决方案资源管理器”窗口的图像,其中红色框突出显示了展开图标。

  2. 添加以下 LearnMore_Clicked 事件处理程序代码,该代码将系统浏览器打开到特定 URL:

    private async void LearnMore_Clicked(object sender, EventArgs e)
    {
        // Navigate to the specified URL in the system browser.
        await Launcher.Default.OpenAsync("https://aka.ms/maui");
    }
    

    请注意,async 关键字已添加到方法声明中,这允许在打开系统浏览器时使用 await 关键字。

  3. 通过按 Ctrl+S 或选择菜单“文件”>“保存 AboutPage.xaml.cs”来保存文件。

现在,AboutPage 的XAML 和代码隐藏已完成,需要将其显示在应用中。

添加图像资源

某些控件可以使用图像,从而增强用户与应用交互的方式。 在本部分中,将下载要在应用中使用的两个图像,以及两个用于 iOS 的备用图像。

下载以下图像:

下载图像后,可以使用文件资源管理器将其移动到项目的 Resources\Images 文件夹。 此文件夹中的任何文件都会作为 MauiImage 资源自动包含在项目中。 还可以使用 Visual Studio 将图像添加到项目中。 如果手动移动图像,请跳过以下过程。

重要

请勿跳过下载特定于 iOS 的图像的步骤,它们是完成本教程所必需的。

使用 Visual Studio 移动图像

  1. 在 Visual Studio 的“解决方案资源管理器” 窗格中,展开“资源”文件夹,其中显示了 图像 文件夹。

    小窍门

    可以使用文件资源管理器将图像直接拖放到“图像”文件夹顶部的“解决方案资源管理器”窗格中。 这会自动将文件移动到文件夹,并将其包含在项目中。 如果选择拖放文件,请忽略此过程的其余部分。

  2. 右键单击 “图像 ”,然后选择“ 添加>现有项”。

  3. 导航到包含下载的图像的文件夹。

  4. 将文件类型筛选器更改为“图像文件”。

  5. 按住Ctrl 并单击下载的每个图像,然后按下“添加”

将 4 个图标图像添加到 .NET MAUI 项目。

修改应用外壳

如本文开头所述,AppShell 类定义了应用的视觉对象层次结构,即用于创建应用 UI 的 XAML 标记。 更新 XAML 以添加 TabBar 控件:

  1. “解决方案资源管理器”窗格中双击 AppShell.xaml 文件打开 XAML 编辑器。 将 XAML 标记替换为以下代码:

    <?xml version="1.0" encoding="UTF-8" ?>
    <Shell
        x:Class="Notes.AppShell"
        xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
        xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
        xmlns:local="clr-namespace:Notes"
        Shell.FlyoutBehavior="Disabled">
    
        <TabBar>
            <ShellContent
                Title="Notes"
                ContentTemplate="{DataTemplate local:MainPage}"
                Icon="{OnPlatform 'icon_notes.png', iOS='icon_notes_ios.png', MacCatalyst='icon_notes_ios.png'}" />
    
            <ShellContent
                Title="About"
                ContentTemplate="{DataTemplate local:AboutPage}"
                Icon="{OnPlatform 'icon_about.png', iOS='icon_about_ios.png', MacCatalyst='icon_about_ios.png'}" />
        </TabBar>
    
    </Shell>
    
  2. 通过按下 Ctrl+S 或选择“文件”>“保存 AppShell.xaml”菜单保存文件。

让我们分解 XAML 的关键部分:

  • <Shell> 是 XAML 标记的根对象。
  • <TabBar> 是 Shell 的内容。
  • <ShellContent> 内的两个 <TabBar> 对象。 在替换模板代码之前,存在一个指向 <ShellContent> 页的 MainPage 对象。

TabBar 及其子项不表示任何用户界面元素,而是表示应用的视觉对象层次结构的组织。 Shell 会采用这些对象并生成内容的用户界面,顶部有表示每个页面的栏。 每个页面的 ShellContent.Icon 属性使用 OnPlatform 标记扩展。 此 XAML 标记扩展用于为不同的平台指定不同的值。 在此示例中,每个平台默认使用 icon_about.png 图标,但 iOS 和 MacCatalyst 使用 icon_about_ios.png

每个 <ShellContent> 对象都指向要显示的页面。 这是由 ContentTemplate 属性设置的。

运行应用

通过按下 F5 或按下 Visual Studio 顶部的“播放”按钮运行应用:

Visual Studio 的“调试目标”按钮,其中包含文本“Windows 计算机”。

将显示两个选项卡:“笔记”“关于”。 按下“关于”选项卡,应用导航至已创建的 AboutPage。 按“ 了解详细信息 ”按钮打开 Web 浏览器。

.NET MAUI 应用教程的“关于”页面。

关闭应用并返回到 Visual Studio。 如果使用的是 Android 模拟器,请在虚拟设备中终止应用,或按下位于 Visual Studio 顶部的停止按钮:

Visual Studio 的“停止调试”按钮。

为笔记创建页面

既然应用包含 MainPageAboutPage,那么可以开始创建应用的其余部分。 首先,你将创建一个允许用户创建和显示笔记的页面,然后编写代码以加载和保存笔记。

“笔记”页将显示笔记,你可以将其保存或删除。 首先,将新页面添加至项目:

  1. 在 Visual Studio 的解决方案资源管理器 窗格中,右键单击 Notes 项目 >“添加>新项”

  2. “添加新项”对话框中,在窗口左侧的模板列表中选择 .NET MAUI。 接下来,选择 .NET MAUI ContentPage (XAML) 模板。 为 NotePage.xaml 文件命名,然后选择“添加”

  3. NotePage.xaml 文件将在新选项卡中打开,显示所有表示页面 UI 的 XAML 标记。 将 XAML 代码标记替换为以下标记:

    <?xml version="1.0" encoding="utf-8" ?>
    <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
                 xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                 x:Class="Notes.NotePage"
                 Title="Note">
        <VerticalStackLayout Spacing="10" Margin="5">
            <Editor x:Name="TextEditor"
                    Placeholder="Enter your note"
                    HeightRequest="100" />
    
            <Grid ColumnDefinitions="*,*" ColumnSpacing="4">
                <Button Text="Save"
                        Clicked="SaveButton_Clicked" />
    
                <Button Grid.Column="1"
                        Text="Delete"
                        Clicked="DeleteButton_Clicked" />
            </Grid>
        </VerticalStackLayout>
    </ContentPage>
    
  4. 通过按 Ctrl + S 或选择菜单“文件”>“保存 NotePage.xaml”来保存文件。

让我们将页面上 XAML 控件的关键部分细分一下:

  • <VerticalStackLayout> 将其子控件逐一垂直排列

  • <Editor> 是多行文本编辑器控件,也是 VerticalStackLayout 内部的第一个控件。

  • <Grid> 是布局控件,是 VerticalStackLayout 内部的第二个控件。

    此控件定义用于创建单元格的列和行。 子控件放置在这些单元格中。

    默认情况下,Grid 控件包含单个行和列,可用于创建单个单元格。 列具有定义的宽度,宽度的 * 值指示列尽可能多地填充空间。 前面的代码片段定义了两个列,两者都尽可能多地使用空间,从而均匀地在分配的空间中分布列:ColumnDefinitions="*,*"。 列大小使用 , 字符分隔。

    Grid 定义的列和行的索引从 0 开始。 因此,第一列为索引 0,第二列为索引 1,依此类推。

  • 两个 <Button> 控件位于 <Grid> 内,并且为它们分配了一个列。 如果子控件未定义列分配,则会自动将其分配给第一列。 在此标记中,第一个按钮是“保存”按钮,被自动分配到第一个列(第 0 列)。 第二个按钮是“删除”按钮,被分配到第二个列(第 1 列)。

    请注意,这两个按钮已处理了 Clicked 事件。 在下一部分中,你将为这些处理程序添加代码。

加载并保存笔记

打开 NotePage.xaml.cs 代码隐藏文件。 可以通过三种方式打开 NotePage.xaml 文件的代码隐藏:

  • 如果 NotePage.xaml 处于打开状态并且是正在编辑的活动文档,请按 F7
  • 如果 NotePage.xaml 处于打开状态并且是正在编辑的活动文档,请在文本编辑器中右键单击并选择“查看代码”
  • 使用解决方案资源管理器展开 NotePage.xaml 条目,显示 NotePage.xaml.cs 文件。 双击文件以将其打开。

添加新的 XAML 文件时,代码隐藏包含构造函数中的单个行,即对 InitializeComponent 方法的调用:

namespace Notes;

public partial class NotePage : ContentPage
{
    public NotePage()
    {
        InitializeComponent();
    }
}

InitializeComponent 方法会读取 XAML 标记并初始化标记定义的所有对象。 对象在其父子关系中连接,代码中定义的事件处理程序将附加到 XAML 中设置的事件。

现在,你已详细了解代码隐藏文件,接下来将代码添加到 NotePage.xaml.cs 代码隐藏文件,以处理加载和保存笔记。

  1. 创建笔记后,它将作为文本文件保存到设备。 文件的名称由 _fileName 变量表示。 将以下 string 变量声明添加到 NotePage 类:

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

    上述代码会构造文件的路径,将其存储在应用的本地数据目录中。 文件名为 notes.txt

  2. 在类的构造函数中,调用 InitializeComponent 方法后,从设备读取文件并将其内容存储在 TextEditor 控件的 Text 属性中:

    public NotePage()
    {
        InitializeComponent();
    
        if (File.Exists(_fileName))
            TextEditor.Text = File.ReadAllText(_fileName);
    }
    
  3. 接下来,添加代码以处理 XAML 中定义的 Clicked 事件:

    private void SaveButton_Clicked(object sender, EventArgs e)
    {
        // Save the file.
        File.WriteAllText(_fileName, TextEditor.Text);
    }
    
    private void DeleteButton_Clicked(object sender, EventArgs e)
    {
        // Delete the file.
        if (File.Exists(_fileName))
            File.Delete(_fileName);
    
        TextEditor.Text = string.Empty;
    }
    

    SaveButton_Clicked 方法会将 Editor 控件中的文本写入由 _fileName 变量表示的文件。

    DeleteButton_Clicked 方法首先检查 _fileName 变量表示的文件,如果该文件存在,则将其删除。 接下来,清除 Editor 控件的文本。

  4. 通过按 Ctrl + S 或选择菜单“文件”>“保存 NotePage.xaml.cs”来保存文件。

代码隐藏文件的最终代码应如下所示:

namespace Notes;

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

    public NotePage()
    {
        InitializeComponent();

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

    private void SaveButton_Clicked(object sender, EventArgs e)
    {
        // Save the file.
        File.WriteAllText(_fileName, TextEditor.Text);
    }

    private void DeleteButton_Clicked(object sender, EventArgs e)
    {
        // Delete the file.
        if (File.Exists(_fileName))
            File.Delete(_fileName);

        TextEditor.Text = string.Empty;
    }
}

测试笔记

现在,已完成笔记页,你需要一种方法来向用户显示它。 打开 AppShell.xaml 文件,并将第一个 ShellContent 条目更改为指向 NotePage,而不是 MainPage

<?xml version="1.0" encoding="UTF-8" ?>
<Shell
    x:Class="Notes.AppShell"
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:local="clr-namespace:Notes"
    Shell.FlyoutBehavior="Disabled">

    <TabBar>
        <ShellContent
            Title="Notes"
            ContentTemplate="{DataTemplate local:NotePage}"
            Icon="{OnPlatform 'icon_notes.png', iOS='icon_notes_ios.png', MacCatalyst='icon_notes_ios.png'}" />

        <ShellContent
            Title="About"
            ContentTemplate="{DataTemplate local:AboutPage}"
            Icon="{OnPlatform 'icon_about.png', iOS='icon_about_ios.png', MacCatalyst='icon_about_ios.png'}" />
    </TabBar>

</Shell>

保存文件并运行应用。 尝试在输入框中键入,然后按“保存”按钮。 关闭应用,然后重新打开。 输入的笔记应从设备的存储中加载。

.NET MAUI 应用中的笔记条目页面。

将数据绑定到 UI 并导航页面

本教程的此部分介绍视图、模型和应用内导航的概念。

在本教程的前面步骤中,你向项目添加了两个页面:NotePageAboutPage。 页面表示数据的视图。 NotePage 是显示“笔记数据”的“视图”,AboutPage 是显示“应用信息数据”的“视图”。这两个视图都具有硬编码或嵌入其中的数据模型,需要将数据模型与视图分开。

将模型与视图分离有什么好处? 它允许你设计视图来表示模型的任何部分并与之交互,而无需担心实现模型的实际代码。 可以使用数据绑定来达成此目的,本教程稍后将介绍这些内容。 不过,现在让我们重新构建项目。

分离视图和模型

重构现有代码以将模型与视图分开。 接下来的几个步骤将组织代码,以便分别定义视图和模型。

  1. 请从项目中删除 MainPage.xamlMainPage.xaml.cs,它们已不再需要。 在“解决方案资源管理器”窗格中,找到 MainPage.xaml 的条目,右键单击它并选择“删除”

    小窍门

    删除 MainPage.xaml 项还应删除 MainPage.xaml.cs 项。 如果未删除 MainPage.xaml.cs,请右键单击它并选择“删除”

  2. 右键单击 Notes 项目,然后选择“添加”>“新建文件夹”。 将该文件夹命名为 Models

  3. 右键单击 Notes 项目,然后选择“添加”>“新建文件夹”。 将该文件夹命名为 Views

  4. 查找“NotePage.xaml”项并将其拖动到 Views 文件夹。 NotePage.xaml.cs 应随其移动。

    重要

    移动文件时,Visual Studio 通常会发出警告,提示移动操作可能需要很长时间。 如果看到此警告,则表示没有任何问题,请按“确定”

    Visual Studio 还可能会询问你是否要调整移动文件的命名空间。 选择“否”,因为后续步骤将更改命名空间。

  5. 查找“AboutPage.xaml”项并将其拖动到 Views 文件夹。 AboutPage.xaml.cs 应随其移动。

更新视图命名空间

将视图移动到 Views 文件夹后,需要更新命名空间才能匹配。 页面的 XAML 和代码隐藏文件的命名空间设置为 Notes。 需要将其更新为 Notes.Views

  1. “解决方案资源管理器”窗格中,展开“NotePage.xaml”“AboutPage.xaml”以显示代码隐藏文件:

    “笔记”项目,其中同时展开了“视图”文件夹和页面视图。

  2. 双击“NotePage.xaml.cs”项以打开代码编辑器。 将命名空间更改为 Notes.Views

    namespace Notes.Views;
    
  3. AboutPage.xaml.cs 项目重复前面的步骤。

  4. 双击“NotePage.xaml”项以打开 XAML 编辑器。 旧命名空间通过 x:Class 特性引用,该特性定义哪个类类型是 XAML 的代码隐藏。 此条目不仅仅是命名空间,而是具有该类型的命名空间。 将 x:Class 值更改为 Notes.Views.NotePage

    x:Class="Notes.Views.NotePage"
    
  5. AboutPage.xaml 项重复上一步,但将 x:Class 值设置为 Notes.Views.AboutPage

修复 Shell 中的命名空间引用

AppShell.xaml 定义两个选项卡,一个用于 NotesPage,另一个用于 AboutPage。 现在,这两个页面已移动到新命名空间,XAML 中的类型映射现已无效。 在“解决方案资源管理器”窗格中,双击“AppShell.xaml”条目以在 XAML 编辑器中将其打开。 它应类似于以下代码片段:

<?xml version="1.0" encoding="UTF-8" ?>
<Shell
    x:Class="Notes.AppShell"
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:local="clr-namespace:Notes"
    Shell.FlyoutBehavior="Disabled">

    <TabBar>
        <ShellContent
            Title="Notes"
            ContentTemplate="{DataTemplate local:NotePage}"
            Icon="{OnPlatform 'icon_notes.png', iOS='icon_notes_ios.png', MacCatalyst='icon_notes_ios.png'}" />

        <ShellContent
            Title="About"
            ContentTemplate="{DataTemplate local:AboutPage}"
            Icon="{OnPlatform 'icon_about.png', iOS='icon_about_ios.png', MacCatalyst='icon_about_ios.png'}" />
    </TabBar>

</Shell>

.NET 命名空间通过 XML 命名空间声明导入 XAML。 在先前的 XAML 标记中,它是根元素中的 xmlns:local="clr-namespace:Notes" 属性:<Shell>。 声明 XML 命名空间以在同一程序集中导入 .NET 命名空间的格式为:

xmlns:{XML namespace name}="clr-namespace:{.NET namespace}"

这样,先前的声明会将 local 的 XML 命名空间映射到 Notes 的 .NET 命名空间。 通常的做法是将 local 名称映射到项目的根命名空间。

删除 local XML 命名空间并添加新命名空间。 此新的 XML 命名空间将映射到 Notes.Views 的 .NET 命名空间,将其命名为 views。 声明应类似于以下特性:xmlns:views="clr-namespace:Notes.Views"

local XML 命名空间已被 ShellContent.ContentTemplate 属性使用,将其更改为 views。 现在,XAML 应类似于以下代码片段:

<?xml version="1.0" encoding="UTF-8" ?>
<Shell
    x:Class="Notes.AppShell"
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:views="clr-namespace:Notes.Views"
    Shell.FlyoutBehavior="Disabled">

    <TabBar>
        <ShellContent
            Title="Notes"
            ContentTemplate="{DataTemplate views:NotePage}"
            Icon="{OnPlatform 'icon_notes.png', iOS='icon_notes_ios.png', MacCatalyst='icon_notes_ios.png'}" />

        <ShellContent
            Title="About"
            ContentTemplate="{DataTemplate views:AboutPage}"
            Icon="{OnPlatform 'icon_about.png', iOS='icon_about_ios.png', MacCatalyst='icon_about_ios.png'}" />
    </TabBar>

</Shell>

现在,你应该能够运行应用且不会出现任何编译器错误,并且所有内容都应像以前一样正常运行。

定义模型

目前,模型是嵌入在“笔记”和“关于”视图中的数据。 我们将创建新类来表示该数据。 第一步是创建用于表示笔记页面数据的模型:

  1. “解决方案资源管理器” 窗格中,右键单击 Models 文件夹并选择“ 添加>”。

  2. 将类命名为 Note.cs,然后按“添加”

  3. 打开 Note.cs,将代码替换为以下片段:

    namespace Notes.Models;
    
    internal class Note
    {
        public string Filename { get; set; }
        public string Text { get; set; }
        public DateTime Date { get; set; }
    }
    
  4. 保存文件。

接下来,创建“关于”页面的模型:

  1. “解决方案资源管理器” 窗格中,右键单击 Models 文件夹并选择“ 添加>”。

  2. 将类命名为 About.cs,然后按“添加”

  3. 打开 About.cs,将代码替换为以下片段:

    namespace Notes.Models;
    
    internal class About
    {
        public string Title => AppInfo.Name;
        public string Version => AppInfo.VersionString;
        public string MoreInfoUrl => "https://aka.ms/maui";
        public string Message => "This app is written in XAML and C# with .NET MAUI.";
    }
    
  4. 保存文件。

更新“关于”页面

“关于”页面将是更新最快的页面,你将能够运行应用并查看它如何从模型加载数据。

  1. “解决方案资源管理器”窗格中,打开 Views\AboutPage.xaml 文件。

  2. 使用以下代码片段替换内容:

    <?xml version="1.0" encoding="utf-8" ?>
    <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
                 xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                 xmlns:models="clr-namespace:Notes.Models"
                 x:Class="Notes.Views.AboutPage"
                 x:DataType="models:About">
        <ContentPage.BindingContext>
            <models:About />
        </ContentPage.BindingContext>
        <VerticalStackLayout Spacing="10" Margin="10">
            <HorizontalStackLayout Spacing="10">
                <Image Source="dotnet_bot.png"
                       SemanticProperties.Description="The dot net bot waving hello!"
                       HeightRequest="64" />
                <Label FontSize="22" FontAttributes="Bold" Text="{Binding Title}" VerticalOptions="End" />
                <Label FontSize="22" Text="{Binding Version}" VerticalOptions="End" />
            </HorizontalStackLayout>
    
            <Label Text="{Binding Message}" />
            <Button Text="Learn more..." Clicked="LearnMore_Clicked" />
        </VerticalStackLayout>
    
    </ContentPage>
    

让我们看看上一个代码片段中突出显示的已更改行:

  • xmlns:models="clr-namespace:Notes.Models"

    此行将 Notes.Models .NET 命名空间映射到 models XML 命名空间。

  • x:DataType="models:About"

    此行指示 XAML 编译器编译所有绑定表达式以提高运行时性能,并针对 Notes.Models.About 该类型解析绑定表达式。

  • 使用 BindingContext 的 XML 命名空间和对象,将 ContentPageNote.Models.About 属性设置为 models:About 类的实例。 这是使用属性元素语法,而不是 XML 特性设置的。

    重要

    到目前为止,已使用 XML 特性设置属性。 这非常适用于简单值,例如 Label.FontSize 属性。 但是,如果属性值更为复杂,则必须使用属性元素语法来创建对象。 请参考以下示例,创建一个标签并设置其 FontSize 属性:

    <Label FontSize="22" />
    

    可以使用FontSize设置相同的 属性:

    <Label>
        <Label.FontSize>
            22
        </Label.FontSize>
    </Label>
    
  • 三个 <Label> 控件的 Text 属性值已从硬编码字符串更改为绑定语法:{Binding PATH}

    在运行时处理 {Binding} 语法,从而允许从绑定返回的值是动态值。 PATH{Binding PATH} 部分是要与其绑定的属性路径。 该属性来自当前控件的 BindingContext。 使用 <Label> 控件时,会取消设置 BindingContext。 上下文在控件取消设置时从父级继承,在本例中,设置上下文的父对象是根对象:ContentPage

    BindingContext 中的对象是 About 模型的实例。 其中一个标签的绑定路径将 Label.Text 属性绑定到 About.Title 属性。

对“关于”页面的最后一个更改是更新打开网页的按钮单击。 URL 已硬编码在代码隐藏中,但 URL 应来自 BindingContext 属性中的模型。

  1. “解决方案资源管理器”窗格中,打开 Views\AboutPage.xaml.cs 文件。

  2. LearnMore_Clicked 方法替换为以下代码:

    private async void LearnMore_Clicked(object sender, EventArgs e)
    {
        if (BindingContext is Models.About about)
        {
            // Navigate to the specified URL in the system browser.
            await Launcher.Default.OpenAsync(about.MoreInfoUrl);
        }
    }
    

如果查看突出显示的行,代码将检查类型是否 BindingContextModels.About 类型,如果是,则将其 about 分配给变量。 if 语句内的下一行打开浏览器,访问 about.MoreInfoUrl 属性提供的 URL。

运行该应用后会发现它运行方式与之前完全相同。 尝试更改模型的值,并查看浏览器打开的 UI 和 URL 如何也发生更改的。

更新笔记页

上一部分将 about 页面视图绑定到 about 模型,现在同样可以将 note 视图绑定到 note 模型。 不过,在这种情况下,模型不会在 XAML 中创建,而会在后续几个步骤中由代码隐藏创建。

  1. “解决方案资源管理器”窗格中,打开 Views\NotePage.xaml 文件。

  2. 使用以下代码片段替换内容:

    <?xml version="1.0" encoding="utf-8" ?>
    <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
                 xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                 xmlns:models="clr-namespace:Notes.Models"
                 x:Class="Notes.Views.NotePage"
                 Title="Note"
                 x:DataType="models:Note">
        <VerticalStackLayout Spacing="10" Margin="5">
            <Editor x:Name="TextEditor"
                    Placeholder="Enter your note"
                    Text="{Binding Text}"
                    HeightRequest="100" />
    
            <Grid ColumnDefinitions="*,*" ColumnSpacing="4">
                <Button Text="Save"
                        Clicked="SaveButton_Clicked" />
    
                <Button Grid.Column="1"
                        Text="Delete"
                        Clicked="DeleteButton_Clicked" />
            </Grid>
        </VerticalStackLayout>
    </ContentPage>
    

让我们看看上一个代码片段中突出显示的已更改行:

  • xmlns:models="clr-namespace:Notes.Models"

    此行将 Notes.Models .NET 命名空间映射到 models XML 命名空间。

  • x:DataType="models:Note"

    此行指示 XAML 编译器编译所有绑定表达式以提高运行时性能,并针对 Notes.Models.Note 该类型解析绑定表达式。

  • Text="{Binding Text}"

    此行通过添加<Editor>属性,并将该属性绑定到Text属性,来修改Text控件。

修改代码隐藏比 XAML 复杂得多。 当前代码是在构造函数中加载文件内容,然后直接将其设置为 TextEditor.Text 属性。 目前的代码如下:

public NotePage()
{
    InitializeComponent();

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

创建新的 LoadNote 方法,而不是在构造函数中加载注释。 该方法操作步骤如下:

  • 接受文件名参数。
  • 创建新的笔记模型并设置文件名。
  • 如果文件存在,请将其内容加载到模型中。
  • 如果文件存在,请使用创建文件的日期更新模型。
  • 将页面的 BindingContext 设置为模型。
  1. “解决方案资源管理器”窗格中,打开 Views\NotePage.xaml.cs 文件。

  2. 将下列方法添加到类:

    private void LoadNote(string fileName)
    {
        Models.Note noteModel = new Models.Note();
        noteModel.Filename = fileName;
    
        if (File.Exists(fileName))
        {
            noteModel.Date = File.GetCreationTime(fileName);
            noteModel.Text = File.ReadAllText(fileName);
        }
    
        BindingContext = noteModel;
    }
    
  3. 更新类构造函数,调用 LoadNote。 笔记的文件名应为随机生成的名称,并创建在应用的本地数据目录中。

    public NotePage()
    {
        InitializeComponent();
    
        string appDataPath = FileSystem.AppDataDirectory;
        string randomFileName = $"{Path.GetRandomFileName()}.notes.txt";
    
        LoadNote(Path.Combine(appDataPath, randomFileName));
    }
    

添加列出所有笔记的视图和模型

本教程的此部分添加了应用的最后一部分,该视图显示了之前创建的所有笔记。

多个笔记和导航

当前,笔记视图显示单个笔记。 如果要显示多个笔记,请创建新视图和模型:AllNotes

  1. “解决方案资源管理器”窗格中,右键单击Views文件夹,然后选择“添加新>
  2. “添加新项”对话框中,在窗口左侧的模板列表中选择 .NET MAUI。 接下来,选择 .NET MAUI ContentPage (XAML) 模板。 将文件命名为 AllNotesPage.xaml ,然后选择“添加”
  3. “解决方案资源管理器”窗格中,右键单击Models文件夹并选择“添加>
  4. 将类命名为 AllNotes.cs ,然后按“添加”

编码 AllNotes 模型

新模型将表示显示多个笔记所需的数据。 该数据将是表示笔记集合的属性。 该集合将是 ObservableCollection,指专用集合。 当列出多个项的控件(例如 ListView)绑定到 ObservableCollection 时,两者协同工作,自动使项列表与集合保持同步。 如果列表添加项,则会更新集合。 如果集合添加项,则控件会自动更新为新项。

  1. “解决方案资源管理器”窗格中,打开 Models\AllNotes.cs 文件。

  2. 将所有代码替换为以下片段:

    using System.Collections.ObjectModel;
    
    namespace Notes.Models;
    
    internal class AllNotes
    {
        public ObservableCollection<Note> Notes { get; set; } = new ObservableCollection<Note>();
    
        public AllNotes() =>
            LoadNotes();
    
        public void LoadNotes()
        {
            Notes.Clear();
    
            // Get the folder where the notes are stored.
            string appDataPath = FileSystem.AppDataDirectory;
    
            // Use Linq extensions to load the *.notes.txt files.
            IEnumerable<Note> notes = Directory
    
                // Select the file names from the directory
                .EnumerateFiles(appDataPath, "*.notes.txt")
    
                // Each file name is used to create a new Note
                .Select(filename => new Note()
                {
                    Filename = filename,
                    Text = File.ReadAllText(filename),
                    Date = File.GetLastWriteTime(filename)
                })
    
                // With the final collection of notes, order them by date
                .OrderBy(note => note.Date);
    
            // Add each note into the ObservableCollection
            foreach (Note note in notes)
                Notes.Add(note);
        }
    }
    

前述代码声明名为 Notes 的集合,并使用 LoadNotes 方法从设备加载笔记。 该方法使用 LINQ 扩展将数据加载、转换和排序到 Notes 集合中。

设计 AllNotes 页

接下来,需要设计视图支持 AllNotes 模型。

  1. “解决方案资源管理器”窗格中,打开 Views\AllNotesPage.xaml 文件。

  2. 用下列标记替换代码:

    <?xml version="1.0" encoding="utf-8" ?>
    <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
                 xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                 xmlns:models="clr-namespace:Notes.Models"
                 x:Class="Notes.Views.AllNotesPage"
                 Title="Your Notes"
                 x:DataType="models:AllNotes">
        <!-- Add an item to the toolbar -->
        <ContentPage.ToolbarItems>
            <ToolbarItem Text="Add" Clicked="Add_Clicked" IconImageSource="{FontImage Glyph='+', Color=Black, Size=22}" />
        </ContentPage.ToolbarItems>
    
        <!-- Display notes in a list -->
        <CollectionView x:Name="notesCollection"
                            ItemsSource="{Binding Notes}"
                            Margin="20"
                            SelectionMode="Single"
                            SelectionChanged="notesCollection_SelectionChanged">
    
            <!-- Designate how the collection of items are laid out -->
            <CollectionView.ItemsLayout>
                <LinearItemsLayout Orientation="Vertical" ItemSpacing="10" />
            </CollectionView.ItemsLayout>
    
            <!-- Define the appearance of each item in the list -->
            <CollectionView.ItemTemplate>
                <DataTemplate x:DataType="models:Note">
                    <StackLayout>
                        <Label Text="{Binding Text}" FontSize="22"/>
                        <Label Text="{Binding Date}" FontSize="14" TextColor="Silver"/>
                    </StackLayout>
                </DataTemplate>
            </CollectionView.ItemTemplate>
        </CollectionView>
    </ContentPage>
    

前述 XAML 介绍一些新概念:

  • ContentPage.ToolbarItems 属性包含 ToolbarItem。 此处定义的按钮通常显示在应用顶部,和页面标题一起。 不过,根据平台的不同,它可能处于不同的位置。 按下其中一个按钮将引发 Clicked 事件,就像普通按钮一样。

    ToolbarItem.IconImageSource 属性设置要在按钮上显示的图标。 该图标可以是项目定义的任何图像资源,但在此示例中使用 FontImageFontImage 可以使用字体中的单个字形作为图像。

  • CollectionView 控件显示项的集合,在本例中,绑定到模型的 Notes 属性。 集合视图显示每个项的方式可通过 CollectionView.ItemsLayoutCollectionView.ItemTemplate 属性设置。

    对于集合中的每个项,CollectionView.ItemTemplate 将生成声明的 XAML。 该 XAML 的 BindingContext 将成为集合项本身,在本例中为每个单独的笔记。 笔记的模板使用两个标签,这些标签绑定到笔记的 TextDate 属性。

  • CollectionView 处理 SelectionChanged 事件,该事件在选择集合视图中的项时引发。

需要写入视图的代码隐藏以加载笔记和处理事件。

  1. “解决方案资源管理器”窗格中,打开 Views/AllNotesPage.xaml.cs 文件。

  2. 将所有代码替换为以下片段:

    namespace Notes.Views;
    
    public partial class AllNotesPage : ContentPage
    {
        public AllNotesPage()
        {
            InitializeComponent();
    
            BindingContext = new Models.AllNotes();
        }
    
        protected override void OnAppearing()
        {
            ((Models.AllNotes)BindingContext).LoadNotes();
        }
    
        private async void Add_Clicked(object sender, EventArgs e)
        {
            await Shell.Current.GoToAsync(nameof(NotePage));
        }
    
        private async void notesCollection_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if (e.CurrentSelection.Count != 0)
            {
                // Get the note model
                var note = (Models.Note)e.CurrentSelection[0];
    
                // Should navigate to "NotePage?ItemId=path\on\device\XYZ.notes.txt"
                await Shell.Current.GoToAsync($"{nameof(NotePage)}?{nameof(NotePage.ItemId)}={note.Filename}");
    
                // Unselect the UI
                notesCollection.SelectedItem = null;
            }
        }
    }
    

此代码使用构造函数将页面的 BindingContext 设置为模型。

已在基类中重写 OnAppearing 方法。 每当显示页面时(例如导航到页面时),都会自动调用此方法。 此处的代码告知模型加载笔记。 由于 AllNotes 视图CollectionView中的 已绑定到 AllNotes 模型Notes 属性(即 ObservableCollection),因此,只要加载注释,CollectionView 就会自动更新。

Add_Clicked 处理程序引入了另一个新概念,即导航。 由于应用使用的是 .NET MAUI Shell,因此可以通过调用 Shell.Current.GoToAsync 方法导航到页面。 请注意,处理程序使用 async 关键字进行声明,这允许在导航时使用 await 关键字。 此处理程序导航到 NotePage

上一个代码片段中的最后一段代码是 notesCollection_SelectionChanged 处理程序。 此方法采用当前选定的项(即 Note 模型),并使用其信息导航到 NotePageGoToAsync 使用 URI 字符串进行导航。 在这种情况下,将构造一个字符串,该字符串使用查询字符串参数在目标页上设置属性。 表示 URI 的内插字符串最终看起来类似于以下字符串:

NotePage?ItemId=path\on\device\XYZ.notes.txt

ItemId= 参数设置为存储笔记的设备上的文件名。

Visual Studio 可能指示 NotePage.ItemId 属性不存在,而实际上它确实不存在。 下一步是修改 Note 视图 以基于要创建的 ItemId 参数加载模型。

查询字符串参数

Note 视图需要支持查询字符串参数 ItemId。 立即创建:

  1. “解决方案资源管理器”窗格中,打开 Views/NotePage.xaml.cs 文件。

  2. QueryProperty 特性添加到 class 关键字,提供查询字符串属性的名称及其分别映射到其上的类属性 ItemIdItemId

    [QueryProperty(nameof(ItemId), nameof(ItemId))]
    public partial class NotePage : ContentPage
    
  3. 添加名为 string 的新 ItemId 属性。 此属性调用 LoadNote 方法,传递属性的值,而该值又应为笔记的文件名:

    public string ItemId
    {
        set { LoadNote(value); }
    }
    
  4. SaveButton_ClickedDeleteButton_Clicked 处理程序替换为以下代码:

    private async void SaveButton_Clicked(object sender, EventArgs e)
    {
        if (BindingContext is Models.Note note)
            File.WriteAllText(note.Filename, TextEditor.Text);
    
        await Shell.Current.GoToAsync("..");
    }
    
    private async void DeleteButton_Clicked(object sender, EventArgs e)
    {
        if (BindingContext is Models.Note note)
        {
            // Delete the file.
            if (File.Exists(note.Filename))
                File.Delete(note.Filename);
        }
    
        await Shell.Current.GoToAsync("..");
    }
    

    按钮现在为 async。 按下它们后,页面将使用 .. 的 URI 导航回上一页。

  5. 从代码顶部删除 _fileName 变量,因为该类不再使用它。

修改应用的可视化树

AppShell 仍在加载单个笔记页,但需要加载的反而是 AllPages 视图。 打开 AppShell.xaml 文件,并将第一个 ShellContent 条目更改为指向 AllNotesPage 而不是 NotePage

<?xml version="1.0" encoding="UTF-8" ?>
<Shell
    x:Class="Notes.AppShell"
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:views="clr-namespace:Notes.Views"
    Shell.FlyoutBehavior="Disabled">

    <TabBar>
        <ShellContent
            Title="Notes"
            ContentTemplate="{DataTemplate views:AllNotesPage}"
            Icon="{OnPlatform 'icon_notes.png', iOS='icon_notes_ios.png', MacCatalyst='icon_notes_ios.png'}" />

        <ShellContent
            Title="About"
            ContentTemplate="{DataTemplate views:AboutPage}"
            Icon="{OnPlatform 'icon_about.png', iOS='icon_about_ios.png', MacCatalyst='icon_about_ios.png'}" />
    </TabBar>

</Shell>

如果现在运行该应用,你会注意到,按“添加”按钮时应用会崩溃,同时报告无法导航到 NotesPage 错误。 可以从其他页面进行导航的每个页面都需要向导航系统注册。 AllNotesPageAboutPage 页面通过在 TabBar 中声明自动注册到导航系统。

NotesPage 注册到导航系统:

  1. “解决方案资源管理器”窗格中,打开 AppShell.xaml.cs 文件。

  2. 向注册导航路由的构造函数添加一行:

    namespace Notes;
    
    public partial class AppShell : Shell
    {
        public AppShell()
        {
            InitializeComponent();
    
            Routing.RegisterRoute(nameof(Views.NotePage), typeof(Views.NotePage));
        }
    }
    

Routing.RegisterRoute 方法采用以下两种参数:

  • 第一个参数是要注册的 URI 的字符串名称,在本例中解析的名称为 "NotePage"
  • 第二个参数是导航到 "NotePage" 时要加载的页面类型。

现在可以运行应用了。 尝试添加新笔记、在笔记之间来回导航以及删除笔记。

浏览代码。 浏览本教程的代码。。 如果要下载已完成项目的副本以与代码进行比较,请下载此项目

你已经完成“创建 .NET MAUI 应用”教程!

后续步骤

在下一教程中,你将了解如何在项目中实现 model-view-viewmodel (MVVM) 模式。

以下链接提供了与本教程中学到的一些概念相关的详细信息: