使用 MVVM 工具包来实现 MVVM

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

安装 MVVM 工具包 NuGet 包

需要在 WinUINotesWinUINotes.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 相关的结构。 在本教程中,模型类(NoteAllNotes)负责数据表示、业务逻辑和更新数据存储。 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]filenametextdate字段自动生成具有更改通知支持的公共属性(FilenameTextDate)。
  • [NotifyCanExecuteChangedFor]:此属性可确保当 FilenameText 发生变化时,关联的命令会重新评估它们是否可以执行。 例如,键入文本时,“保存”按钮将根据验证逻辑自动启用或禁用。
  • [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 中,更新 TextHeader 的绑定为使用 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);
                }
            }
        }
    }
    

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

在文档中了解详细信息: