创建 .NET MAUI 应用
本教程系列旨在演示如何创建仅使用跨平台代码的 .NET 多平台应用 UI (.NET MAUI) 应用。 这意味着,你编写的代码不会特定于 Windows、Android、iOS 或 macOS。 你将创建的应用将是一个笔记记录应用,用户可以在其中创建、保存和加载多个笔记。
在本教程中,你将了解如何执行以下操作:
- 创建 .NET MAUI Shell 应用。
- 在所选平台上运行应用。
- 使用可扩展的应用程序标记语言 (XAML) 定义用户界面,并通过代码与 XAML 元素交互。
- 创建视图并将其绑定到数据。
- 使用导航可转到页面和从页面移动。
你将使用 Visual Studio 2022 创建一个应用程序,你可以使用该应用程序输入笔记并将其保存到设备存储。 最终的应用程序如下所示:
创建项目
在开始本教程之前,必须遵循 生成第一个应用一文。 创建项目时,请使用以下设置:
项目名称
此属性必须设置为
Notes
。 如果项目的名称不同,则从本教程中复制和粘贴的代码可能会导致生成错误。将解决方案和项目放在同一目录中
取消选中此设置。
选择目标设备
根据设计,.NET MAUI 应用可在多个操作系统和设备上运行。 你需要选择要用于测试和调试应用的目标。
在 Visual Studio 工具栏中,将“调试目标”设置为要用于调试和测试的设备。 以下步骤演示如何将 调试目标 设置为 Android:
- 选择 “调试目标 ”下拉按钮。
- 选择 Android Emulators 项。
- 选择仿真器设备。
自定义应用 shell
当 Visual Studio 创建 .NET MAUI 项目时,将生成四个重要代码文件。 可以在 Visual Studio 的“解决方案资源管理器”窗格中看到这些内容:
这些文件有助于配置和运行 .NET MAUI 应用。 每个文件都有不同的用途,如下所述:
MauiProgram.cs
这是一个用于启动应用的代码文件。 此文件中的代码充当应用的跨平台入口点,用于配置和启动应用。 模板启动代码指向
App
App.xaml 文件定义的 类。App.xaml 和 App.xaml.cs
为了简单起见,这两个文件都称为单个文件。 通常有两个包含任何 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>
显示一个图像,在本例中,它使用dotnet_bot.png
每个 .NET MAUI 项目附带的图像。重要
添加到项目的文件实际上是
dotnet_bot.svg
。 .NET MAUI 将可缩放矢量图形 (SVG) 文件转换为可移植网络图形 (PNG) 文件。 因此,将 SVG 文件添加到 .NET MAUI 应用项目时,应从扩展名为 XAML 或 C#.png
引用该文件。 对 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
关键字 (keyword) 已添加到方法声明中,这允许在打开系统浏览器时使用await
关键字 (keyword) 。按 Ctrl+S 或选择菜单“文件保存 AboutPage.xaml.cs”>来保存文件。
现在,的 XAML 和代码隐藏 AboutPage
已完成,你需要在应用中显示它。
添加图像资源
某些控件可以使用图像,从而增强用户与应用交互的方式。 在本部分中,你将下载将在应用中使用的两个图像,以及两个用于 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。- 内的两
<ShellContent>
个<TabBar>
对象。 在替换模板代码之前,有一个<ShellContent>
指向页面的MainPage
对象。
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 ”。 接下来, (XAML) 模板选择 .NET MAUI ContentPage 。 将文件命名为 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
,从设备读取文件并将其内容存储在 控件的Text
属性中TextEditor
:public NotePage() { InitializeComponent(); if (File.Exists(_fileName)) TextEditor.Text = File.ReadAllText(_fileName); }
接下来,添加代码以处理
Clicked
XAML 中定义的事件: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
使用了 ShellContent.ContentTemplate
XML 命名空间,将其更改为 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 ,然后按 Add。
打开 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 ,然后按 Add。
打开 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 命名空间。BindingContext
的 ContentPage 属性设置为 类的Note.Models.About
实例,使用 的models:About
XML 命名空间和 对象。 这是使用 属性元素语法 而不是 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
模型。
在“解决方案资源管理器”窗格中,打开 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 ,然后按 Add。
编写 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.GetCreationTime(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=White, 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.ItemTemplate
属性设置CollectionView.ItemsLayout
集合视图呈现每个项的方式。对于集合中的每个项, 生成
CollectionView.ItemTemplate
声明的 XAML。BindingContext
该 XAML 的 将成为集合项本身,在本例中为每个单独的注释。 注释的模板使用两个标签,这些标签绑定到注释的Text
和Date
属性。处理 CollectionView 在
SelectionChanged
选择集合视图中的项时引发的事件。
需要编写视图的代码隐藏以加载笔记并处理事件。
在“解决方案资源管理器”窗格中,打开“视图/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
从基类重写。 每当显示页面时(例如导航到页面时),都会自动调用此方法。 此处的代码告知模型加载笔记。 CollectionView由于 AllNotes 视图中的 绑定到 AllNotes 模型的Notes
属性(即 ObservableCollection
),每当加载笔记时,都会CollectionView自动更新。
处理程序 Add_Clicked
引入了另一个新概念导航。 由于应用使用的是 .NET MAUI Shell,因此可以通过调用 Shell.Current.GoToAsync
方法导航到页面。 请注意,处理程序是使用 async
关键字 (keyword) 声明的await
,这允许在导航时使用关键字 (keyword) 。 此处理程序导航到 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
关键字 (keyword) ,分别提供查询字符串属性的名称及其映射到的类属性: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 应用教程!
后续步骤
本教程系列的下一部分介绍如何在项目中实现 model-view-viewmodel (MVVM) 模式。
以下链接提供了与本教程中学习的一些概念相关的详细信息:
你有关于此部分的问题? 如果有,请向我们提供反馈,以便我们对此部分作出改进。
反馈
提交和查看相关反馈