Поделиться через


Добавить внедрение зависимостей

Внедрение зависимостей (DI) помогает управлять жизненным циклом ваших ViewModels и служб. Это делает код более тестируемым и удобным для обслуживания. На этом шаге вы настроите DI в приложении и обновите модели для использования файловой службы для операций с файлами.

Дополнительные сведения о платформе внедрения зависимостей .NET см. в статье "Внедрение зависимостей в .NET" и руководстве "Использование внедрения зависимостей в .NET".

Установка пакетов Microsoft.Extensions

Добавьте поддержку DI в проекты.

  1. Установите Microsoft.Extensions.DependencyInjection в проектах WinUINotes и 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();
        }
    }
    

    Интерфейс службы файлов определяет методы для операций с файлами. Он абстрагирует сведения об обработке файлов из ViewModels и Models. Параметры и возвращаемые значения — это все базовые типы или интерфейсы .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 предоставляет конкретные операции с файлами с помощью API среды выполнения Windows (WinRT) и .NET:

  • Внедрение зависимостей через конструктор: служба принимает IStorageFolder в своем конструкторе. Этот подход позволяет настроить расположение хранилища при создании экземпляра службы. Такой подход делает службу гибкой и проверяемой.
  • CreateOrUpdateFileAsync(): этот метод используется TryGetItemAsync() для проверки наличия файла. Если это так, метод обновляет существующий файл. В противном случае он создает новый файл с помощью CreateFileAsync(). Этот подход обрабатывает сценарии создания и обновления в одном методе.
  • DeleteFileAsync(): перед удалением файла этот метод проверяет, существует ли файл с помощью TryGetItemAsync(). Эта проверка предотвращает выбрасывание исключений при попытке удалить несуществующий файл.
  • FileExists(): этот синхронный метод проверяет существование файла, вызывая асинхронный метод TryGetItemAsync() и блокируя с помощью .Result. Хотя этот подход обычно не рекомендуется, он используется здесь для поддержки CanDelete() метода проверки в ViewModel, который должен быть синхронным.
  • Методы элемента хранилища: Методы GetStorageItemsAsync() и GetTextFromFileAsync() предоставляют доступ к файлам и их содержимому, используя API хранилища WinRT. Эти методы позволяют моделям загружать и перечислять заметки.

Реализуя 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() метод: статический метод, который создает и настраивает коллекцию служб. Разделение этого метода упрощает обслуживание конфигурации и упрощает тестирование.
  • Services свойство: свойство экземпляра, которое содержит объект IServiceProvider. Конструктор задает это свойство путем вызова ConfigureServices().
  • App.Current статичное свойство: предоставляет удобный доступ к текущему App экземпляру, который полезен, если модели или другие классы должны получить доступ к поставщику услуг.
  • IFileService регистрация: ActivatorUtilities.CreateInstance используется для создания экземпляра WindowsFileService с ApplicationData.Current.LocalFolder в качестве параметра. Этот подход позволяет внедрить параметр конструктора во время регистрации. Зарегистрируйте службу в качестве синглтона, так как операции с файлами не зависят от состояния, и один экземпляр можно разделять в приложении.
  • Регистрация ViewModels: регистрируйте оба ViewModels как временные, то есть создается новый экземпляр каждый раз, когда он запрашивается. Такой подход гарантирует, что каждая страница получает собственный экземпляр 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() делегируют задачи в внедренную файловую службу, сохраняя модель сосредоточенной на координации операций, а не на реализации процессов файлового ввода-вывода.

Этот подход устраняет необходимость для модели использовать шаблон локатора служб (непосредственно обращаясь к 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 для использования службы файлов

Поскольку модели теперь используют файловую службу, необходимо обновить ViewModel. Однако, так как модели обрабатывают операции файлов напрямую, ViewModels в основном сосредоточены на оркестрации моделей и управлении наблюдаемыми свойствами.

Обновить 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 в конструктор. На втором шаге ViewModel создана AllNotes с использованием конструктора без параметров (allNotes = new AllNotes()). Теперь, когда модели требуется, чтобы служба файлов AllNotes выполняла свои операции, ViewModel получает IFileService через инициализацию конструктора и передает его в модель.

Это изменение поддерживает правильный поток зависимостей— служба файлов внедряется на верхнем уровне (ViewModel) и переходит к модели. ViewModel продолжает сосредоточиваться на координации процесса загрузки и поддержании наблюдаемой Notes коллекции, синхронизированной с данными модели, без необходимости знать подробности о реализации загрузки файлов.

Обновление NoteViewModel

Обновите NoteViewModel, чтобы внедрить файловую службу и использовать систему обмена сообщениями MVVM Toolkit.

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?

Несколько важных изменений поддерживают внедрение зависимостей и взаимодействие между моделями представления.

  1. Внедрение файловой службы: конструктор теперь принимает IFileService в качестве параметра и сохраняет его в поле. Эта служба передается модели Note при создании новых инстансов, обеспечивая, что все записки могут выполнять операции с файлами.

  2. WeakReferenceMessenger: метод Delete() теперь использует набор средств MVVM Toolkit WeakReferenceMessenger.Default.Send() для оповещения NoteDeletedMessage после удаления заметки. Этот подход позволяет создать свободную связь между ViewModels - другие части приложения (например, 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. Отправитель: NoteViewModel.Delete() метод отправляет сообщение с помощью WeakReferenceMessenger.Default.Send(new NoteDeletedMessage(note)).
  2. Получатель: Страницы (например NotePage) могут регистрироваться для получения сообщений путем реализации IRecipient<NoteDeletedMessage> и регистрации в messenger. При получении сообщения страница может вернуться к списку всех заметок.
  3. Слабо связано: отправитель не должен знать, кто (если кто-либо) слушает. Получатель не нуждается в прямой ссылке на отправителя. Эта настройка обеспечивает независимые и тестируемые компоненты.

Подход с использованием слабых ссылок означает, что если компонент собран сборщиком мусора, то его подписка на сообщения автоматически очищается, предотвращая утечки памяти.

Обновление страниц для использования внедрения зависимостей

Обновите конструкторы страниц, чтобы получать ViewModels через механизм DI.

Обновить 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?

Теперь приложение получает AllNotesViewModel из контейнера внедрения зависимостей, используя App.Current.Services.GetService<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. ViewModel из контейнера DI: NoteViewModel теперь извлекается из контейнера внедрения зависимостей с использованием метода App.Current.Services.GetService<NoteViewModel>()OnNavigatedTo(), вместо непосредственного создания экземпляра. Этот подход гарантирует, что ViewModel автоматически получает необходимую IFileService зависимость.
  2. Регистрация сообщений: новый метод RegisterForDeleteMessages() подписывается на NoteDeletedMessage с использованием WeakReferenceMessenger. При удалении заметки (из NoteViewModel.Delete() метода) эта страница получает сообщение и возвращается к списку всех заметок с помощью Frame.GoBack().
  3. Шаблон обмена сообщениями: этот шаблон демонстрирует свободное связывание, включенное системой обмена сообщениями MVVM Toolkit. Элемент NoteViewModel не нуждается в знании о навигации или структуре страницы - он просто отправляет сообщение, когда заметка удаляется, и страница самостоятельно обрабатывает ответ навигации.
  4. Время жизненного цикла: Экземпляр ViewModel создается, и регистрация сообщений производится OnNavigatedTo(), обеспечивая правильную инициализацию всего, когда страница становится активной.

Этот шаблон эффективно разделяет аспекты: ViewModel фокусируется на бизнес-логике и операциях данных, а страница обрабатывает аспекты, связанные с пользовательским интерфейсом, такие как навигация.