添加依赖注入

依赖关系注入(DI)可帮助你管理 ViewModels 和服务生命周期。 它使代码更易于测试且更易于维护。 在此步骤中,您将在您的应用程序中配置 DI,并更新模型以使用文件服务进行文件操作。

有关 .NET 依赖项注入框架的更多背景信息,请参阅 .NET 依赖项注入 和 .NET 教程中的“使用依赖项注入 ”。

安装 Microsoft.Extensions 包

向项目添加 DI 支持。

  1. 在 Microsoft.Extensions.DependencyInjection 和 WinUINotes.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();
        }
    }
    

    文件服务接口定义了用于文件操作的方法。 它抽象化了文件处理的详细信息,使得视图模型和模型不必处理这些细节。 参数和返回值都是基本的 .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():此同步方法通过调用异步 TryGetItemAsync() 并使用 .Result 来阻塞以检查文件是否存在。 虽然通常不建议使用此方法,但此处用于支持 CanDelete() ViewModel 中的验证方法,该方法必须是同步的。
  • 存储项方法: GetStorageItemsAsync() 和 GetTextFromFileAsync() 方法通过使用 WinRT 存储 API 提供对文件及其内容的访问。 这些方法使模型能够加载和枚举笔记。

通过实现 IFileService 接口,可以轻松地将此类替换为用于测试的模拟实现,或在需要时替换为其他存储提供程序。

在文档中了解详细信息:

在 App.xaml.cs 中配置依赖项注入

在更新模型和 ViewModels 以使用文件服务之前,请配置依赖项注入,以便解析服务并将其注入到构造函数中。

更新 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() 方法:创建和配置服务集合的静态方法。 分离此方法可使配置更易于维护且更易于测试。
  • Services 属性:这是用于保存 IServiceProvider 的实例属性。 构造函数通过调用 ConfigureServices()来设置此属性。
  • App.Current static 属性:提供对当前 App 实例的便捷访问,当模型或其他类需要访问服务提供程序时,这非常有用。
  • IFileService 注册:使用 ActivatorUtilities.CreateInstance 创建一个以 ApplicationData.Current.LocalFolder 作为参数的 WindowsFileService 实例。 此方法允许在注册时注入构造函数参数。 将服务注册为单一实例,因为文件操作是无状态的,并且可以在整个应用程序中共享单个实例。
  • ViewModels 注册:将这两个 ViewModel 注册为暂时性实例,这意味着每次请求一个实例时都会创建一个新实例。 此方法可确保每个页面获取其自己的 ViewModel 实例,且其状态为干净。

模型和其他类可以通过App.Current.Services.GetService()访问服务供应商,以便在需要时检索已注册的服务。

在文档中了解详细信息:

更新模型以使用文件服务

现在文件服务可通过依赖项注入获得,请更新模型类以使用它。 模型接收文件服务并将其用于所有文件操作。

更新注释模型

更新 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项目中,因此无法从App.Current.Services项目访问WinUINotes(由于项目引用约束)。

该方法 LoadNotes() 调用专用 GetFilesInFolderAsync() 方法,以递归枚举本地存储文件夹及其子文件夹中的所有文件。 对于每个存储项:

  1. 如果它是一个文件夹,则该方法以递归方式调用自身来处理文件夹的内容
  2. 如果是文件,则会创建一个新的 Note 实例,并注入文件服务。
  3. 注释的 Filename 被设置为文件的名称
  4. 通过使用Text读取文件的内容来填充笔记的GetTextFromFileAsync()
  5. 注释的Date设置为文件的创建日期
  6. 备注已添加到可观测集合中Notes

此方法可确保从存储加载的所有笔记都有权访问将来保存和删除作所需的文件服务。

更新 ViewModels 来使用文件服务

模型现在已开始使用文件服务,需要更新 ViewModels。 但是,由于模型直接处理文件作,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 实例化 AllNotes 为无参数构造函数 (allNotes = 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 工具包 WeakReferenceMessenger.Default.Send() 在删除笔记后广播 NoteDeletedMessage 。 此方法允许 ViewModels 之间的松散耦合 - 应用程序的其他部分(如 NotePage)可以侦听此消息并相应地做出响应(例如,通过导航回已刷新的备注列表),而无需 NoteViewModel 直接引用它们。

MVVM WeakReferenceMessenger 工具包的一项关键功能,它通过使用弱引用来防止内存泄漏。 组件可以在不创建阻止垃圾回收的强引用的情况下订阅消息。

在文档中了解详细信息:

创建 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. 发送方:通过使用 的 方法发送消息。
  2. 接收方:页面(如 NotePage)可以通过实现 IRecipient<NoteDeletedMessage> 和注册信使来注册以接收消息。 收到消息后,页面可以导航回所有备注列表。
  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 容器会自动在其构造函数中提供 IFileService 所需的依赖项 AllNotesViewModel 。
  2. 生命周期管理:DI 容器根据注册方式(在本例中为暂时性实例)管理 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: 现在,通过在OnNavigatedTo()方法中使用App.Current.Services.GetService<NoteViewModel>()从依赖注入容器中检索NoteViewModel,而不是直接实例化。 此方法可确保 ViewModel 自动接收其所需的 IFileService 依赖项。
  2. 消息注册:新RegisterForDeleteMessages()方法使用WeakReferenceMessenger订阅NoteDeletedMessage。 删除笔记时(通过 NoteViewModel.Delete() 方法),此页面将接收消息并通过 Frame.GoBack() 导航回所有笔记列表。
  3. 消息模式:此模式演示通过 MVVM 工具包的消息系统实现的松散耦合。 NoteViewModel不需要知道导航或页面结构 - 它只是在删除笔记时发送一条消息,并且页面独立处理导航响应。
  4. 生命周期计时:在视图模型实例化和消息注册过程中,确保在页面变为活动状态时,所有内容都正确初始化。

此模式有效地分隔了关注点:ViewModel 侧重于业务逻辑和数据作,而页面处理特定于 UI 的问题(如导航)。