共用方式為


新增相依性注入

依附元件注入 (DI) 可協助您管理 ViewModel 和服務的生命週期。 它使您的程式碼更具可測試性且更易於維護。 在此步驟中,您會在應用程式中設定 DI,並更新模型,以使用檔案服務進行檔案作業。

如需 .NET 相依性插入式架構的更多背景資訊,請參閱 .NET 相依性插入式 和在 .NET 中使用相依性插入 教學課程。

安裝 Microsoft.Extensions 套件

將 DI 支援新增至您的專案。

  1. Microsoft.Extensions.DependencyInjectionWinUINotes.Bus 專案中安裝

    dotnet add WinUINotes package Microsoft.Extensions.DependencyInjection
    dotnet add WinUINotes.Bus package Microsoft.Extensions.DependencyInjection
    

建立檔案服務介面和實作

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

  2. 新增介面檔案 IFileService.cs

    using System.Collections.Generic;
    using System.Threading.Tasks;
    using Windows.Storage;
    
    namespace WinUINotes.Services
    {
        public interface IFileService
        {
            Task<IReadOnlyList<IStorageItem>> GetStorageItemsAsync();
            Task<IReadOnlyList<IStorageItem>> GetStorageItemsAsync(IStorageFolder storageFolder);
            Task<string> GetTextFromFileAsync(IStorageFile file);
            Task CreateOrUpdateFileAsync(string filename, string contents);
            Task DeleteFileAsync(string filename);
            bool FileExists(string filename);
            IStorageFolder GetLocalFolder();
        }
    }
    

    檔案服務介面會定義檔案作業的方法。 它從 ViewModel 和 Model 中抽象化出檔案處理的詳細資料。 參數和傳回值都是基本的 .NET 類型或介面。 這種設計確保了在單元測試中可以輕鬆模擬或替換服務,從而促進鬆散耦合和可測試性。

  3. 新增實作檔案 WindowsFileService.cs

    using System;
    using System.Collections.Generic;
    using System.Threading.Tasks;
    using Windows.Storage;
    
    namespace WinUINotes.Services
    {
        public class WindowsFileService : IFileService
        {
             public StorageFolder storageFolder;
    
             public WindowsFileService(IStorageFolder storageFolder)
             {
                 this.storageFolder = (StorageFolder)storageFolder;
    
                 if (this.storageFolder is null)
                 {
                     throw new ArgumentException("storageFolder must be of type StorageFolder", nameof(storageFolder));
                 }
             }
    
             public async Task CreateOrUpdateFileAsync(string filename, string contents)
             {
                 // Save the note to a file.
                 StorageFile storageFile = (StorageFile)await storageFolder.TryGetItemAsync(filename);
                 if (storageFile is null)
                 {
                     storageFile = await storageFolder.CreateFileAsync(filename, CreationCollisionOption.ReplaceExisting);
                 }
                 await FileIO.WriteTextAsync(storageFile, contents);
             }
    
         public async Task DeleteFileAsync(string filename)
         {
             // Delete the note from the file system.
             StorageFile storageFile = (StorageFile)await storageFolder.TryGetItemAsync(filename);
             if (storageFile is not null)
             {
                 await storageFile.DeleteAsync();
             }
         }
    
         public bool FileExists(string filename)
         {
             StorageFile storageFile = (StorageFile)storageFolder.TryGetItemAsync(filename).AsTask().Result;
             return storageFile is not null;
         }
    
         public IStorageFolder GetLocalFolder()
         {
             return storageFolder;
         }
    
         public async Task<IReadOnlyList<IStorageItem>> GetStorageItemsAsync()
         {
             return await storageFolder.GetItemsAsync();
         }
    
         public async Task<IReadOnlyList<IStorageItem>> GetStorageItemsAsync(IStorageFolder folder)
         {
             return await folder.GetItemsAsync();
         }
    
         public async Task<string> GetTextFromFileAsync(IStorageFile file)
         {
             return await FileIO.ReadTextAsync(file);
         }
        }
    }
    

