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.