使用 MVVM 工具組實作 MVVM

現在您已準備好專案結構,您可以使用 MVVM 工具組開始實作 MVVM 模式。 此步驟牽涉到建立利用 MVVM 工具組功能的檢視模型,例如 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 時,請務必決定如何建構與 ViewModel 相關的模型類別。 在本教學課程中,模型類別 (Note 和 AllNotes) 負責資料表示、商業邏輯和更新資料儲存。 ViewModel 會處理可觀察的屬性、變更通知,以及 UI 互動的命令。

在更簡單的實作中,您可以針對模型類別使用純舊的 CLR 物件 (POCO),而不需要任何商務邏輯或資料存取方法。 在此情況下,ViewModel 會透過服務層處理所有資料作業。 不過,在本教學課程中,模型類別包含載入、儲存及刪除附註的方法,以提供更清楚的關注點區隔,並讓 ViewModel 專注於呈現邏輯。

移動註記模型

將類別移至 NoteWinUINotes.Bus 專案。 它仍然是簡單的模型類別,具有一些資料表示法和狀態管理的邏輯,但沒有任何 MVVM 工具組功能。 ViewModel 會處理可觀察的屬性和變更通知,而不是模型本身。

  1. 在 WinUINotes.Bus 專案中,建立名為 Models 的新資料夾。

  2. 將檔案從 Note.cs 專案移至 WinUINotes.Bus/Models 資料夾。

  3. 更新命名空間以符合新位置:

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

該 Note 類別是一個簡單的資料模型。 它不需要變更通知,因為 ViewModel 會管理可觀察的屬性,並通知使用者介面變更。

移動 AllNotes 模型

將類別移至 AllNotesWinUINotes.Bus 專案。

  1. 將檔案從 AllNotes.cs 專案移至 WinUINotes.Bus/Models 資料夾。

  2. 更新命名空間以符合新位置:

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

與類別一樣 Note , AllNotes 是一個簡單的模型類別。 ViewModel 會處理可觀察的行為,並管理筆記的集合。

建立 AllNotesViewModel

  1. 在 WinUINotes.Bus 專案中,建立名為 ViewModel 的新資料夾。

  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 model:這個私有欄位保存模型的 AllNotes 一個實例,用於處理實際的資料操作。
  • [RelayCommand]:此屬性會從LoadAsync()方法產生LoadCommand屬性,允許UI透過資料繫結觸發載入作業。
  • LoadAsync() method:這個方法會從模型載入註解,清除目前可觀察的集合,並填入載入的註釋。 此模式可確保 UI 繫結集合與基礎資料保持同步。

模型 (資料作業) 與allNotes可觀察集合 (UI 繫結) 之間的Notes分離是重要的 MVVM 模式,可將關注點分開,並將檢視與 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()的屬性。 只有在兩者都有TextFilename值時,才會啟用此命令。
  • InitializeForExistingNote():這個方法會將現有筆記的資料載入 ViewModel 屬性,然後透過資料繫結更新 UI。
  • 儲存邏輯:此 Save() 方法會使用目前的屬性值更新基礎 Note 模型,並呼叫 SaveAsync() 模型。 儲存之後,它會通知DeleteCommand重新評估(因為檔案現在存在且可以刪除)。
  • 刪除邏輯:該方法在筆記模型上呼叫Delete()DeleteAsync(),並建立一個新的空筆記。

在本教學課程稍後,您會整合檔案服務來處理實際的檔案作業,並使用 MVVM 工具組的 WeakReferenceMessenger 類別在刪除附註時通知應用程式的其他部分,同時保持鬆散耦合。

在檔中深入瞭解:

更新視圖以使用 ViewModel

現在,您必須更新 XAML 頁面,以系結至新的 ViewModel。

更新 AllNotesPage 視圖

  1. 在 AllNotesPage.xaml 中,將 ItemsView 的 ItemsSource 繫結更新為使用 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中,更新 TextBox 和 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>
    ...
    

    您也可以設定UpdateSourceTriggerTextBox.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);
                }
            }
        }
    }
    

    已移除Save和Delete的Click事件,因為按鈕現在會直接繫結至 ViewModel 中的命令。 在 OnNavigatedTo() 方法中實例化NoteViewModel。 如果傳遞了 Note 參數,則會使用現有的備註資料初始化 ViewModel。

在檔中深入瞭解: