使用 MVVM 工具包来实现 MVVM

现已准备好项目结构,可以使用 MVVM 工具包开始实现 MVVM 模式。 此步骤涉及创建利用 MVVM 工具包功能的 ViewModel,例如 ObservableObject 属性更改通知和 RelayCommand 命令实现。

安装 MVVM 工具包 NuGet 包

需要在 WinUINotes 和 WinUINotes.Bus 项目中安装 MVVM 工具包。

使用 Visual Studio

  1. 右键单击解决方案资源管理器中的 WinUINotes.Bus 项目。
  2. 选择“管理 NuGet 包”。
  3. 搜索 CommunityToolkit.Mvvm 并安装最新的稳定版本。
  4. 对 WinUINotes 项目重复这些步骤。

使用 .NET CLI

或者,你可以使用 .NET CLI 来安装该包:

dotnet add WinUINotes.Bus package CommunityToolkit.Mvvm
dotnet add WinUINotes package CommunityToolkit.Mvvm

模型层的设计决策

实现 MVVM 时,请务必决定如何构建模型类与 ViewModels 相关的结构。 在本教程中,模型类(Note 和 AllNotes)负责数据表示、业务逻辑和更新数据存储。 ViewModels 处理可观察属性、更改通知和用于 UI 交互的命令。

在更简单的实现中,可以将普通旧 CLR 对象(POCO)用于模型类,而无需任何业务逻辑或数据访问方法。 在这种情况下,ViewModels 通过服务层处理所有数据操作。 但是,在本教程中,模型类包括用于加载、保存和删除笔记的方法,以提供更清晰的关注点分离,并使 ViewModel 专注于呈现逻辑。

移动Note模型

将 Note 类移动到 WinUINotes.Bus 项目。 它仍然是一个简单的模型类,其中包含一些用于数据表示和状态管理的逻辑,但没有任何 MVVM 工具包功能。 ViewModel 处理可观察属性和更改通知,而不是模型本身。

  1. 在 WinUINotes.Bus 项目中,创建名为 Models 的新文件夹。

  2. 将 Note.cs 文件从 WinUINotes 项目移动到 WinUINotes.Bus/Models 文件夹。

  3. 更新命名空间以匹配新位置:

    namespace WinUINotes.Models
    {
        public class Note
        {
            // Existing code remains unchanged
            ...
        }
    }
    

该 Note 类是一个简单的数据模型。 它不需要更改通知,因为 ViewModels 管理可观察属性并通知 UI 更改。

移动 AllNotes 模型

将 AllNotes 类移动到 WinUINotes.Bus 项目。

  1. 将 AllNotes.cs 文件从 WinUINotes 项目移动到 WinUINotes.Bus/Models 文件夹。

  2. 更新命名空间以匹配新位置:

    namespace WinUINotes.Models
    {
        public class AllNotes
        {
            // Existing code remains unchanged
            ...
        }
    }
    

与类 Note 一样, AllNotes 是一个简单的模型类。 ViewModel 处理可观察的行为并管理笔记集合。

创建 AllNotesViewModel

  1. 在 WinUINotes.Bus 项目中,创建名为 ViewModels 的新文件夹。

  2. 添加包含以下内容的新 AllNotesViewModel.cs 类文件:

    using CommunityToolkit.Mvvm.ComponentModel;
    using CommunityToolkit.Mvvm.Input;
    using System.Collections.ObjectModel;
    using System.Threading.Tasks;
    using WinUINotes.Models;
    
    namespace WinUINotes.ViewModels
    {
        public partial class AllNotesViewModel : ObservableObject
        {
            private readonly AllNotes allNotes;
    
            [ObservableProperty]
            private ObservableCollection<Note> notes;
    
            public AllNotesViewModel()
            {
                allNotes = new AllNotes();
                notes = new ObservableCollection<Note>();
            }
    
            [RelayCommand]
            public async Task LoadAsync()
            {
                await allNotes.LoadNotes();
                Notes.Clear();
                foreach (var note in allNotes.Notes)
                {
                    Notes.Add(note);
                }
            }
        }
    }
    

