Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
Dependency injection (DI) helps you manage the lifecycle of your ViewModels and services. It makes your code more testable and easier to maintain. In this step, you configure DI in your app and update your models to use a file service for file operations.
For more background on the .NET dependency injection framework, see .NET dependency injection and the Use dependency injection in .NET tutorial.
Install Microsoft.Extensions packages
Add DI support to your projects.
Install
Microsoft.Extensions.DependencyInjectionin both WinUINotes and WinUINotes.Bus projects:dotnet add WinUINotes package Microsoft.Extensions.DependencyInjection dotnet add WinUINotes.Bus package Microsoft.Extensions.DependencyInjection
Create a file service interface and implementation
In the WinUINotes.Bus project, create a new folder named Services.
Add an interface file
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(); } }The file service interface defines methods for file operations. It abstracts away the details of file handling from the ViewModels and Models. The parameters and return values are all either basic .NET types or interfaces. This design ensures that the service can be easily mocked or replaced in unit tests, promoting loose coupling and testability.
Add the implementation file
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); } } }
The WindowsFileService implementation provides concrete file operations by using the Windows Runtime (WinRT) and .NET storage APIs:
- Constructor injection: The service accepts an
IStorageFolderin its constructor. This approach allows you to configure the storage location when you instantiate the service. This approach makes the service flexible and testable. CreateOrUpdateFileAsync(): This method usesTryGetItemAsync()to check if a file already exists. If it does, the method updates the existing file. Otherwise, it creates a new file by usingCreateFileAsync(). This approach handles both create and update scenarios in a single method.DeleteFileAsync(): Before deleting a file, this method verifies that the file exists by usingTryGetItemAsync(). This check prevents exceptions from being thrown when attempting to delete non-existent files.FileExists(): This synchronous method checks for file existence by calling the asyncTryGetItemAsync()and blocking with.Result. While this approach is generally not recommended, it's used here to support theCanDelete()validation method in the ViewModel, which must be synchronous.- Storage item methods: The
GetStorageItemsAsync()andGetTextFromFileAsync()methods provide access to files and their contents by using WinRT storage APIs. These methods enable the Models to load and enumerate notes.
By implementing the IFileService interface, you can easily replace this class with a mock implementation for testing or a different storage provider if needed.
Learn more in the docs:
Configure dependency injection in App.xaml.cs
Before updating the models and ViewModels to use the file service, configure dependency injection so the service can be resolved and injected into the constructors.
Update the App.xaml.cs file to set up the DI container:
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;
}
This configuration sets up the dependency injection container with all the required services:
ConfigureServices()method: A static method that creates and configures the service collection. Separating this method makes the configuration more maintainable and easier to test.Servicesproperty: An instance property that holds theIServiceProvider. The constructor sets this property by callingConfigureServices().App.Currentstatic property: Provides convenient access to the currentAppinstance, which is useful when models or other classes need to access the service provider.IFileServiceregistration: UsesActivatorUtilities.CreateInstanceto create aWindowsFileServiceinstance with theApplicationData.Current.LocalFolderas a parameter. This approach allows the constructor parameter to be injected at registration time. Register the service as a singleton since file operations are stateless and a single instance can be shared across the application.- ViewModels registration: Register both ViewModels as transient, meaning a new instance is created each time one is requested. This approach ensures each page gets its own ViewModel instance with clean state.
Models and other classes can access the service provider through App.Current.Services.GetService() to retrieve registered services when needed.
Learn more in the docs:
Update models to use the file service
Now that the file service is available through dependency injection, update the model classes to use it. The models receive the file service and use it for all file operations.
Update the Note model
Update the Note class to accept the file service and use it for save, delete, and file existence operations:
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);
}
}
The Note model now receives the file service through constructor injection:
- Constructor: Accepts an
IFileServiceparameter, making the dependency explicit and required. This design promotes testability and ensures the model always has access to the file service it needs. - Filename generation: The constructor automatically generates a unique filename by using the current timestamp, ensuring each note has a distinct filename.
- File operations: The
SaveAsync(),DeleteAsync(), andNoteFileExists()methods all delegate to the injected file service, keeping the model focused on coordinating operations rather than implementing file I/O details.
This approach eliminates the need for the model to use the service locator pattern (accessing App.Services directly), which improves testability and makes dependencies clear.
Update the AllNotes model
Update the AllNotes class to load notes from storage by using the file service:
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);
}
}
}
}
The AllNotes model receives the file service through constructor injection, just like the Note model. Since this class is in the WinUINotes.Bus project, it can't access App.Current.Services from the WinUINotes project (due to project reference constraints).
The LoadNotes() method calls the private GetFilesInFolderAsync() method to recursively enumerate all files in the local storage folder and its subfolders. For each storage item:
- If it's a folder, the method recursively calls itself to process the folder's contents
- If it's a file, it creates a new
Noteinstance with the file service injected - The note's
Filenameis set to the file's name - The note's
Textis populated by reading the file's contents by usingGetTextFromFileAsync() - The note's
Dateis set to the file's creation date - The note is added to the
Notesobservable collection
This approach ensures all notes loaded from storage have access to the file service they need for future save and delete operations.
Update ViewModels to use the file service
With the models now using the file service, you need to update the ViewModels. However, since the models handle the file operations directly, the ViewModels primarily focus on orchestrating the models and managing observable properties.
Update AllNotesViewModel
Update the AllNotesViewModel to work with the updated AllNotes model:
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);
}
}
}
}
What changed since Step 2?
The key change is the addition of the IFileService parameter to the constructor. In Step 2, the ViewModel instantiated AllNotes with a parameterless constructor (allNotes = new AllNotes()). Now that the AllNotes model requires the file service to perform its operations, the ViewModel receives the IFileService through constructor injection and passes it to the model.
This change maintains proper dependency flow - the file service is injected at the top level (ViewModel) and flows down to the model. The ViewModel continues to focus on coordinating the loading process and keeping the observable Notes collection synchronized with the model's data, without needing to know the implementation details of how files are loaded.
Update NoteViewModel
Update the NoteViewModel to inject the file service and use the MVVM Toolkit's messaging system:
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();
}
}
}
What changed since Step 2?
Several important changes support dependency injection and inter-ViewModel communication:
File service injection: The constructor now accepts
IFileServiceas a parameter and stores it in a field. This service is passed to theNotemodel when creating new instances, ensuring all notes can perform file operations.WeakReferenceMessenger: The
Delete()method now uses the MVVM Toolkit'sWeakReferenceMessenger.Default.Send()to broadcast aNoteDeletedMessageafter deleting a note. This approach enables loose coupling between ViewModels - other parts of the application (likeNotePage) can listen for this message and respond appropriately (for example, by navigating back to the list of notes, which has refreshed) without theNoteViewModelneeding a direct reference to them.
The WeakReferenceMessenger is a key feature of the MVVM Toolkit that prevents memory leaks by using weak references. Components can subscribe to messages without creating strong references that would prevent garbage collection.
Learn more in the docs:
Create the NoteDeletedMessage class
The WeakReferenceMessenger needs a message class to send between components. Create a new class to represent the note deletion event:
In the WinUINotes.Bus project, add a new class file
NoteDeletedMessage.cs:using CommunityToolkit.Mvvm.Messaging.Messages; using WinUINotes.Models; namespace WinUINotes { public class NoteDeletedMessage : ValueChangedMessage<Note> { public NoteDeletedMessage(Note note) : base(note) { } } }
This message class inherits from ValueChangedMessage<Note>, which is a specialized message type provided by the MVVM Toolkit for carrying value change notifications. The constructor accepts a Note and passes it to the base class, making it available to message recipients through the Value property. When NoteViewModel sends this message, any component that subscribes to NoteDeletedMessage receives it and can access the deleted note through the Value property.
How messaging works in the MVVM Toolkit:
- Sender: The
NoteViewModel.Delete()method sends the message by usingWeakReferenceMessenger.Default.Send(new NoteDeletedMessage(note)). - Receiver: Pages (like
NotePage) can register to receive messages by implementingIRecipient<NoteDeletedMessage>and registering with the messenger. When the message is received, the page can navigate back to the all notes list. - Loose coupling: The sender doesn't need to know who (if anyone) is listening. The receiver doesn't need a direct reference to the sender. This setup keeps your components independent and testable.
The weak reference approach means that if a component is garbage collected, its message subscription is automatically cleaned up without causing memory leaks.
Update pages to use dependency injection
Update your page constructors to receive the ViewModels through DI.
Update 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();
}
}
}
}
What changed since Step 2?
The app now gets the AllNotesViewModel from the dependency injection container by using App.Current.Services.GetService<AllNotesViewModel>() instead of creating it directly with new AllNotesViewModel(). This approach has several benefits:
- Automatic dependency resolution: The DI container automatically provides the
IFileServicedependency thatAllNotesViewModelrequires in its constructor. - Lifecycle management: The DI container manages the ViewModel's lifecycle according to how it was registered (as a transient in this case, providing a fresh instance).
- Testability: This pattern makes it easier to swap implementations or mock dependencies in tests.
- Maintainability: If the ViewModel's dependencies change in the future, you only need to update the DI configuration, not every place where the ViewModel is created.
The rest of the code stays the same. The OnNavigatedTo() method still calls LoadAsync() to refresh the notes list when the user navigates to this page.
Update 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);
}
}
}
}
What changed since Step 2?
Several important changes integrate dependency injection and messaging features:
- ViewModel from DI container: The
NoteViewModelis now retrieved from the dependency injection container by usingApp.Current.Services.GetService<NoteViewModel>()in theOnNavigatedTo()method instead of being instantiated directly. This approach ensures the ViewModel automatically receives its requiredIFileServicedependency. - Message registration: The new
RegisterForDeleteMessages()method subscribes toNoteDeletedMessageby using theWeakReferenceMessenger. When a note is deleted (from theNoteViewModel.Delete()method), this page receives the message and navigates back to the all notes list by usingFrame.GoBack(). - Messaging pattern: This pattern demonstrates the loose coupling enabled by the MVVM Toolkit's messaging system. The
NoteViewModeldoesn't need to know about navigation or the page structure - it simply sends a message when a note is deleted, and the page handles the navigation response independently. - Lifecycle timing: The ViewModel is instantiated and message registration occurs in
OnNavigatedTo(), ensuring everything is properly initialized when the page becomes active.
This pattern separates concerns effectively: the ViewModel focuses on business logic and data operations, while the page handles UI-specific concerns like navigation.
Windows developer