使用 MVVM 升级应用
本教程系列旨在继续学习 创建 .NET MAUI 应用 教程,该教程创建了记笔记应用。 在本系列的这一部分中,你将了解如何:
- Model-View-ViewModel (MVVM) 模式
- 使用其他样式的查询字符串在导航期间传递数据。
我们强烈建议你首先遵循“创建 .NET MAUI 应用”教程,因为在该教程中创建的代码是本教程的基础。 如果丢失了代码,或者想要重新开始,请下载 此项目。
了解 MVVM
.NET 开发人员体验通常涉及在 XAML 中创建用户界面,然后添加在用户界面上运行的代码隐藏。 随着应用的修改以及规模和范围的扩大,可能会出现复杂的维护问题。 这些问题包括 UI 控件和业务逻辑之间的紧密耦合,这增加了进行 UI 修改的成本,以及对此类代码进行单元测试的难度。
模型-视图-视图模型 (MVVM) 模式有助于将应用程序的业务和表示逻辑与用户界面 (UI) 清晰分离。 保持应用程序逻辑和 UI 之间的清晰分离有助于解决许多开发问题,并使应用程序更易于测试、维护和演变。 它还可以显著提高代码重用机会,并允许开发人员和 UI 设计人员在开发应用各自的部分时更轻松地进行协作。
模式。
MVVM 模式中有三个核心组件:模型、视图和视图模型。 每个组件的用途不同。 下图显示了这三个组件之间的关系。
除了要了解每个组件的责任外,了解它们如何交互也很重要。 在较高的层次上,视图“了解”视图模型,视图模型“了解”模型,但模型不知道视图模型,而视图模型不知道视图。 因此,视图模型将视图与模型隔离开来,并允许模型独立于视图进行演变。
有效使用 MVVM 的关键在于了解如何将应用代码分解为正确的类以及这些类的交互方式。
视图
视图负责定义用户在屏幕上看到的结构、布局和外观。 理想情况下,每个视图在 XAML 中定义,代码隐藏有限,不包含业务逻辑。 但是,在某些情况下,代码隐藏可能包含用于实现在 XAML 中难以表达的视觉行为的 UI 逻辑,例如动画。
视图模型
视图模型实现视图可以数据绑定到的属性和命令,并通过更改通知事件通知视图任何状态更改。 视图模型提供的属性和命令定义了要由 UI 提供的功能,但视图决定了如何显示该功能。
视图模型还负责协调视图与所需的任何模型类的交互。 视图模型与模型类之间通常存在一对多关系。
每个视图模型以一种视图可以轻松使用的形式提供来自模型的数据。 为此,视图模型有时会执行数据转换。 将此数据转换置于视图模型中是一个好主意,因为它提供视图可以绑定到的属性。 例如,视图模型可能会合并两个属性的值,以便于视图显示。
重要说明
.NET MAUI 封送对 UI 线程的绑定更新。 使用 MVVM 时,可以使用 .NET MAUI 的绑定引擎将更新 UI 线程中的数据绑定 viewmodel 属性。
模型
模型类是封装应用数据的非可视类。 因此,可以将模型视为表示应用的域模型,该模型通常包括数据模型以及业务和验证逻辑。
更新模型
在本教程的第一部分,你将实现 model-view-viewmodel (MVVM) 模式。 首先,在 Visual Studio 中打开 Notes.sln 解决方案。
清理模型
在上一教程中,模型类型既充当模型(数据),又充当视图模型(数据准备),该模型直接映射到视图。 下表对这些规则进行说明。
代码文件 | 说明 |
---|---|
Models/About.cs | About 模型 包含描述应用本身的只读字段,例如应用标题和版本。 |
Models/Note.cs | Note 模型 表示一个角色。 |
Models/AllNotes.cs | AllNotes 模型 将设备上的所有笔记加载到集合中。 |
考虑应用本身时,应用只使用一条数据。Note
笔记从设备加载,保存到设备,并通过应用 UI 进行编辑。 真的不需要 About
模型 AllNotes
。 从项目中删除这些模型:
- 在 Visual Studio 中,打开“解决方案资源管理器”窗格。
- 右键单击 Models\About.cs 文件,然后选择“ 删除”。 按 “确定” 删除文件。
- 右键单击 Models\AllNotes.cs 文件,然后选择“ 删除”。 按 “确定” 删除文件。
剩余的唯一模型文件是 Models\Note.cs 文件。
更新模型
Note
包含模型 ID。
- 唯一标识符,它是存储在设备上的笔记的文件名。
- 文本大小。
- 用于指示何时创建或上次更新笔记的日期。
目前,加载和保存模型是通过视图完成的,在某些情况下,由刚刚删除的其他模型类型完成。 类型所需的 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
模型完成后,可以创建视图模型。
创建“关于”视图
在向项目添加视图模型之前,请添加对 MVVM 社区工具包的引用。 此库在 NuGet 上可用,并提供有助于实现 MVVM 模式的类型和系统。
在 Visual Studio 的“解决方案资源管理器”中,右键单击“笔记”项目>,然后选择“管理 NuGet 包”。
选择“浏览”选项卡。
搜索 communitytoolkit mvvm 并选择应为第一个结果的
CommunityToolkit.Mvvm
包。确保至少选择了版本 8。 本教程是使用版本 8.0.0 编写的。
接下来,选择“ 安装 ”并接受显示的任何提示。
现在,你已准备好通过添加视图模型开始更新项目。
与视图模型分离
视图到 viewmodel 关系在很大程度上依赖于 .NET 多平台应用 UI (.NET MAUI) 提供的绑定系统。 应用已在视图中使用绑定来显示笔记列表,并显示单个笔记的文本和日期。 应用逻辑当前由视图的代码隐藏提供,并直接绑定到视图。 例如,当用户编辑笔记并按 “保存”按钮时,Clicked
将引发该按钮的事件。 然后,事件处理程序的代码隐藏会将笔记文本保存到文件中,并导航到上一屏幕。
视图更改时,在视图的代码隐藏中具有应用逻辑可能会成为问题。 例如,如果按钮替换为其他输入控件,或者更改了控件的名称,则事件处理程序可能会变得无效。 无论视图的设计方式如何,视图的用途都是调用某种应用逻辑并向用户显示信息。 对于此应用, Save
按钮将保存笔记,然后导航回上一个屏幕。
viewmodel 为应用提供了一个特定位置,用于放置应用逻辑,而不考虑 UI 的设计方式或数据的加载或保存方式。 viewmodel 是代表视图表示数据模型并与数据模型交互的粘附。
视图模型存储在 ViewModels 文件夹中。
- 在 Visual Studio 中,打开“解决方案资源管理器”窗格。
- 在笔记项目上右键单击,选择添加>新建文件夹。 命名文件夹为ViewModels。
- 右键单击 ViewModels 文件夹>“添加>类”并将其命名为 AboutViewModel.cs。
- 重复上一步,再创建两个视图模型:
- NoteViewModel.cs
- NotesViewModel.cs
项目结构应如下图所示:
关于 viewmodel 和关于视图
“ 关于”视图 在屏幕上显示一些数据,还可以选择导航到包含详细信息的网站。 由于此视图没有任何要更改的数据,例如文本输入控件或从列表中选择项,因此最好演示如何添加 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
.NET 命名空间并面向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"> <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
viewmodel 中的属性。
在此视图中,当用户按下按钮时,将调用该 Command
按钮。 绑定到 Command
ShowMoreInfoCommand
viewmodel 中的属性,并在调用时在方法中 ShowMoreInfo
运行代码,该方法将 Web 浏览器打开到特定页面。
清理 About 代码隐藏
该 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(); } }
创建 Note viewmodel
更新 注释视图 的目标是将尽可能多的功能移出 XAML 代码隐藏,并将其放入 Note viewmodel 中。
注意 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
viewmodel,你将在其中添加属性和命令以支持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); }
这两个构造函数用于使用新的后盾模型(即空注释)创建 viewmodel,或者创建使用指定模型实例的 viewmodel。
构造函数还设置 viewmodel 的命令。 接下来,添加这些命令的代码。
添加
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
该方法。 此 viewmodel 用作注释视图的绑定上下文。 导航到 “注释”视图 时,视图的绑定上下文(此 viewmodel)将传递导航期间使用的查询字符串参数。此代码在字典中
query
提供密钥时load
检查。 如果找到此键,该值应为要加载的笔记的标识符(文件名)。 该注释将加载并设置为此 viewmodel 实例的基础模型对象。最后,将这两个帮助程序方法添加到类:
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文件中,应用以下更改:
xmlns:viewModels
添加面向 .NET 命名空间的Notes.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">
<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 viewmodel
最终的 viewmodel-view 对是 Notes viewmodel 和 AllNotes 视图。 但目前,视图直接绑定到在本教程开始时删除的模型。 更新 AllNotes 视图 的目标是将尽可能多的功能移出 XAML 代码隐藏,并将其放入 viewmodel 中。 同样,视图可以更改其设计的好处,对代码的影响不大。
Notes viewmodel
根据 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 { }
此代码是用于添加属性和命令以支持视图的
AllNotes
空白NotesViewModel
代码。向
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 与此 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 文件中,应用以下更改:
xmlns:viewModels
添加面向 .NET 命名空间的Notes.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"> <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 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> <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 Source={RelativeSource Self}, Path=SelectedItem}">
但是,SelectionChangedCommandParameter
属性使用 Source={RelativeSource Self}
绑定。 引用 Self
当前对象,即 CollectionView
. 请注意,绑定路径是 SelectedItem
属性。 通过更改所选项调用命令时, SelectNoteCommand
将调用该命令,并将所选项作为参数传递给命令。
清理 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
使用该方法将索引 0 移动到matchedNote
列表顶部。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 处插入注释,而不是将其追加到列表末尾,而是在索引 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。
在 XAML 中
<ContentPage>
,添加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"> <ContentPage.BindingContext> <viewModels:NotesViewModel /> </ContentPage.BindingContext>
可以通过右键单击事件方法名称
ContentPage_NavigatedTo
并选择“ 转到定义”来添加默认事件处理程序。 此操作将在 代码编辑器中打开 Views\AllNotesPage.xaml.cs 。将 事件处理程序代码替换为以下内容:
private void ContentPage_NavigatedTo(object sender, NavigatedToEventArgs e) { notesCollection.SelectedItem = null; }
在 XAML 中,为
CollectionView
给定的名称notesCollection
。 此代码使用该名称访问CollectionView
和设置为SelectedItem
null
。 每次导航到页面时,都会清除所选项。
现在,运行应用。 尝试导航到笔记,按后退按钮,并再次选择相同的笔记。 应用行为已修复!
恭喜!
你的应用现在正在使用 MVVM 模式!
后续步骤
以下链接提供了与本教程中学到的一些概念相关的详细信息:
你有关于此部分的问题? 如果有,请向我们提供反馈,以便我们对此部分作出改进。