创建 .NET MAUI 应用
本教程系列旨在演示如何创建仅使用跨平台代码的 .NET Multi-platform App UI (.NET MAUI) 应用。 这意味着,你编写的代码不会特定于 Windows、Android、iOS 或 macOS。 你将创建的应用是记笔记应用,用户可以在其中创建、保存和加载多个笔记。
本教程介绍如何执行下列操作:
- 创建 .NET MAUI shell 应用。
- 在所选平台上运行应用。
- 使用可扩展应用程序标记语言 (XAML) 定义用户界面,并通过代码与 XAML 元素进行交互。
- 创建视图并将其绑定到数据。
- 使用导航在页面之间移动。
你将使用 Visual Studio 2022 创建应用程序,可以使用它输入笔记并将其保存到设备存储中。 最终的应用程序如下所示:
创建项目
在开始本教程之前,必须遵循构建第一个应用文章中的操作。 创建项目时,请使用以下设置:
项目名称
此属性必须设置为
Notes
。 如果项目的名称不同,则从本教程复制和粘贴的代码可能会导致生成错误。将解决方案和项目放在同一目录中
取消选中此设置。
创建项目时,请选择最新的 .NET Framework。
选择目标设备
根据设计,.NET MAUI 应用可在多个操作系统和设备上运行。 你需要选择要用于测试和调试应用的目标。
在 Visual Studio 工具栏中,将“调试目标”设置为要用于调试和测试的设备。 以下步骤演示如何将“调试目标”设置为 Android:
- 选择“调试目标”下拉列表按钮。
- 选择“Android 仿真器”项。
- 选择仿真器设备。
自定义应用 shell
Visual Studio 创建 .NET MAUI 项目时,将生成四个重要的代码文件。 可以在 Visual Studio 的“解决方案资源管理器”窗格中看到这些文件:
这些文件有助于配置和运行 .NET MAUI 应用。 每个文件都有不同的用途,如下所述:
MauiProgram.cs
这是启动应用的代码文件。 此文件中的代码充当应用的跨平台入口点,用于配置和启动应用。 模板启动代码指向 App.xaml 文件定义的
App
类。App.xaml 和 App.xaml.cs
为了简单起见,这两个文件称为单个文件。 所有 XAML 文件通常都包含两个文件,即 .xaml 文件本身,以及一个相应的代码文件,该文件是“解决方案资源管理器”中的 .xaml 文件的子项。 .xaml 文件包含 XAML 标记,代码文件包含用户创建的用于与 XAML 标记交互的代码。
App.xaml 文件包含应用范围的 XAML 资源,例如颜色、样式或模板。 App.xaml.cs 文件通常包含用于实例化 Shell 应用程序的代码。 在此项目中,它指向
AppShell
类。AppShell.xaml 和 AppShell.xaml.cs
此文件定义
AppShell
类,该类用于定义应用的视觉层次结构。MainPage.xaml 和 MainPage.xaml.cs
这是应用显示的启动页。 MainPage.xaml 文件定义页面的 UI(用户界面)。 MainPage.xaml.cs 包含 XAML 的代码隐藏,如按钮单击事件的代码。
添加“关于”页
你将执行的第一个自定义操作是向项目添加另一个页面。 此页面是一个“关于”页面,它表示有关此应用的信息,例如作者、版本和可能提供更多详细信息的链接。
在 Visual Studio 的“解决方案资源管理器”窗格中,右键单击“笔记”“项目”>“添加”>“新建项...”。
在“添加新项”对话框中,在窗口左侧的模板列表中选择 .NET MAUI。 接下来,选择 .NET MAUI ContentPage (XAML) 模板。 将文件命名为 AboutPage.xaml,然后选择“添加”。
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>
通过按 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
事件添加代码。
在 Visual Studio 的“解决方案资源管理器”窗格中,展开 AboutPage.xaml 文件以显示其代码隐藏文件 AboutPage.xaml.cs。 然后,双击 AboutPage.xaml.cs 文件,在代码编辑器中打开该文件。
添加以下
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
关键字。通过按 Ctrl+S 或选择菜单“文件”>“保存 AboutPage.xaml.cs”来保存文件。
现在,AboutPage
的XAML 和代码隐藏已完成,需要将其显示在应用中。
添加图像资源
某些控件可以使用图像,从而增强用户与应用交互的方式。 在本部分中,将下载要在应用中使用的两个图像,以及两个用于 iOS 的备用图像。
下载以下图像:
下载图像后,可以使用文件资源管理器将其移动到项目的 Resources\Images 文件夹。 此文件夹中的任何文件都会作为 MauiImage 资源自动包含在项目中。 还可以使用 Visual Studio 将图像添加到项目中。 如果手动移动图像,请跳过以下过程。
重要说明
请勿跳过下载特定于 iOS 的图像的步骤,它们是完成本教程所必需的。
使用 Visual Studio 移动图像
在 Visual Studio 的“解决方案资源管理器”窗格中,展开“资源”文件夹,其中显示了“图像”文件夹。
提示
可以使用文件资源管理器将图像直接拖放到“图像”文件夹顶部的“解决方案资源管理器”窗格中。 这会自动将文件移动到文件夹,并将其包含在项目中。 如果选择拖放文件,请忽略此过程的其余部分。
右键单击“图像”,然后选择“添加”>“现有项...”。
导航到包含已下载图像的文件夹。
将文件类型筛选器更改为“图像文件”。
按住Ctrl 并单击下载的每个图像,然后按下“添加”
修改应用 Shell
如本文开头所述,AppShell
类定义了应用的视觉对象层次结构,即用于创建应用 UI 的 XAML 标记。 更新 XAML 以添加 TabBar 控件:
在“解决方案资源管理器”窗格中双击 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>
通过按下 Ctrl+S 或选择“文件”>“保存 AppShell.xaml”菜单保存文件。
让我们分解 XAML 的关键部分:
<Shell>
是 XAML 标记的根对象。<TabBar>
是 Shell 的内容。<TabBar>
内的两个<ShellContent>
对象。 在替换模板代码之前,存在一个指向MainPage
页的<ShellContent>
对象。
TabBar
及其子项不表示任何用户界面元素,而是表示应用的视觉对象层次结构的组织。 Shell 会采用这些对象并生成内容的用户界面,顶部有表示每个页面的栏。 每个页面的 ShellContent.Icon
属性使用特殊语法:{OnPlatform ...}
。 当为每个平台编译 XAML 页面时,会处理此语法,并且可以使用它为每个平台指定属性值。 在这种情况下,每个平台默认使用 icon_about.png
图标,但 iOS 和 MacCatalyst 将使用 icon_about_ios.png
。
每个 <ShellContent>
对象都指向要显示的页面。 这是由 ContentTemplate
属性设置的。
运行应用
通过按下 F5 或按下 Visual Studio 顶部的“播放”按钮运行应用:
将显示两个选项卡:“笔记”和“关于”。 按下“关于”选项卡,应用导航至已创建的 AboutPage
。 按下“了解详细信息...”按钮打开 Web 浏览器。
关闭应用并返回到 Visual Studio。 如果使用的是 Android 模拟器,请在虚拟设备中终止应用,或按下位于 Visual Studio 顶部的停止按钮:
为笔记创建页面
既然应用包含 MainPage
与 AboutPage
,那么可以开始创建应用的其余部分。 首先,你将创建一个允许用户创建和显示笔记的页面,然后编写代码以加载和保存笔记。
“笔记”页将显示笔记,你可以将其保存或删除。 首先,将新页面添加至项目:
在 Visual Studio 的“解决方案资源管理器”窗格中,右键单击“笔记”“项目”>“添加”>“新建项...”。
在“添加新项”对话框中,在窗口左侧的模板列表中选择 .NET MAUI。 接下来,选择 .NET MAUI ContentPage (XAML) 模板。 为 NotePage.xaml 文件命名,然后选择“添加”。
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>
通过按 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 代码隐藏文件,以处理加载和保存笔记。
创建笔记后,它将作为文本文件保存到设备。 文件的名称由
_fileName
变量表示。 将以下string
变量声明添加到NotePage
类:public partial class NotePage : ContentPage { string _fileName = Path.Combine(FileSystem.AppDataDirectory, "notes.txt");
上述代码会构造文件的路径,将其存储在应用的本地数据目录中。 文件名为 notes.txt。
在类的构造函数中,调用
InitializeComponent
方法后,从设备读取文件并将其内容存储在TextEditor
控件的Text
属性中:public NotePage() { InitializeComponent(); if (File.Exists(_fileName)) TextEditor.Text = File.ReadAllText(_fileName); }
接下来,添加代码以处理 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 控件的文本。通过按 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>
保存文件并运行应用。 尝试在输入框中键入,然后按“保存”按钮。 关闭应用,然后重新打开。 输入的笔记应从设备的存储中加载。
将数据绑定到 UI 并导航页面
本教程的此部分介绍视图、模型和应用内导航的概念。
在本教程的前面步骤中,你向项目添加了两个页面:NotePage
和 AboutPage
。 页面表示数据的视图。 NotePage
是显示“笔记数据”的“视图”,AboutPage
是显示“应用信息数据”的“视图”。这两个视图都具有硬编码或嵌入其中的数据模型,需要将数据模型与视图分开。
将模型与视图分离有什么好处? 它允许你设计视图来表示模型的任何部分并与之交互,而无需担心实现模型的实际代码。 可以使用数据绑定来达成此目的,本教程稍后将介绍这些内容。 不过,现在让我们重新构建项目。
分离视图和模型
重构现有代码以将模型与视图分开。 接下来的几个步骤将组织代码,以便分别定义视图和模型。
从不再需要 MainPage.xaml 和 MainPage.xaml.cs 的项目中删除这两者。 在“解决方案资源管理器”窗格中,找到 MainPage.xaml 的条目,右键单击它并选择“删除”。
提示
删除 MainPage.xaml 项还应删除 MainPage.xaml.cs 项。 如果未删除 MainPage.xaml.cs,请右键单击它并选择“删除”。
右键单击 Notes 项目,然后选择“添加”>“新建文件夹”。 将该文件夹命名为 Models。
右键单击 Notes 项目,然后选择“添加”>“新建文件夹”。 将该文件夹命名为 Views。
查找“NotePage.xaml”项并将其拖动到 Views 文件夹。 NotePage.xaml.cs 应随其移动。
重要说明
移动文件时,Visual Studio 通常会发出有关移动操作可能需要很长时间的警告进行提示。 如果看到此警告,则表示没有任何问题,请按“确定”。
Visual Studio 还可能会询问是否要调整已移动文件的命名空间。 选择“否”,因为后续步骤将更改命名空间。
查找“AboutPage.xaml”项并将其拖动到 Views 文件夹。 AboutPage.xaml.cs 应随其移动。
更新视图命名空间
将视图移动到 Views 文件夹后,需要更新命名空间才能匹配。 页面的 XAML 和代码隐藏文件的命名空间设置为 Notes
。 需要将其更新为 Notes.Views
。
在“解决方案资源管理器”窗格中,展开“NotePage.xaml”和“AboutPage.xaml”以显示代码隐藏文件:
双击“NotePage.xaml.cs”项以打开代码编辑器。 将命名空间更改为
Notes.Views
:namespace Notes.Views;
对 AboutPage.xaml.cs 项目重复前面的步骤。
双击“NotePage.xaml”项以打开 XAML 编辑器。 旧命名空间通过
x:Class
特性引用,该特性定义哪个类类型是 XAML 的代码隐藏。 此条目不仅仅是命名空间,而是具有该类型的命名空间。 将x:Class
值更改为Notes.Views.NotePage
:x:Class="Notes.Views.NotePage"
对 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>
现在,你应该能够运行应用且不会出现任何编译器错误,并且所有内容都应像以前一样正常运行。
定义模型
目前,模型是嵌入在“笔记”和“关于”视图中的数据。 我们将创建新类来表示该数据。 第一步是创建用于表示笔记页面数据的模型:
在“解决方案资源管理器”窗格中,右键单击 Models 文件夹,然后选择“添加”>“类...”。
将类命名为 Note.cs,然后按“添加”。
打开 Note.cs,将代码替换为以下片段:
namespace Notes.Models; internal class Note { public string Filename { get; set; } public string Text { get; set; } public DateTime Date { get; set; } }
保存文件。
接下来,创建“关于”页面的模型:
在“解决方案资源管理器”窗格中,右键单击 Models 文件夹,然后选择“添加”>“类...”。
将类命名为 About.cs,然后按“添加”。
打开 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."; }
保存文件。
更新“关于”页面
“关于”页面将是更新最快的页面,你将能够运行应用并查看它如何从模型加载数据。
在“解决方案资源管理器”窗格中,打开 Views\AboutPage.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" xmlns:models="clr-namespace:Notes.Models" x:Class="Notes.Views.AboutPage"> <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 命名空间。使用
models:About
的 XML 命名空间和对象,将 ContentPage 的BindingContext
属性设置为Note.Models.About
类的实例。 这是使用属性元素语法,而不是 XML 特性设置的。重要说明
到目前为止,已使用 XML 特性设置属性。 这非常适用于简单值,例如
Label.FontSize
属性。 但是,如果属性值更为复杂,则必须使用属性元素语法来创建对象。 请参考以下示例,创建一个标签并设置其FontSize
属性:<Label FontSize="22" />
可以使用属性元素语法设置相同的
FontSize
属性:<Label> <Label.FontSize> 22 </Label.FontSize> </Label>
三个
<Label>
控件的Text
属性值已从硬编码字符串更改为绑定语法:{Binding PATH}
。在运行时处理
{Binding}
语法,从而允许从绑定返回的值是动态值。{Binding PATH}
的PATH
部分是要与其绑定的属性路径。 该属性来自当前控件的BindingContext
。 使用<Label>
控件时,会取消设置BindingContext
。 上下文在控件取消设置时从父级继承,在本例中,设置上下文的父对象是根对象:ContentPageBindingContext
中的对象是About
模型的实例。 其中一个标签的绑定路径将Label.Text
属性绑定到About.Title
属性。
对“关于”页面的最后一个更改是更新打开网页的按钮单击。 URL 已硬编码在代码隐藏中,但 URL 应来自 BindingContext
属性中的模型。
在“解决方案资源管理器”窗格中,打开 Views\AboutPage.xaml.cs 文件。
将
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); } }
如果查看突出显示的行,代码将检查类型是否 BindingContext
为 Models.About
类型,如果是,则将其 about
分配给变量。 if
语句内的下一行打开浏览器,访问 about.MoreInfoUrl
属性提供的 URL。
运行该应用后会发现它运行方式与之前完全相同。 尝试更改模型的值,并查看浏览器打开的 UI 和 URL 如何也发生更改的。
更新笔记页
上一部分将 about 页面视图绑定到 about 模型,现在同样可以将 note 视图绑定到 note 模型。 不过,在这种情况下,模型不会在 XAML 中创建,而会在后续几个步骤中由代码隐藏创建。
在“解决方案资源管理器”窗格中,打开 Views\NotePage.xaml 文件。
更改
<Editor>
控件,添加Text
属性。 将属性绑定到Text
属性:<Editor ... Text="{Binding Text}"
:<?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.Views.NotePage" Title="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>
修改代码隐藏比 XAML 复杂得多。 当前代码是在构造函数中加载文件内容,然后直接将其设置为 TextEditor.Text
属性。 目前的代码如下:
public NotePage()
{
InitializeComponent();
if (File.Exists(_fileName))
TextEditor.Text = File.ReadAllText(_fileName);
}
创建新的 LoadNote
方法,而不是在构造函数中加载注释。 该方法操作步骤如下:
- 接受文件名参数。
- 创建新的笔记模型并设置文件名。
- 如果文件存在,请将其内容加载到模型中。
- 如果文件存在,请使用创建文件的日期更新模型。
- 将页面的
BindingContext
设置为模型。
在“解决方案资源管理器”窗格中,打开 Views\NotePage.xaml.cs 文件。
将下列方法添加到类:
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; }
更新类构造函数,调用
LoadNote
。 笔记的文件名应为随机生成的名称,并创建在应用的本地数据目录中。public NotePage() { InitializeComponent(); string appDataPath = FileSystem.AppDataDirectory; string randomFileName = $"{Path.GetRandomFileName()}.notes.txt"; LoadNote(Path.Combine(appDataPath, randomFileName)); }
添加列出所有笔记的视图和模型
本教程的此部分添加了应用的最后一部分,该视图显示了之前创建的所有笔记。
多个笔记和导航
当前,笔记视图显示单个笔记。 如果要显示多个笔记,请创建新视图和模型:AllNotes 。
- 在“解决方案资源管理器”窗格中,右键单击 Views 文件夹并选择“添加”>新项...。
- 在“添加新项”对话框中,在窗口左侧的模板列表中选择 .NET MAUI。 接下来,选择 .NET MAUI ContentPage (XAML) 模板。 将文件命名为 AllNotesPage.xaml ,然后选择“添加”。
- 在“解决方案资源管理器”窗格中,右键单击 Models 文件夹并选择“添加”>类...。
- 将类命名为 AllNotes.cs ,然后按“添加”。
编码 AllNotes 模型
新模型将表示显示多个笔记所需的数据。 该数据将是表示笔记集合的属性。 该集合将是 ObservableCollection
,指专用集合。 当列出多个项的控件(例如 ListView)绑定到 ObservableCollection
时,两者协同工作,自动使项列表与集合保持同步。 如果列表添加项,则会更新集合。 如果集合添加项,则控件会自动更新为新项。
在“解决方案资源管理器”窗格中,打开 Models\AllNotes.cs 文件。
将所有代码替换为以下片段:
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 模型。
在“解决方案资源管理器”窗格中,打开 Views\AllNotesPage.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.Views.AllNotesPage" Title="Your Notes"> <!-- 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> <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
属性设置要在按钮上显示的图标。 该图标可以是项目定义的任何图像资源,但在此示例中使用FontImage
。FontImage
可以使用字体中的单个字形作为图像。CollectionView 控件显示项的集合,在本例中,绑定到模型的
Notes
属性。 集合视图显示每个项的方式可通过CollectionView.ItemsLayout
和CollectionView.ItemTemplate
属性设置。对于集合中的每个项,
CollectionView.ItemTemplate
将生成声明的 XAML。 该 XAML 的BindingContext
将成为集合项本身,在本例中为每个单独的笔记。 笔记的模板使用两个标签,这些标签绑定到笔记的Text
和Date
属性。CollectionView 处理
SelectionChanged
事件,该事件在选择集合视图中的项时引发。
需要写入视图的代码隐藏以加载笔记和处理事件。
在“解决方案资源管理器”窗格中,打开 Views/AllNotesPage.xaml.cs 文件。
将所有代码替换为以下片段:
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 模型),并使用其信息导航到 NotePage
。 GoToAsync 使用 URI 字符串进行导航。 在这种情况下,将构造一个字符串,该字符串使用查询字符串参数在目标页上设置属性。 表示 URI 的内插字符串最终看起来类似于以下字符串:
NotePage?ItemId=path\on\device\XYZ.notes.txt
ItemId=
参数设置为存储笔记的设备上的文件名。
Visual Studio 可能指示 NotePage.ItemId
属性不存在,但事实并非如此。 下一步是修改 Note 视图 以基于要创建的 ItemId
参数加载模型。
查询字符串参数
Note 视图需要支持查询字符串参数 ItemId
。 立即创建:
在“解决方案资源管理器”窗格中,打开 Views/NotePage.xaml.cs 文件。
将
QueryProperty
特性添加到class
关键字,提供查询字符串属性的名称及其分别映射到其上的类属性ItemId
和ItemId
:[QueryProperty(nameof(ItemId), nameof(ItemId))] public partial class NotePage : ContentPage
添加名为
ItemId
的新string
属性。 此属性调用LoadNote
方法,传递属性的值,而该值又应为笔记的文件名:public string ItemId { set { LoadNote(value); } }
将
SaveButton_Clicked
和DeleteButton_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 导航回上一页。从代码顶部删除
_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
错误。 可以从其他页面进行导航的每个页面都需要向导航系统注册。 AllNotesPage
和 AboutPage
页面通过在 TabBar 中声明自动注册到导航系统。
将 NotesPage
注册到导航系统:
在“解决方案资源管理器”窗格中,打开 AppShell.xaml.cs 文件。
向注册导航路由的构造函数添加一行:
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 应用”教程!
后续步骤
在本教程系列接下来的部分中,你将了解如何在项目中实现模型-视图-视图模型 (MVVM) 模式。
以下链接提供了与本教程中学到的一些概念相关的详细信息:
你有关于此部分的问题? 如果有,请向我们提供反馈,以便我们对此部分作出改进。