共用方式為


使用 MVVM 工具組實作 MVVM

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

與類別一樣 NoteAllNotes 是一個簡單的模型類別。 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 會自動產生公用內容 (FilenameTextDate),並支援變更通知。
  • [NotifyCanExecuteChangedFor]:此屬性可確保當FilenameText變更時,相關聯的命令會重新評估它們是否可以執行。 例如,當您輸入文字時,儲存按鈕會根據驗證邏輯自動啟用或停用。
  • [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 中,將 ItemsViewItemsSource 繫結更新為使用 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中,更新 TextBoxTextHeader繫結,以使用 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);
                }
            }
        }
    }
    

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

在檔中深入瞭解: