本教程旨在继续 学习创建 .NET MAUI 应用 教程,该教程创建了记笔记应用。 本教程介绍以下操作:
- 实现 model-view-viewmodel (MVVM) 模式。
- 使用其他样式的查询字符串在导航期间传递数据。
建议首先按照 “创建 .NET MAUI 应用 ”教程进行作,因为在该教程中创建的代码是本教程的基础。 如果丢失了代码,或者想要重新开始,请下载 此项目。
了解 MVVM
.NET MAUI 开发人员体验通常包括在 XAML 中创建用户界面,然后添加操作用户界面的后台代码。 随着应用被修改并扩大大小和范围,可能会出现复杂的维护问题。 这些问题包括 UI 控件与业务逻辑之间的紧密耦合,这会增加进行 UI 修改的成本,以及单元测试此类代码的难度。
model-view-viewmodel (MVVM) 模式有助于将应用的业务逻辑和演示逻辑与其用户界面(UI)清晰地分离。 在应用逻辑和 UI 之间保持干净分离有助于解决许多开发问题,并使应用更易于测试、维护和改进。 它还可以显著提高代码重用机会,并允许开发人员和 UI 设计人员在开发应用各自的部分时更轻松地进行协作。
模式
MVVM 模式中有三个核心组件:模型、视图和视图模型。 每个服务都有不同的用途。 下图显示了三个组件之间的关系。
除了了解每个组件的责任外,了解它们如何交互也很重要。 在高级别上,视图“了解”视图模型,视图模型“了解”模型,但模型不知道视图模型,视图模型不知道视图。 因此,视图模型将视图与模型隔离开来,并允许模型独立于视图发展。
有效使用 MVVM 的关键在于了解如何将应用代码分解成正确的类以及类的交互方式。
查看
该视图负责定义用户在屏幕上看到的内容的结构、布局和外观。 理想情况下,每个视图在 XAML 中定义,代码隐藏有限,不包含业务逻辑。 但是,在某些情况下,代码隐藏可能包含实现在 XAML 中难以表达的视觉行为的 UI 逻辑,例如动画。
视图模型 (ViewModel)
视图模型实现供视图数据绑定的属性和命令,并通过更改通知事件来通知视图任何状态更改。 视图模型提供的属性和命令定义 UI 提供的功能,但视图决定如何显示该功能。
视图模型还负责协调视图与所需的任何模型类的交互。 视图模型和模型类之间通常存在一对多关系。
每个视图模型都以视图可以轻松使用的形式提供模型中的数据。 为此,视图模型有时会执行数据转换。 将此数据转换放在视图模型中是个好主意,因为它提供视图可以绑定到的属性。 例如,视图模型可以将两个属性的值合并,以便视图更容易显示。
重要
.NET MAUI 封送对 UI 线程的绑定更新。 在使用 MVVM 时,这使您能够从任何线程更新数据绑定的视图模型属性,.NET MAUI 的绑定引擎会将更新带到 UI 线程上。
型号
模型类是封装应用数据的非视觉类。 因此,可以将模型视为表示应用的域模型,该模型通常包括数据模型以及业务和验证逻辑。
更新模型
在本教程的第一部分中,您将实施模型-视图-视图模型(MVVM)模式。 若要开始,请在 Visual Studio 中打开 Notes.sln 解决方案。
清理模型
在上一教程中,模型类型既充当模型(数据),又充当视图模型(数据准备),该模型直接映射到视图。 下表描述了模型:
代码文件 | DESCRIPTION |
---|---|
Models/About.cs | 模型 About 。 包含描述应用本身的只读字段,例如应用标题和版本。 |
Models/Note.cs | 模型 Note 。 表示注释。 |
模型/AllNotes.cs | 模型 AllNotes 。 将设备上的所有笔记加载到集合中。 |
考虑应用本身时,应用只使用一条数据,Note
。 笔记从设备加载,保存到设备,并通过应用 UI 进行编辑。 的确不需要 About
和 AllNotes
模型。 从项目中删除这些模型:
- 找到 Visual Studio 解决方案资源管理器 窗格。
- 右键单击 Models\About.cs 文件,然后选择“ 删除”。 按 “确定” 删除文件。
- 右键单击 Models\AllNotes.cs 文件,然后选择“ 删除”。 按 “确定” 删除文件。
剩余的唯一模型文件是 Models\Note.cs 文件。
更新模型
模型 Note
包含:
- 唯一标识符,它是存储在设备上的笔记的文件名。
- 注释的文本。
- 用于指示何时创建或上次更新笔记的日期。
目前,加载和保存模型是通过视图完成的,在某些情况下,由刚刚删除的其他模型类型完成。 类型为 Note
时,代码应为以下内容:
namespace Notes.Models;
internal class Note
{
public string Filename { get; set; }
public string Text { get; set; }
public DateTime Date { get; set; }
}
将 Note
扩展模型以处理加载、保存和删除笔记。
在 Visual Studio 的解决方案资源管理器 窗格中,双击 Models\Note.cs。
在代码编辑器中,将以下两种方法添加到
Note
类。 这些方法基于实例,分别处理保存或删除设备中的当前笔记:public void Save() => File.WriteAllText(System.IO.Path.Combine(FileSystem.AppDataDirectory, Filename), Text); public void Delete() => File.Delete(System.IO.Path.Combine(FileSystem.AppDataDirectory, Filename));
应用需要以两种方式加载笔记,从文件加载单个笔记,并在设备上加载所有笔记。 要处理加载的代码可以是
static
成员,不需要类实例运行。将以下代码添加到类,以按文件名加载注释:
public static Note Load(string filename) { filename = System.IO.Path.Combine(FileSystem.AppDataDirectory, filename); if (!File.Exists(filename)) throw new FileNotFoundException("Unable to find file on local storage.", filename); return new() { Filename = Path.GetFileName(filename), Text = File.ReadAllText(filename), Date = File.GetLastWriteTime(filename) }; }
此代码采用文件名作为参数,生成存储在设备上的笔记的路径,并尝试加载该文件(如果存在)。
加载笔记的第二种方法是枚举设备上的所有笔记并将其加载到集合中。
将以下代码添加到 类:
public static IEnumerable<Note> LoadAll() { // Get the folder where the notes are stored. string appDataPath = FileSystem.AppDataDirectory; // Use Linq extensions to load the *.notes.txt files. return Directory // Select the file names from the directory .EnumerateFiles(appDataPath, "*.notes.txt") // Each file name is used to load a note .Select(filename => Note.Load(Path.GetFileName(filename))) // With the final collection of notes, order them by date .OrderByDescending(note => note.Date); }
此代码通过检索与备注文件模式匹配的设备上的文件来返回模型类型的可枚举集合
Note
: *.notes.txt。 每个文件名都会传递给Load
方法,并加载单个笔记。 最后,笔记集合按每个笔记的日期排序,并返回到调用方。最后,向类添加构造函数,该类设置属性的默认值,包括随机文件名:
public Note() { Filename = $"{Path.GetRandomFileName()}.notes.txt"; Date = DateTime.Now; Text = ""; }
类 Note
代码应如下所示:
namespace Notes.Models;
internal class Note
{
public string Filename { get; set; }
public string Text { get; set; }
public DateTime Date { get; set; }
public Note()
{
Filename = $"{Path.GetRandomFileName()}.notes.txt";
Date = DateTime.Now;
Text = "";
}
public void Save() =>
File.WriteAllText(System.IO.Path.Combine(FileSystem.AppDataDirectory, Filename), Text);
public void Delete() =>
File.Delete(System.IO.Path.Combine(FileSystem.AppDataDirectory, Filename));
public static Note Load(string filename)
{
filename = System.IO.Path.Combine(FileSystem.AppDataDirectory, filename);
if (!File.Exists(filename))
throw new FileNotFoundException("Unable to find file on local storage.", filename);
return
new()
{
Filename = Path.GetFileName(filename),
Text = File.ReadAllText(filename),
Date = File.GetLastWriteTime(filename)
};
}
public static IEnumerable<Note> LoadAll()
{
// Get the folder where the notes are stored.
string appDataPath = FileSystem.AppDataDirectory;
// Use Linq extensions to load the *.notes.txt files.
return Directory
// Select the file names from the directory
.EnumerateFiles(appDataPath, "*.notes.txt")
// Each file name is used to load a note
.Select(filename => Note.Load(Path.GetFileName(filename)))
// With the final collection of notes, order them by date
.OrderByDescending(note => note.Date);
}
}
Note
模型完成后,可以创建视图模型。
创建 About 视图模型
在向项目添加视图模型之前,请添加对 MVVM 社区工具包的引用。 此库在 NuGet 上可用,并提供有助于实现 MVVM 模式的类型和系统。
在 Visual Studio 的 解决方案资源管理器 窗格中,右键单击 Notes 项目,然后选择>。
选择“浏览”选项卡。
搜索 communitytoolkit mvvm 并选择
CommunityToolkit.Mvvm
包,它应该是第一个结果。确保至少选择了版本 8。 本教程是使用版本 8.0.0 编写的。
接下来,选择“ 安装 ”并接受显示的任何提示。
现在,你已准备好通过添加视图模型开始更新项目。
与视图模型分离
视图到视图模型关系在很大程度上依赖于由 .NET 多平台应用 UI (.NET MAUI) 提供的绑定系统。 应用已在视图中使用绑定来显示笔记列表,并显示单个笔记的文本和日期。 应用程序逻辑当前由视图的后台代码提供,并直接关联到视图。 例如,当用户编辑笔记并按 “保存 ”按钮时, Clicked
将引发该按钮的事件。 然后,事件处理程序的后台代码将笔记文本保存到文件中,并返回上一屏幕。
当视图更改时,应用程序逻辑在视图后端代码中可能会成为一个问题。 例如,如果按钮替换为其他输入控件,或者更改了控件的名称,则事件处理程序可能会变得无效。 无论视图的设计方式如何,视图的用途都是调用某种应用逻辑并向用户显示信息。 对于这个应用程序,Save
按钮将保存笔记,然后返回到之前的屏幕。
viewmodel 为应用提供了一个特定位置,用于放置应用逻辑,而不考虑 UI 的设计方式或数据的加载或保存方式。 viewmodel 是连接视图和数据模型的纽带,代表视图与数据模型进行交互。
视图模型存储在 ViewModels 文件夹中。
- 找到 Visual Studio 解决方案资源管理器 窗格。
- 右键单击 “备注 ”项目,然后选择“ 添加新>文件夹”。 将文件夹命名为 ViewModels。
- 右键单击 ViewModels 文件夹 >“添加>类 ”并将其命名 为AboutViewModel.cs。
- 重复上一步,再创建两个视图模型:
- NoteViewModel.cs
- NotesViewModel.cs
项目结构应如下图所示:
关于 ViewModel 和视图的相关信息
关于视图在屏幕上显示一些数据,并可选导航至包含更多信息的网站。 因为此视图没有任何可以更改的数据,例如文本输入控件或从列表中选择项,因此它是一个很好的示例对象,用于演示如何添加视图模型。 对于 About viewmodel,没有后盾模型。
创建 About viewmodel:
在 Visual Studio 的解决方案资源管理器 窗格中,双击 ViewModels\AboutViewModel.cs。
粘贴以下代码。
using CommunityToolkit.Mvvm.Input; using System.Windows.Input; namespace Notes.ViewModels; internal class AboutViewModel { 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."; public ICommand ShowMoreInfoCommand { get; } public AboutViewModel() { ShowMoreInfoCommand = new AsyncRelayCommand(ShowMoreInfo); } async Task ShowMoreInfo() => await Launcher.Default.OpenAsync(MoreInfoUrl); }
前面的代码片段包含一些属性,这些属性表示有关应用的信息,例如名称和版本。 此代码片段与之前删除的 About 模型 完全相同。 但是,在这个viewmodel中引入了一个新的概念,即ShowMoreInfoCommand
命令属性。
命令是可以绑定的操作,可以调用代码,是放置应用程序逻辑的理想选择。 在此示例中,ShowMoreInfoCommand
指向ShowMoreInfo
方法,该方法将 Web 浏览器打开到特定页面。 在下一部分中,你将了解有关命令系统的详细信息。
关于视图
需要稍微更改 “关于”视图 ,以便将其与在上一部分创建的 viewmodel 挂钩。 在 Views\AboutPage.xaml 文件中,应用以下更改:
- 将
xmlns:models
XML 命名空间更新为xmlns:viewModels
,并将目标设为Notes.ViewModels
.NET 命名空间。 - 将
ContentPage.BindingContext
属性更改为 viewmodel 的新实例About
。 - 删除按钮的
Clicked
事件处理程序并使用Command
属性。
更新 关于页面:
在 Visual Studio 中的 解决方案资源管理器 窗格中,双击 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:viewModels="clr-namespace:Notes.ViewModels" x:Class="Notes.Views.AboutPage" x:DataType="viewModels:AboutViewModel"> <ContentPage.BindingContext> <viewModels:AboutViewModel /> </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..." Command="{Binding ShowMoreInfoCommand}" /> </VerticalStackLayout> </ContentPage>
前面的代码片段突出显示在此版本的视图中已更改的行。
请注意,按钮正在使用该 Command
属性。 许多控件具有一个Command
属性,该属性在用户与控件交互时调用。 与按钮一起使用时,当用户按下该按钮时会调用该命令,这类似于事件处理程序 Clicked
被调用的方式,只是你可以将 Command
绑定到视图模型中的属性。
在此视图中,当用户按下按钮时,将调用该 Command
按钮。
Command
被绑定到视图模型中的 ShowMoreInfoCommand
属性,当调用时,会运行 ShowMoreInfo
方法中的代码,该方法将打开 Web 浏览器到特定页面。
关于页面的后台代码进行清理
该 ShowMoreInfo
按钮不使用事件处理程序,因此 LearnMore_Clicked
应从 Views\AboutPage.xaml.cs 文件中删除代码。 删除该代码,类应仅包含构造函数:
在 Visual Studio 解决方案资源管理器窗格中,双击 Views\AboutPage.xaml.cs。
小窍门
可能需要展开 Views\AboutPage.xaml 以显示文件。
将所有代码替换为以下片段:
namespace Notes.Views; public partial class AboutPage : ContentPage { public AboutPage() { InitializeComponent(); } }
创建笔记视图模型
更新 注释视图 的目标是将尽可能多的功能移出 XAML 代码隐藏,并将其放入 Note viewmodel 中。
注意视图模型
根据 注释视图 需要的内容, Note viewmodel 需要提供以下项:
- 注释的文本。
- 创建笔记或上次更新笔记的日期/时间。
- 保存笔记的命令。
- 删除注释的命令。
创建 Note viewmodel:
在 Visual Studio 的解决方案资源管理器 窗格中,双击 ViewModels\NoteViewModel.cs。
将此文件中的代码替换为以下代码片段:
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.ComponentModel; using System.Windows.Input; namespace Notes.ViewModels; internal class NoteViewModel : ObservableObject, IQueryAttributable { private Models.Note _note; }
此代码是初始
Note
视图模型,你将在其中添加属性和命令以支持Note
视图。 请注意,CommunityToolkit.Mvvm.ComponentModel
正在导入命名空间。 此命名空间提供ObservableObject
作为基类使用。 你将在下一步中了解更多关于ObservableObject
的信息。 命名空间CommunityToolkit.Mvvm.Input
也是导入的。 此命名空间提供一些异步调用方法的命令类型。该
Models.Note
模型将存储为专用字段。 此类的属性和方法将使用此字段。将以下属性添加到类:
public string Text { get => _note.Text; set { if (_note.Text != value) { _note.Text = value; OnPropertyChanged(); } } } public DateTime Date => _note.Date; public string Identifier => _note.Filename;
属性
Date
Identifier
是简单属性,只需从模型检索相应的值。小窍门
对于属性,语法
=>
会创建一个只读属性,其中=>
右侧的语句必须计算为要返回的值。该
Text
属性首先检查所设置的值是否为其他值。 如果值不同,则该值将传递给模型的属性,并OnPropertyChanged
调用该方法。该方法
OnPropertyChanged
由ObservableObject
基类提供。 此方法使用调用代码的名称(在本例中为 Text 的属性名称)并引发ObservableObject.PropertyChanged
事件。 此事件向任何事件订阅者提供属性的名称。 .NET MAUI 提供的绑定系统可识别此事件,并更新 UI 中的任何相关绑定。 对于 Note viewmodel,当Text
属性发生更改时,将引发该事件,并且绑定到Text
该属性的任何 UI 元素都会收到属性更改的通知。将以下命令属性添加到类,这些命令是视图可以绑定到的命令:
public ICommand SaveCommand { get; private set; } public ICommand DeleteCommand { get; private set; }
将以下构造函数添加到类:
public NoteViewModel() { _note = new Models.Note(); SaveCommand = new AsyncRelayCommand(Save); DeleteCommand = new AsyncRelayCommand(Delete); } public NoteViewModel(Models.Note note) { _note = note; SaveCommand = new AsyncRelayCommand(Save); DeleteCommand = new AsyncRelayCommand(Delete); }
这两个构造函数用于创建一个包含新的基础模型(即一个空笔记)的视图模型,或创建一个使用指定模型实例的视图模型。
构造函数还设置了视图模型的指令。 接下来,添加这些命令的代码。
添加
Save
和Delete
方法:private async Task Save() { _note.Date = DateTime.Now; _note.Save(); await Shell.Current.GoToAsync($"..?saved={_note.Filename}"); } private async Task Delete() { _note.Delete(); await Shell.Current.GoToAsync($"..?deleted={_note.Filename}"); }
这些方法由关联的命令调用。 他们对模型执行相关操作,并使应用导航到上一页面。 在导航路径中添加了查询字符串参数到
..
,以指示执行的操作和笔记的唯一标识符。接下来,将
ApplyQueryAttributes
方法添加到满足接口要求的 IQueryAttributable 类:void IQueryAttributable.ApplyQueryAttributes(IDictionary<string, object> query) { if (query.ContainsKey("load")) { _note = Models.Note.Load(query["load"].ToString()); RefreshProperties(); } }
当页面或页面的绑定上下文实现此接口时,导航中使用的查询字符串参数将传递给
ApplyQueryAttributes
该方法。 此视图模型被用作注释视图的绑定上下文。 导航到 “注释”视图 时,视图的绑定上下文(即此视图模型)将被传递导航期间使用的查询字符串参数。此代码检查字典
load
中是否提供了query
键。 如果找到此键,该值应为要加载的笔记的标识符(文件名)。 该笔记被加载并设为此视图模型实例的基础模型对象。最后,将这两个帮助程序方法添加到类:
public void Reload() { _note = Models.Note.Load(_note.Filename); RefreshProperties(); } private void RefreshProperties() { OnPropertyChanged(nameof(Text)); OnPropertyChanged(nameof(Date)); }
Reload
方法是一个辅助方法,用于刷新底层模型对象,并从设备存储重新加载它。该方法
RefreshProperties
是另一个帮助程序方法,用于确保任何绑定到该对象的订阅者都会收到通知,Text
和Date
属性已更改。 由于基础模型(_note
字段)在导航期间加载笔记时更改,因此实际上不会将Text
属性Date
设置为新值。 由于这些属性未直接设置,因此不会通知附加到这些属性的任何绑定,因为OnPropertyChanged
不会为每个属性调用这些绑定。RefreshProperties
确保刷新对这些属性的绑定。
类的代码应类似于以下代码片段:
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.ComponentModel;
using System.Windows.Input;
namespace Notes.ViewModels;
internal class NoteViewModel : ObservableObject, IQueryAttributable
{
private Models.Note _note;
public string Text
{
get => _note.Text;
set
{
if (_note.Text != value)
{
_note.Text = value;
OnPropertyChanged();
}
}
}
public DateTime Date => _note.Date;
public string Identifier => _note.Filename;
public ICommand SaveCommand { get; private set; }
public ICommand DeleteCommand { get; private set; }
public NoteViewModel()
{
_note = new Models.Note();
SaveCommand = new AsyncRelayCommand(Save);
DeleteCommand = new AsyncRelayCommand(Delete);
}
public NoteViewModel(Models.Note note)
{
_note = note;
SaveCommand = new AsyncRelayCommand(Save);
DeleteCommand = new AsyncRelayCommand(Delete);
}
private async Task Save()
{
_note.Date = DateTime.Now;
_note.Save();
await Shell.Current.GoToAsync($"..?saved={_note.Filename}");
}
private async Task Delete()
{
_note.Delete();
await Shell.Current.GoToAsync($"..?deleted={_note.Filename}");
}
void IQueryAttributable.ApplyQueryAttributes(IDictionary<string, object> query)
{
if (query.ContainsKey("load"))
{
_note = Models.Note.Load(query["load"].ToString());
RefreshProperties();
}
}
public void Reload()
{
_note = Models.Note.Load(_note.Filename);
RefreshProperties();
}
private void RefreshProperties()
{
OnPropertyChanged(nameof(Text));
OnPropertyChanged(nameof(Date));
}
}
笔记视图
创建 viewmodel 后,请更新 “注释”视图。 在 Views\NotePage.xaml 文件中,应用以下更改:
- 添加一个定位于 .NET 命名空间的
xmlns:viewModels
XML 命名空间。 - 向页面添加
BindingContext
。 - 删除删除并保存按钮
Clicked
事件处理程序,并将其替换为命令。
更新 “注释”视图:
在 Visual Studio 的解决方案资源管理器 窗格中,双击 Views\NotePage.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" xmlns:viewModels="clr-namespace:Notes.ViewModels" x:Class="Notes.Views.NotePage" Title="Note" x:DataType="viewModels:NoteViewModel"> <ContentPage.BindingContext> <viewModels:NoteViewModel /> </ContentPage.BindingContext> <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" Command="{Binding SaveCommand}"/> <Button Grid.Column="1" Text="Delete" Command="{Binding DeleteCommand}"/> </Grid> </VerticalStackLayout> </ContentPage>
以前,此视图未声明绑定上下文,因为它由页面本身的后台代码提供。 直接在 XAML 中设置绑定上下文提供了两项内容:
在运行时,当导航到页面时,会显示空白笔记。 这是因为调用绑定上下文中的视图模型 (viewmodel) 的无参数构造函数。 如果你记得没错的话,Note viewmodel的无参数构造函数会创建一个空笔记。
XAML 编辑器中的 Intellisense 在开始键入
{Binding
语法后立即显示可用属性。 语法也会得到验证,并向你发出无效值的警报。 尝试将绑定语法SaveCommand
更改为Save123Command
。 如果将鼠标光标悬停在文本上,你会注意到会显示工具提示,通知你找不到 Save123Command 。 这条通知不会被视为错误,因为绑定是动态变化的,并且实际上只是一个小提示,可能会在你键入错误属性时有所帮助。如果将 SaveCommand 更改为其他值,请立即还原它。
清理笔记代码后置
现在,与视图的交互已从事件处理程序更改为命令,请打开 Views\NotePage.xaml.cs 文件,并将所有代码替换为仅包含构造函数的类:
在 Visual Studio 的解决方案资源管理器窗格中,双击Views\NotePage.xaml.cs。
小窍门
可能需要展开 Views\NotePage.xaml 以显示文件。
将所有代码替换为以下片段:
namespace Notes.Views; public partial class NotePage : ContentPage { public NotePage() { InitializeComponent(); } }
创建笔记视图模型
最终的视图模型-视图对是 Notes 视图模型 和 AllNotes 视图。 但目前,视图直接绑定到在本教程开始时删除的模型。 更新 AllNotes 视图 的目标是将尽可能多的功能移出 XAML 代码隐藏,并将其放入 viewmodel 中。 当视图能够更改其设计时,对代码的影响几乎为零,这样能带来设计灵活性的好处。
Notes 视图模型
根据 AllNotes 视图 将要显示的内容以及用户将进行的交互,Notes viewmodel 必须提供以下项:
- 笔记集合。
- 用于处理导航到笔记的命令。
- 用于创建新笔记的命令。
- 在创建、删除或更改笔记时更新笔记列表。
创建 Notes viewmodel:
在 Visual Studio 的解决方案资源管理器 窗格中,双击 ViewModels\NotesViewModel.cs。
将此文件中的代码替换为以下代码:
using CommunityToolkit.Mvvm.Input; using System.Collections.ObjectModel; using System.Windows.Input; namespace Notes.ViewModels; internal class NotesViewModel: IQueryAttributable { }
此代码是空白
NotesViewModel
,您将在此处添加属性和命令以支持AllNotes
视图。在
NotesViewModel
类代码中,添加以下属性:public ObservableCollection<ViewModels.NoteViewModel> AllNotes { get; } public ICommand NewCommand { get; } public ICommand SelectNoteCommand { get; }
该
AllNotes
属性是一个ObservableCollection
,用于存储从设备加载的所有笔记。 视图将使用这两个命令来触发创建笔记或选择现有笔记的动作。将无参数构造函数添加到类,该构造函数初始化命令并从模型加载注释:
public NotesViewModel() { AllNotes = new ObservableCollection<ViewModels.NoteViewModel>(Models.Note.LoadAll().Select(n => new NoteViewModel(n))); NewCommand = new AsyncRelayCommand(NewNoteAsync); SelectNoteCommand = new AsyncRelayCommand<ViewModels.NoteViewModel>(SelectNoteAsync); }
请注意,
AllNotes
集合使用Models.Note.LoadAll
方法将笔记填充到可观察集合中。LoadAll
方法将注释作为Models.Note
类型返回,但可观测集合是由ViewModels.NoteViewModel
类型组成的集合。 该代码使用Select
Linq 扩展从从LoadAll
返回的注释模型创建 viewmodel 实例。创建命令的目标方法:
private async Task NewNoteAsync() { await Shell.Current.GoToAsync(nameof(Views.NotePage)); } private async Task SelectNoteAsync(ViewModels.NoteViewModel note) { if (note != null) await Shell.Current.GoToAsync($"{nameof(Views.NotePage)}?load={note.Identifier}"); }
注意,
NewNoteAsync
方法不接受参数,而SelectNoteAsync
方法则接受参数。 命令可以选择具有在调用命令时提供的单个参数。 对于SelectNoteAsync
方法,参数表示所选的注释。最后,实现
IQueryAttributable.ApplyQueryAttributes
方法:void IQueryAttributable.ApplyQueryAttributes(IDictionary<string, object> query) { if (query.ContainsKey("deleted")) { string noteId = query["deleted"].ToString(); NoteViewModel matchedNote = AllNotes.Where((n) => n.Identifier == noteId).FirstOrDefault(); // If note exists, delete it if (matchedNote != null) AllNotes.Remove(matchedNote); } else if (query.ContainsKey("saved")) { string noteId = query["saved"].ToString(); NoteViewModel matchedNote = AllNotes.Where((n) => n.Identifier == noteId).FirstOrDefault(); // If note is found, update it if (matchedNote != null) matchedNote.Reload(); // If note isn't found, it's new; add it. else AllNotes.Add(new NoteViewModel(Note.Load(noteId))); } }
在上一教程步骤中创建的 Note viewmodel ,在保存或删除笔记时使用了导航。 viewmodel 导航回 AllNotes 视图,该视图与此 viewmodel 相关联。 此代码检测查询字符串是否包含
deleted
或saved
键。 键的值是笔记的唯一标识符。如果笔记被删除,则该笔记会通过提供的标识符在
AllNotes
集合中进行匹配并移除。有两个可能的原因笔记被保存。 该笔记刚刚创建或现有笔记已更改。 如果注释已经在
AllNotes
集合中,这是说明该注释已更新。 在这种情况下,集合中的笔记实例只需要进行刷新。 如果集合中缺少笔记,则它是一个新笔记,必须添加到集合中。
类的代码应类似于以下代码片段:
using CommunityToolkit.Mvvm.Input;
using Notes.Models;
using System.Collections.ObjectModel;
using System.Windows.Input;
namespace Notes.ViewModels;
internal class NotesViewModel : IQueryAttributable
{
public ObservableCollection<ViewModels.NoteViewModel> AllNotes { get; }
public ICommand NewCommand { get; }
public ICommand SelectNoteCommand { get; }
public NotesViewModel()
{
AllNotes = new ObservableCollection<ViewModels.NoteViewModel>(Models.Note.LoadAll().Select(n => new NoteViewModel(n)));
NewCommand = new AsyncRelayCommand(NewNoteAsync);
SelectNoteCommand = new AsyncRelayCommand<ViewModels.NoteViewModel>(SelectNoteAsync);
}
private async Task NewNoteAsync()
{
await Shell.Current.GoToAsync(nameof(Views.NotePage));
}
private async Task SelectNoteAsync(ViewModels.NoteViewModel note)
{
if (note != null)
await Shell.Current.GoToAsync($"{nameof(Views.NotePage)}?load={note.Identifier}");
}
void IQueryAttributable.ApplyQueryAttributes(IDictionary<string, object> query)
{
if (query.ContainsKey("deleted"))
{
string noteId = query["deleted"].ToString();
NoteViewModel matchedNote = AllNotes.Where((n) => n.Identifier == noteId).FirstOrDefault();
// If note exists, delete it
if (matchedNote != null)
AllNotes.Remove(matchedNote);
}
else if (query.ContainsKey("saved"))
{
string noteId = query["saved"].ToString();
NoteViewModel matchedNote = AllNotes.Where((n) => n.Identifier == noteId).FirstOrDefault();
// If note is found, update it
if (matchedNote != null)
matchedNote.Reload();
// If note isn't found, it's new; add it.
else
AllNotes.Add(new NoteViewModel(Note.Load(noteId)));
}
}
}
AllNotes 视图
现在已创建 viewmodel,请更新 AllNotes 视图 以指向 viewmodel 属性。 在 Views\AllNotesPage.xaml 文件中,应用以下更改:
- 添加一个定位于 .NET 命名空间的
xmlns:viewModels
XML 命名空间。 - 向页面添加
BindingContext
。 - 删除工具栏按钮
Clicked
的事件并使用Command
属性。 - 将
CollectionView
更改为将其ItemSource
绑定到AllNotes
。 - 将
CollectionView
更改为使用命令以响应所选项发生变化时的情况。
更新 AllNotes 视图:
在 Visual Studio 的解决方案资源管理器 窗格中,双击 Views\AllNotesPage.xaml。
粘贴以下代码。
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:viewModels="clr-namespace:Notes.ViewModels" x:Class="Notes.Views.AllNotesPage" Title="Your Notes" x:DataType="viewModels:NotesViewModel"> <ContentPage.BindingContext> <viewModels:NotesViewModel /> </ContentPage.BindingContext> <!-- Add an item to the toolbar --> <ContentPage.ToolbarItems> <ToolbarItem Text="Add" Command="{Binding NewCommand}" IconImageSource="{FontImage Glyph='+', Color=Black, Size=22}" /> </ContentPage.ToolbarItems> <!-- Display notes in a list --> <CollectionView x:Name="notesCollection" ItemsSource="{Binding AllNotes}" Margin="20" SelectionMode="Single" SelectionChangedCommand="{Binding SelectNoteCommand}" SelectionChangedCommandParameter="{Binding x:DataType='CollectionView', Source={RelativeSource Self}, Path=SelectedItem}"> <!-- 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="viewModels:NoteViewModel"> <StackLayout> <Label Text="{Binding Text}" FontSize="22"/> <Label Text="{Binding Date}" FontSize="14" TextColor="Silver"/> </StackLayout> </DataTemplate> </CollectionView.ItemTemplate> </CollectionView> </ContentPage>
工具栏不再使用 Clicked
事件,而是使用命令。
CollectionView
支持通过 SelectionChangedCommand
和 SelectionChangedCommandParameter
属性进行指令操作。 在更新的 XAML 中,SelectionChangedCommand
属性绑定到 viewmodel 的 SelectNoteCommand
,这意味着在所选项更改时将调用该命令。 调用命令时, SelectionChangedCommandParameter
属性值将传递给该命令。
查看用于CollectionView
的绑定。
<CollectionView x:Name="notesCollection"
ItemsSource="{Binding AllNotes}"
Margin="20"
SelectionMode="Single"
SelectionChangedCommand="{Binding SelectNoteCommand}"
SelectionChangedCommandParameter="{Binding x:DataType='CollectionView', Source={RelativeSource Self}, Path=SelectedItem}">
该 SelectionChangedCommandParameter
属性使用 Source={RelativeSource Self}
绑定。
Self
参照当前对象,即为 CollectionView
。 因此, x:DataType
指定 CollectionView
为已编译绑定的类型。 请注意,绑定路径是 SelectedItem
属性。 通过更改所选项调用命令时, SelectNoteCommand
将调用该命令,并将所选项作为参数传递给命令。
要使属性中 SelectionChangedCommandParameter
定义的绑定表达式进行编译,必须指示项目在指定属性的表达式中启用已编译的 Source
绑定。 为此,请编辑解决方案的项目文件并在 <MauiEnableXamlCBindingWithSourceCompilation>true</MauiEnableXamlCBindingWithSourceCompilation>
元素中添加 <PropertyGroup>
:
<PropertyGroup>
<MauiEnableXamlCBindingWithSourceCompilation>true</MauiEnableXamlCBindingWithSourceCompilation>
</PropertyGroup>
清理 AllNotes 后台代码
现在,与视图的交互已从事件处理程序更改为命令,请打开 Views\AllNotesPage.xaml.cs 文件,并将所有代码替换为仅包含构造函数的类:
在 Visual Studio 的解决方案资源管理器 窗格中,双击 Views\AllNotesPage.xaml.cs。
小窍门
可能需要展开 Views\AllNotesPage.xaml 以显示文件。
将所有代码替换为以下片段:
namespace Notes.Views; public partial class AllNotesPage : ContentPage { public AllNotesPage() { InitializeComponent(); } }
运行应用
现在可以运行应用,一切正常。 但是,应用的行为方式有两个问题:
- 如果选择一个笔记会打开编辑器,然后按保存,再尝试选择同一个笔记,结果不会成功。
- 每当笔记被更改或添加时,笔记列表不会重新排序,以显示最新笔记在顶部。
下一教程步骤中修复了这两个问题。
修复应用行为
现在,应用代码可以编译和运行,你可能已经注意到应用的行为有两个缺陷。 应用不允许你重新选择已选择的笔记,并且创建或更改笔记后不会重新排序笔记列表。
将笔记置于列表首位
首先,修复注释列表的重新排序问题。 在 ViewModels\NotesViewModel.cs 文件中, AllNotes
集合包含要向用户呈现的所有注释。 遗憾的是,使用某个 ObservableCollection
的缺点是必须手动进行排序。 若要将新项或更新的项目获取到列表顶部,请执行以下步骤:
在 Visual Studio 的解决方案资源管理器 窗格中,双击 ViewModels\NotesViewModel.cs。
在
ApplyQueryAttributes
方法中,查看 保存 的查询字符串键的逻辑。当
matchedNote
不是null
时,便笺正在更新。 使用AllNotes.Move
方法将matchedNote
移动到索引 0,即列表顶部。string noteId = query["saved"].ToString(); NoteViewModel matchedNote = AllNotes.Where((n) => n.Identifier == noteId).FirstOrDefault(); // If note is found, update it if (matchedNote != null) { matchedNote.Reload(); AllNotes.Move(AllNotes.IndexOf(matchedNote), 0); }
该方法
AllNotes.Move
采用两个参数来移动对象在集合中的位置。 第一个参数是要移动的对象索引,第二个参数是移动对象的位置的索引。 该方法AllNotes.IndexOf
检索注释的索引。当是
matchedNote
null
时,便笺是新的,正在添加到列表中。 不要将注释追加到列表末尾,而是将其插入到索引 0,即列表的顶部。 将AllNotes.Add
方法更改为AllNotes.Insert
.string noteId = query["saved"].ToString(); NoteViewModel matchedNote = AllNotes.Where((n) => n.Identifier == noteId).FirstOrDefault(); // If note is found, update it if (matchedNote != null) { matchedNote.Reload(); AllNotes.Move(AllNotes.IndexOf(matchedNote), 0); } // If note isn't found, it's new; add it. else AllNotes.Insert(0, new NoteViewModel(Models.Note.Load(noteId)));
该方法 ApplyQueryAttributes
应类似于以下代码片段:
void IQueryAttributable.ApplyQueryAttributes(IDictionary<string, object> query)
{
if (query.ContainsKey("deleted"))
{
string noteId = query["deleted"].ToString();
NoteViewModel matchedNote = AllNotes.Where((n) => n.Identifier == noteId).FirstOrDefault();
// If note exists, delete it
if (matchedNote != null)
AllNotes.Remove(matchedNote);
}
else if (query.ContainsKey("saved"))
{
string noteId = query["saved"].ToString();
NoteViewModel matchedNote = AllNotes.Where((n) => n.Identifier == noteId).FirstOrDefault();
// If note is found, update it
if (matchedNote != null)
{
matchedNote.Reload();
AllNotes.Move(AllNotes.IndexOf(matchedNote), 0);
}
// If note isn't found, it's new; add it.
else
AllNotes.Insert(0, new NoteViewModel(Models.Note.Load(noteId)));
}
}
允许选择笔记两次
在 AllNotes 视图中,列出 CollectionView
所有笔记,但不允许你选择同一笔记两次。 项目保持选中状态有两种情况:当用户更改现有笔记时,以及当用户强行向后导航时。 用户保存笔记的情况是使用上一部分中 AllNotes.Move
的代码更改修复的,因此无需担心这种情况。
现在必须解决的问题与导航相关。 无论如何访问Allnotes视图,都会为页面引发NavigatedTo
事件。 此事件是在 CollectionView
中强制取消选择已选项目的合适场所。
但是,在此处应用 MVVM 模式时,viewmodel 无法直接触发视图中的内容,例如保存笔记后清除所选项目。 那么,如何做到这一点? MVVM 模式的良好实现可最大程度地减少视图中的代码隐藏。 可通过几种不同的方法来解决此问题以支持 MVVM 分离模式。 但是,也可以将代码放入视图的后台代码文件中,尤其是在代码直接关联到视图时。 MVVM 具有许多出色的设计和概念,可帮助你隔离应用、提高可维护性并让你更轻松地添加新功能。 但是,在某些情况下,你可能会发现 MVVM 鼓励过度设计。
不要对这个问题的解决方案过度设计,只需使用NavigatedTo
事件从CollectionView
中清除所选项。
在 Visual Studio 的解决方案资源管理器 窗格中,双击 Views\AllNotesPage.xaml。
在
<ContentPage>
的 XAML 中,添加NavigatedTo
事件:<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:viewModels="clr-namespace:Notes.ViewModels" x:Class="Notes.Views.AllNotesPage" Title="Your Notes" NavigatedTo="ContentPage_NavigatedTo" x:DataType="viewModels:NotesViewModel"> <ContentPage.BindingContext> <viewModels:NotesViewModel />
可以通过右键单击事件方法名称
ContentPage_NavigatedTo
并选择 “转到定义”来添加默认事件处理程序。 此操作将在代码编辑器中打开 Views\AllNotesPage.xaml.cs。将事件处理程序代码替换为以下代码片段:
private void ContentPage_NavigatedTo(object sender, NavigatedToEventArgs e) { notesCollection.SelectedItem = null; }
在 XAML 中,
CollectionView
被赋予了名称notesCollection
。 此代码使用该名称访问CollectionView
,并将SelectedItem
设置为null
。 每次导航到页面时,所选项都会被清除。
现在,运行应用。 尝试导航到笔记,按后退按钮,并再次选择相同的笔记。 应用行为已修复!
浏览本教程的代码。。 如果要下载已完成项目的副本以与代码进行比较,请下载此项目。
你的应用现在正在使用 MVVM 模式!
后续步骤
以下链接提供了与本教程中学到的一些概念相关的详细信息: