Mustafa Can Yücel
blog-post-24

Creating Window Services with MVVM for Parameterized View Models

# Problem The default implementation of Service Locator pattern in *CommunityToolkit.MVVM* and *Microsoft.Services.DependencyInjection* does not allow parameterized view model instantiation: ```csharp var vm = App.Current.ServiceLocator.GetRequiredService<TViewModel>(); // cannot add any parameter ``` Moreover, if you want to create a `WindowService` that controls the open windows, and do not want to include the `Window` classes within the App logic (due to separation of concerns), things become complicated quite fast. Especially if your service interface definitions are in a separate platform-agnostic class library; you cannot add OS-dependent definitions. # Solution: Combining a Variety of Patterns ## Core `Core` is a separate class library project, its output will be a DLL file. It is also platform-agnostic; the same classes can be used in WPF, ASP.NET, or MAUI (mobile) environments. ### `Core.ViewModels.IViewModel` This interface is just for enforcing the type safety in view model factories and other services that deal with them: ```csharp namespace Core.ViewModels; public interface IViewModel; ``` ### `Core.Services.IWindowService` This is the interface that contains the definitions of window manipulation functions. Each platform-dependent concrete class will implement them based on their environment: ```csharp using Core.ViewModels; namespace Core.Services; public interface IWindowService { void ShowWindow<TViewModel>(int? id = null) where TViewModel : IViewModel; } ``` #### `ShowWindow` This function is responsible for displaying a *window* for the given view model type `TViewModel`. If this is a detail window, the `id` will contain the key for acquiring the object whose details will be displayed. Note that the `TViewModel` is constrained to be an instance of `IViewModel`. ### `Core.ViewModels.Factory.IViewModelFactory` This interface contains the definitions for creating a view-model that is compliant with the application requirements: ```csharp namespace Core.ViewModels.Factory; public interface IViewModelFactory { TViewModel Create<TViewModel>(int? id = null) where TViewModel : IViewModel; } ``` ### `Core.Services.ServiceExtensions.GetHash` This static function returns the combined hash of the name of a view-model class type, and an integer, if the given integer is not null. Otherwise, it returns the hash of the name. This hash allows to identify windows/view models that are of the same type but displaying different items. ```csharp namespace Core.Services; public static class ServiceExtensions { public static int GetHash(string viewModelName, int? id = null) => id.HasValue ? HashCode.Combine(viewModelName, id.Value) : viewModelName.GetHashCode(); } ``` ## WPF Application The WPF application, called *Desktop_Client* onwards, is the user application that is dependent on the *Core*. ### `Desktop_Client.ViewModels.Factory.ViewModelFactory` Implementing the `IViewModel` interface, this factory can create any view-model class for the given hash and item id: ```csharp using Core.Services; using Core.ViewModels; using Core.ViewModels.Factory; using Microsoft.Extensions.DependencyInjection; namespace Desktop_Client.ViewModels.Factories; public class ViewModelFactory(IServiceProvider serviceProvider) : IViewModelFactory { public TViewModel Create<TViewModel>(int? id = null) where TViewModel : IViewModel { int hash = ServiceExtensions.GetHash(typeof(TViewModel).Name, id); return id.HasValue ? ActivatorUtilities.CreateInstance<TViewModel>(serviceProvider, hash, id.Value) : ActivatorUtilities.CreateInstance<TViewModel>(serviceProvider, hash); } } ``` ### `Desktop_Client.Windows.Factory.IWindowFactory` This interface contains definitions on creating a window for a given view model. Since `Window` class is *Windows OS* dependent, this interface is defined within the WPF application: ```csharp using Core.ViewModels; using System.Windows; namespace Desktop_Client.Windows.Factory; public interface IWindowFactory { Window CreateWindowForViewModel<TViewModel>() where TViewModel : IViewModel; } ``` ### `Desktop_Client.Windows.Factory.WindowFactory` This class contains the concrete implementation of the `IWindowFactory` interface. It uses the view model - window type mappings that is defined within the `ServiceLocator` itself, so this dictionary is supplied via constructor injection: ```csharp using Core.ViewModels; using Serilog; using System.Windows; namespace Desktop_Client.Windows.Factory; public class WindowFactory(IDictionary<Type, Type> viewModelWindowMappings) : IWindowFactory { public Window CreateWindowForViewModel<TViewModel>() where TViewModel : IViewModel { var viewModelType = typeof(TViewModel); if (!viewModelWindowMappings.TryGetValue(viewModelType, out var windowType)) { logger.Error("No window type found for view model type {ViewModelType}", viewModelType); throw new InvalidOperationException($"No window type found for view model type {viewModelType}"); } if (!typeof(Window).IsAssignableFrom(windowType)) { logger.Error("Window type {WindowType} is not a Window", windowType); throw new InvalidOperationException($"Window type {windowType} is not a Window"); } if (Activator.CreateInstance(windowType) is not Window window) { logger.Error("Failed to create window of type {WindowType}", windowType); throw new InvalidOperationException($"Failed to create window of type {windowType}"); } return window; } private readonly ILogger logger = Log.Logger; } ``` ### `App` Class The registration of the services defined above is included within the `App` class: ```csharp using Core.Services; using Core.ViewModels.Factory; using Desktop_Client.Services; using Desktop_Client.ViewModels; using Desktop_Client.ViewModels.Factories; using Desktop_Client.Windows; using Desktop_Client.Windows.Factory; using Microsoft.Extensions.DependencyInjection; using Serilog; using System.Windows; namespace Desktop_Client; /// <summary> /// Interaction logic for App.xaml /// </summary> public partial class App : Application { public IServiceProvider ServiceProvider { get; } public new static App Current => (App)Application.Current; public App() { // configure the service provider ServiceProvider = ConfigureServices(); } private static ServiceProvider ConfigureServices() { ServiceCollection services = new(); // Services services.AddSingleton<IWindowService, WindowService>(); // Factories services.AddSingleton<IViewModelFactory, ViewModelFactory>(); services.AddSingleton<IWindowFactory, WindowFactory>(); // Mappings services.AddSingleton<IDictionary<Type, Type>>(sp => new Dictionary<Type, Type> { { typeof(HomeViewModel), typeof(HomeWindow) }, ... }); return services.BuildServiceProvider(); } protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); var windowService = ServiceProvider.GetRequiredService<IWindowService>(); windowService.ShowWindow<HomeViewModel>(); } } ``` The `OnStartup` override displays how to use this pattern; once you get an instance of `ServiceProvider` (through `App.Current`), you can just call `windowService.ShowWindow<TViewModelClass>()`. It will generate a view model, create a window, connect them, and handle the rest.