AllNotesViewModel 管理 UI 中显示的笔记集合。

  • [ObservableProperty]:该 notes 字段自动生成具有更改通知的公共 Notes 属性。 集合 Notes 发生更改时,UI 会自动更新。
  • allNotes 模型:此专用字段包含模型实例 AllNotes ,用于处理实际数据作。
  • [RelayCommand]:此属性从LoadCommand方法生成属性LoadAsync(),允许 UI 通过数据绑定触发加载作。
  • LoadAsync() 方法:此方法从模型加载笔记,清除当前可观测集合,并使用加载的笔记填充它。 此模式可确保 UI 绑定集合与基础数据保持同步。

模型(数据操作)与Notes可观测集合(UI 绑定)之间的allNotes分离是一个关键的 MVVM 模式,它保持关注点分离,并使 View 与 ViewModel 的数据保持同步。

在文档中了解详细信息:

创建 NoteViewModel

  1. 在 ViewModels 文件夹中,添加一个新的类文件,命名为 NoteViewModel.cs:

    using CommunityToolkit.Mvvm.ComponentModel;
    using CommunityToolkit.Mvvm.Input;
    using System;
    using System.Threading.Tasks;
    using WinUINotes.Models;
    
    namespace WinUINotes.ViewModels
    {
        public partial class NoteViewModel : ObservableObject
        {
            private Note note;
    
            [ObservableProperty]
            [NotifyCanExecuteChangedFor(nameof(SaveCommand))]
            [NotifyCanExecuteChangedFor(nameof(DeleteCommand))]
            private string filename = string.Empty;
    
            [ObservableProperty]
            [NotifyCanExecuteChangedFor(nameof(SaveCommand))]
            private string text = string.Empty;
    
            [ObservableProperty]
            private DateTime date = DateTime.Now;
    
            public NoteViewModel()
            {
                this.note = new Note();
                this.Filename = note.Filename;
            }
    
            public void InitializeForExistingNote(Note note)
            {
                this.note = note;
                this.Filename = note.Filename;
                this.Text = note.Text;
                this.Date = note.Date;
            }
    
            [RelayCommand(CanExecute = nameof(CanSave))]
            private async Task Save()
            {
                note.Filename = this.Filename;
                note.Text = this.Text;
                note.Date = this.Date;
                await note.SaveAsync();
    
                // Check if the DeleteCommand can now execute
                // (it can if the file now exists)
                DeleteCommand.NotifyCanExecuteChanged();
            }
    
            private bool CanSave()
            {
                return note is not null
                    && !string.IsNullOrWhiteSpace(this.Text)
                    && !string.IsNullOrWhiteSpace(this.Filename);
            }
    
            [RelayCommand(CanExecute = nameof(CanDelete))]
            private async Task Delete()
            {
                await note.DeleteAsync();
                note = new Note();
            }
    
            private bool CanDelete()
            {
                // Note: This is to illustrate how commands can be
                // enabled or disabled.
                // In a real application, you shouldn't perform
                // file operations in your CanExecute logic.
                return note is not null
                    && !string.IsNullOrWhiteSpace(this.Filename)
                    && this.note.NoteFileExists();
            }
        }
    }
    

NoteViewModel 演示了几个 MVVM 工具包的关键功能:

  • [ObservableProperty]:filename、text和date字段自动生成具有更改通知支持的公共属性(Filename、Text、Date)。
  • [NotifyCanExecuteChangedFor]:此属性可确保当 Filename 或 Text 发生变化时,关联的命令会重新评估它们是否可以执行。 例如,键入文本时,“保存”按钮将根据验证逻辑自动启用或禁用。
  • [RelayCommand(CanExecute = nameof(CanSave))]:此属性生成一个 SaveCommand 属性,该属性绑定到验证方法 CanSave()。 仅当两者 Text 都具有 Filename 值时,才启用该命令。
  • InitializeForExistingNote():此方法将现有笔记的数据加载到 ViewModel 属性中,然后通过数据绑定更新 UI。
  • 保存逻辑:该方法将当前属性值更新到基础Note模型,并在模型上调用SaveAsync()方法。 保存后,它会通知 DeleteCommand 它应重新评估(因为文件现在存在,可以删除)。
  • 删除逻辑:该方法 Delete() 对笔记模型调用 DeleteAsync() 并创建新的空笔记。

在本教程稍后的部分,您将集成文件服务以处理实际文件操作,并使用 MVVM 工具包的 WeakReferenceMessenger 类在删除笔记时通知应用的其他部分,同时保持低耦合。

在文档中了解详细信息:

更新视图以使用视图模型