實作會 WindowsFileService 使用 Windows 執行階段 (WinRT) 和 .NET 儲存體 API 來提供具體的檔案作業:

  • 建構函式注入:服務在其建構函式中接受IStorageFolder。 此方法可讓您在具現化服務時設定儲存位置。 這種方法使服務具有彈性且可測試。
  • CreateOrUpdateFileAsync():此方法用來 TryGetItemAsync() 檢查檔案是否已存在。 如果是,方法會更新現有的檔案。 否則,它會使用 CreateFileAsync()建立新檔案。 此方法會在單一方法中同時處理建立和更新案例。
  • DeleteFileAsync():在刪除檔案之前,此方法會使用 TryGetItemAsync()來驗證檔案是否存在。 此檢查可防止在嘗試刪除不存在的檔案時拋出異常。
  • FileExists():此同步方法會呼叫 async TryGetItemAsync() 並封鎖 .Result來檢查檔案是否存在。 雖然通常不建議使用此方法,但這裡會使用它來支援 CanDelete() ViewModel 中的驗證方法,且必須是同步的。
  • 儲存專案方法:這些方法,例如GetStorageItemsAsync()GetTextFromFileAsync(),會使用 WinRT 儲存 API 用於存取檔案和內容。 這些方法可讓模型載入和列舉筆記。

藉由實作 IFileService 介面,您可以輕鬆地將這個類別取代為用於測試的模擬實作,或視需要使用不同的儲存提供者。

在檔中深入瞭解:

在App.xaml.cs中設定相依性插入

在更新模型和 ViewModel 以使用檔案服務之前,請設定相依性插入,以便解析服務並將其插入建構函式。

更新 App.xaml.cs 檔案以設定 DI 容器:

using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml;
using WinUINotes.ViewModels;

namespace WinUINotes;

public partial class App : Application
{
    private readonly IServiceProvider _serviceProvider;

    public App()
    {
        Services = ConfigureServices();
        this.InitializeComponent();
    }

    private static IServiceProvider ConfigureServices()
    {
        var services = new ServiceCollection();

        // Services
        services.AddSingleton<Services.IFileService>(x =>
            ActivatorUtilities.CreateInstance<Services.WindowsFileService>(x,
                            Windows.Storage.ApplicationData.Current.LocalFolder)
        );

        // ViewModels
        services.AddTransient<AllNotesViewModel>();
        services.AddTransient<NoteViewModel>();

        return services.BuildServiceProvider();
    }

    protected override void OnLaunched(LaunchActivatedEventArgs args)
    {
        m_window = new MainWindow();
        m_window.Activate();
    }

    public IServiceProvider Services { get; }

    private Window? m_window;

    public new static App Current => (App)Application.Current;
}

此設定會設定具有所有必要服務的相依性插入容器:

  • ConfigureServices() method:建立和設定服務集合的靜態方法。 將此方法分開,可讓配置更易於維護且更易於測試。
  • Services 屬性:一個實例屬性,用於保存 IServiceProvider。 建構函式會呼叫 ConfigureServices()來設定此屬性。
  • App.Current static 屬性:提供對目前 App 實例的便利訪問,這在模型或其他類別需要存取服務提供者時非常有用。
  • IFileService registration:使用 ActivatorUtilities.CreateInstance 建立一個以 ApplicationData.Current.LocalFolder 作為參數的 WindowsFileService 實例。 這種方法允許在註冊時注入建構函式參數。 將服務註冊為單一,因為檔案作業是無狀態的,而且單一執行個體可以在應用程式之間共用。
  • ViewModel 註冊:將兩個 ViewModel 註冊為暫時性,這表示每次請求一個實例時都會建立一個新實例。 這種方法可確保每個頁面都有自己的 ViewModel 實例,並具有乾淨的狀態。

模型和其他類別可以透過App.Current.Services.GetService()來存取服務提供者,以便在需要時檢索已註冊的服務。

在檔中深入瞭解:

更新模型以使用檔案服務

現在檔案服務可透過相依性插入取得,請更新模型類別以使用它。 模型會接收檔案服務,並將其用於所有檔案作業。

更新 Note 模型

更新 Note 類別以接受檔案服務,並將其用於儲存、刪除和檔案存在作業:

using System;
using System.Threading.Tasks;
using WinUINotes.Services;

namespace WinUINotes.Models;

public class Note
{
    private IFileService fileService;
    public string Filename { get; set; } = string.Empty;
    public string Text { get; set; } = string.Empty;
    public DateTime Date { get; set; } = DateTime.Now;

    public Note(IFileService fileService)
    {
        Filename = "notes" + DateTime.Now.ToBinary().ToString() + ".txt";
        this.fileService = fileService;
    }

    public async Task SaveAsync()
    {
        await fileService.CreateOrUpdateFileAsync(Filename, Text);
    }

    public async Task DeleteAsync()
    {
        await fileService.DeleteFileAsync(Filename);
    }

    public bool NoteFileExists()
    {
        return fileService.FileExists(Filename);
    }
}

Note模型現在會透過建構函式插入接收檔案服務:

  • 建構函式:接受IFileService參數,使依賴關係明確且必須。 這種設計提高了可測試性並確保模型始終可以存取所需的檔案服務。
  • 檔案名稱產生:建構函式使用目前時間戳記自動產生唯一的檔案名,確保每個筆記都有不同的檔案名稱。
  • 檔案操作SaveAsync()、 和DeleteAsync()NoteFileExists()方法都委派給注入的檔案服務,使模型專注於協調操作,而不是實現檔案 I/O 詳細資訊。

這種方法消除了模型使用服務定位器模式(直接存取 App.Services )的需要,從而提高了可測試性並使依賴關係變得清晰。

更新 AllNotes 模型

更新 AllNotes 類別,以使用檔案服務從儲存體載入筆記:

using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using Windows.Storage;
using WinUINotes.Services;

namespace WinUINotes.Models;

public class AllNotes
{
        private IFileService fileService;
        public ObservableCollection<Note> Notes { get; set; } = [];

        public AllNotes(IFileService fileService)
        {
            this.fileService = fileService;
        }

        public async Task LoadNotes()
        {
            Notes.Clear();
            await GetFilesInFolderAsync(fileService.GetLocalFolder());
        }

        private async Task GetFilesInFolderAsync(IStorageFolder folder)
        {
            // Each StorageItem can be either a folder or a file.
            IReadOnlyList<IStorageItem> storageItems =
                                        await fileService.GetStorageItemsAsync(folder);
            foreach (IStorageItem item in storageItems)
            {
                if (item.IsOfType(StorageItemTypes.Folder))
                {
                    // Recursively get items from subfolders.
                    await GetFilesInFolderAsync((IStorageFolder)item);
                }
                else if (item.IsOfType(StorageItemTypes.File))
                {
                    IStorageFile file = (IStorageFile)item;
                    Note note = new(fileService)
                    {
                        Filename = file.Name,
                        Text = await fileService.GetTextFromFileAsync(file),
                        Date = file.DateCreated.DateTime
                    };
                    Notes.Add(note);
                }
            }
        }
}

AllNotes 模型透過建構函式注入接收檔案服務,就像 Note 模型一樣。 由於這個類別位於 WinUINotes.Bus 專案中,無法從 WinUINotes 專案存取 App.Current.Services (由於專案參考限制)。

LoadNotes() 方法會呼叫 private GetFilesInFolderAsync() 方法,以遞迴列舉本機儲存體資料夾及其子資料夾中的所有檔案。 針對每個儲存項目:

  1. 如果是資料夾,則方法會遞迴呼叫自己來處理資料夾的內容
  2. 如果是檔案,則會建立插入 Note 檔案服務的新實例
  3. 附註中的 Filename 標籤設定為檔案的名稱
  4. 附註的Text是藉由使用GetTextFromFileAsync()讀取檔案內容後填入的。
  5. 附註的 Date 設定為檔案的建立日期
  6. 附註已被新增到 Notes 可觀察集合

這種方法可確保從儲存中載入的所有筆記都可以存取未來儲存和刪除操作所需的檔案服務。

更新 ViewModel 以使用檔案服務

現在這些模型使用了檔案服務,您需要更新視圖模型。 不過,由於模型會直接處理檔案作業,因此 ViewModel 主要著重於協調模型和管理可觀察的屬性。

更新 AllNotesViewModel

更新 AllNotesViewModel 以配合更新的 AllNotes 模型:

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using WinUINotes.Models;
using WinUINotes.Services;

namespace WinUINotes.ViewModels
{
    public partial class AllNotesViewModel : ObservableObject
    {
        private readonly AllNotes allNotes;

        [ObservableProperty]
        private ObservableCollection<Note> notes;

        public AllNotesViewModel(IFileService fileService)
        {
            allNotes = new AllNotes(fileService);
            notes = new ObservableCollection<Note>();
        }

        [RelayCommand]
        public async Task LoadAsync()
        {
            await allNotes.LoadNotes();
            Notes.Clear();
            foreach (var note in allNotes.Notes)
            {
                Notes.Add(note);
            }
        }
    }
}

自步驟 2 以來發生了什麼變化?

關鍵的變化是將參數添加到 IFileService 構造函數中。 在步驟 2 中,ViewModel 使用無參數建構函式實例化 AllNotesallNotes = new AllNotes())。 現在模型 AllNotes 需要檔案服務來執行其作業,ViewModel 會接收 IFileService 貫通建構函式注入,並將它傳遞給模型。

此變更會維持適當的相依性流程 - 檔案服務會在最上層(ViewModel)插入,並向下傳遞到模型。 ViewModel 繼續專注於協調載入程序,並讓可觀察集合 Notes 與模型資料保持同步,而不需要知道檔案載入方式的實作詳細資料。

更新 NoteViewModel

請更新 NoteViewModel 以注入檔案服務,並使用 MVVM 工具組的訊息系統:

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using System;
using System.Threading.Tasks;
using WinUINotes.Models;
using WinUINotes.Services;

namespace WinUINotes.ViewModels
{
    public partial class NoteViewModel : ObservableObject
    {
        private Note note;
        private IFileService fileService;

        [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(IFileService fileService)
        {
            this.fileService = fileService;
            this.note = new Note(fileService);
            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(fileService);
            // Send a message from some other module
            WeakReferenceMessenger.Default.Send(new NoteDeletedMessage(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();
        }
    }
}

自步驟 2 以來發生了什麼變化?

有幾項重要變更支援依附元件插入和 ViewModel 間通訊:

  1. 檔案服務注入:建構函式現在接受 IFileService 作為參數,並將其儲存在欄位中。 在建立新實例時,將此服務傳遞給 Note 模型,以確保所有註釋都可以執行檔案操作。

  2. WeakReferenceMessenger:該Delete()方法現在使用 MVVM 工具包在刪除筆記後廣播NoteDeletedMessage。 這種方法使 ViewModel 之間實現鬆散耦合 - 應用程式的其他部分(例如 NotePage)可以接聽此訊息並適當地回應 (例如,導覽回已重新整理的筆記清單),而不需要 NoteViewModel 直接參考它們。

WeakReferenceMessenger 是 MVVM 工具組的一個關鍵功能,通過使用弱參考防止記憶體洩漏。 元件可以訂閱訊息,而不需要建立強式參照來防止記憶體回收。

在檔中深入瞭解:

建立 NoteDeletedMessage 類別

需要一個訊息類別在WeakReferenceMessenger元件之間進行傳送。 建立一個新類別來表示筆記刪除事件:

  1. WinUINotes.Bus 專案中,新增新的類別檔案 NoteDeletedMessage.cs

    using CommunityToolkit.Mvvm.Messaging.Messages;
    using WinUINotes.Models;
    
    namespace WinUINotes
    {
        public class NoteDeletedMessage : ValueChangedMessage<Note>
        {
            public NoteDeletedMessage(Note note) : base(note)
            {
            }
        }
    }
    

此訊息類別繼承自 ValueChangedMessage<Note>,這是 MVVM 工具組所提供的特殊訊息類型,用於攜帶值變更通知。 建構函式接受一個Note並將其傳遞給基類,使訊息收件者可以透過Value屬性使用它。 傳送此訊息時,由 NoteViewModel 發送,任何訂閱 NoteDeletedMessage 的元件都會接收到,並且可以通過 Value 屬性存取已刪除的附註。

傳訊在MVVM工具組中的運作方式:

  1. 寄件者:該方法透過 WeakReferenceMessenger.Default.Send(new NoteDeletedMessage(note)) 使用 NoteViewModel.Delete() 傳送訊息。
  2. 接收者:頁面(如 NotePage)可以透過實作 IRecipient<NoteDeletedMessage> 並向 Messenger 註冊來註冊接收訊息。 收到訊息時,頁面可以導覽回所有附註清單。
  3. 鬆散耦合:發送者不需要知道誰(如果有的話)在監聽。 接收者不需要直接引用發送者。 此設定可讓您的元件保持獨立且可測試。

弱引用方式是指如果某個元件被垃圾回收,則會自動清理其訊息訂閱,而不會造成記憶體洩漏。

更新頁面以使用相依性注入

更新您的頁面建構函式,以透過 DI 接收 ViewModel。

更新AllNotesPage.xaml.cs

using Microsoft.Extensions.DependencyInjection;
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 = App.Current.Services.GetService<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();
            }
        }
    }
}

自步驟 2 以來發生了什麼變化?

應用程式現在使用App.Current.Services.GetService<AllNotesViewModel>()從相依性插入容器AllNotesViewModel取得,而不是直接使用new AllNotesViewModel()來建立。 這種方法有幾個好處:

  1. 自動相依性解析:DI 容器會自動提供 AllNotesViewModel 在其建構函式中所需的 IFileService 相依性。
  2. 生命週期管理:DI 容器會根據 ViewModel 的註冊方式來管理 ViewModel 的生命週期 (在本例中為暫時性,提供新的執行個體)。
  3. 可測試性:這種模式使得在測試中交換實作或模擬依賴關係變得更加容易。
  4. 可維護性:如果 ViewModel 的依賴關係在未來發生變化,您只需要更新 DI 配置,而不是每個建立 ViewModel 的地方。

程式碼的其餘部分保持不變。 當使用者流覽至此頁面時,方法 OnNavigatedTo() 仍會呼叫 LoadAsync() 重新整理附註清單。

更新 NotePage.xaml.cs

using CommunityToolkit.Mvvm.Messaging;
using Microsoft.Extensions.DependencyInjection;
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();
        }

        public void RegisterForDeleteMessages()
        {
            WeakReferenceMessenger.Default.Register<NoteDeletedMessage>(this, (r, m) =>
            {
                if (Frame.CanGoBack)
                {
                    Frame.GoBack();
                }
            });
        }

        protected override void OnNavigatedTo(NavigationEventArgs e)
        {
            base.OnNavigatedTo(e);
            noteVm = App.Current.Services.GetService<NoteViewModel>();
            RegisterForDeleteMessages();

            if (e.Parameter is Note note && noteVm is not null)
            {
                noteVm.InitializeForExistingNote(note);
            }
        }
    }
}

自步驟 2 以來發生了什麼變化?

幾個重要的變更整合了依賴注入和傳訊功能:

  1. 來自 DI 容器的 ViewModel:現在從相依性注入容器中使用App.Current.Services.GetService<NoteViewModel>()OnNavigatedTo()方法中取得NoteViewModel,而不是直接實例化。 這種方法可確保 ViewModel 自動接收其所需的 IFileService 相依性。
  2. 訊息註冊:新 RegisterForDeleteMessages() 方法透過使用 WeakReferenceMessenger 來訂閱 NoteDeletedMessage。 當附註透過 NoteViewModel.Delete() 方法刪除時,此頁面會收到訊息,並使用 Frame.GoBack() 返回所有附註清單。
  3. 傳訊模式:此模式示範 MVVM 工具組傳訊系統所啟用的鬆散耦合。 不需要 NoteViewModel 知道導覽或頁面結構 - 它只會在刪除筆記時傳送訊息,而頁面會獨立處理導覽回應。
  4. 生命週期計時:ViewModel 在OnNavigatedTo()內實例化,並進行訊息註冊,確保在頁面啟動時所有內容正確初始化。

這種模式有效地區分了關注點:ViewModel 專注於業務邏輯和數據操作,而頁面則處理特定於 UI 的問題,例如導航。