现在,需要更新 XAML 页面以绑定到新的 ViewModel。

更新 AllNotesPage 视图

  1. 在AllNotesPage.xamlItemsSource中,更新ItemsView的绑定,以使用 ViewModel 的Notes属性:

    <ItemsView ItemsSource="{x:Bind viewModel.Notes}"
    ...
    
  2. 更新 AllNotesPage.xaml.cs 文件,如下所示:

    using Microsoft.UI.Xaml;
    using Microsoft.UI.Xaml.Controls;
    using Microsoft.UI.Xaml.Navigation;
    using WinUINotes.ViewModels;
    
    namespace WinUINotes.Views
    {
        public sealed partial class AllNotesPage : Page
        {
            private AllNotesViewModel? viewModel;
    
            public AllNotesPage()
            {
                this.InitializeComponent();
                viewModel = new AllNotesViewModel();
            }
    
            private void NewNoteButton_Click(object sender, RoutedEventArgs e)
            {
                Frame.Navigate(typeof(NotePage));
            }
    
            private void ItemsView_ItemInvoked(ItemsView sender, ItemsViewItemInvokedEventArgs args)
            {
                Frame.Navigate(typeof(NotePage), args.InvokedItem);
            }
    
            protected override async void OnNavigatedTo(NavigationEventArgs e)
            {
                base.OnNavigatedTo(e);
    
                if (viewModel is not null)
                {
                    await viewModel.LoadAsync();
                }
            }
        }
    }
    

在此代码隐藏文件中,构造函数直接实例化 AllNotesViewModel 。 该方法 OnNavigatedTo() 在导航到该页面时在 ViewModel 上调用 LoadAsync() 该方法。 此方法从存储加载笔记并更新可观察集合。 此模式可确保用户在导航到所有备注页时始终刷新数据。

本教程稍后将重构此代码以使用依赖项注入,从而允许将 ViewModel 注入到页面构造函数中,而不是直接创建。 此方法可提高可测试性,并更轻松地管理 ViewModel 生命周期。

更新 NotePage 视图

  1. 在 NotePage.xaml 中,更新 Text 和 Header 的绑定为使用 ViewModel 的属性。 请将StackPanel按钮更新为绑定到命令,而不是使用Click事件:

    ...
    <TextBox x:Name="NoteEditor"
             Text="{x:Bind noteVm.Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
             AcceptsReturn="True"
             TextWrapping="Wrap"
             PlaceholderText="Enter your note"
             Header="{x:Bind noteVm.Date.ToString()}"
             ScrollViewer.VerticalScrollBarVisibility="Auto"
             MaxWidth="400"
             Grid.Column="1"/>
    
    <StackPanel Orientation="Horizontal"
                HorizontalAlignment="Right"
                Spacing="4"
                Grid.Row="1" Grid.Column="1">
        <Button Content="Save" Command="{x:Bind noteVm.SaveCommand}"/>
        <Button Content="Delete" Command="{x:Bind noteVm.DeleteCommand}"/>
    </StackPanel>
    ...
    

    还可以在UpdateSourceTrigger绑定上设置TextBox.Text,以确保更改随着用户输入被发送到 ViewModel。 此设置允许 Save 该按钮根据输入实时启用或禁用。

  2. 在 NotePage.xaml.cs中,更新代码以使用 NoteViewModel:

    using Microsoft.UI.Xaml.Controls;
    using Microsoft.UI.Xaml.Navigation;
    using WinUINotes.Models;
    using WinUINotes.ViewModels;
    
    namespace WinUINotes.Views
    {
        public sealed partial class NotePage : Page
        {
            private NoteViewModel? noteVm;
    
            public NotePage()
            {
                this.InitializeComponent();
            }
    
            protected override void OnNavigatedTo(NavigationEventArgs e)
            {
                base.OnNavigatedTo(e);
                noteVm = new NoteViewModel();
    
                if (e.Parameter is Note note && noteVm is not null)
                {
                    noteVm.InitializeForExistingNote(note);
                }
            }
        }
    }
    

    Click 和 Save 的事件已被移除,因为这些按钮现在直接绑定到 ViewModel 中的命令。 NoteViewModel 在方法 OnNavigatedTo() 中被实例化。 如果传递参数 Note ,它将使用现有笔记数据初始化 ViewModel。

在文档中了解详细信息: