Designing a UI is persnickety. You want it to look just right. But what it looks like is often a function of the data it’s displaying. For example, the height of a WPF ListView may depend on how many items it’s displaying and what their internal state is.
Getting the right data to model at design-time is rarely as easy as simply creating an instance of the view model the UI element uses at run-time. That’s because the required data may not be known at design-time, may be expensive to obtain, etc.
So what you do is create a mock up of the run-time viewmodel used only at design-time. Without dependency injection and a composition root that would involve specifying things like design-time only DataContext
objects1. That’s the traditional approach…but there’s a better way.
J4JViewModelLocator lets you register, with the dependency injection system, different classes for the same viewmodel interface, one for use at design-time and one for use at run-time. And the locator is “aware” of what state it’s running in, so it serves up instances of the correct class. That means UI elements can simply refer to, for example, “the” DataContext without worrying which particular one — design-time or run-time — is required.
You configure the viewmodels like this (details removed for clarity):
public class CompositionRoot : J4JViewModelLocator<J4JLoggerConfiguration> { public IMainViewModel MainViewModel => Host!.Services.GetRequiredService<IMainViewModel>(); public IProcessorViewModel ProcessorViewModel => Host!.Services.GetRequiredService<IProcessorViewModel>(); public IOptionsViewModel OptionsViewModel => Host!.Services.GetRequiredService<IOptionsViewModel>(); public IRouteDisplayViewModel RouteDisplayViewModel => Host!.Services.GetRequiredService<IRouteDisplayViewModel>(); public IRouteEnginesViewModel RouteEnginesViewModel => Host!.Services.GetRequiredService<IRouteEnginesViewModel>(); protected override void RegisterViewModels( ViewModelDependencyBuilder builder ) { base.RegisterViewModels( builder ); builder.RegisterViewModelInterface<IMainViewModel>() .DesignTime<DesignTimeMainViewModel>() .RunTime<MainViewModel>(); builder.RegisterViewModelInterface<IOptionsViewModel>() .DesignTime<DesignTimeOptionsViewModel>() .RunTime<OptionsViewModel>(); builder.RegisterViewModelInterface<IRouteDisplayViewModel>() .DesignTime<DesignTimeRouteDisplayViewModel>() .RunTime<RouteDisplayViewModel>(); builder.RegisterViewModelInterface<IRouteEnginesViewModel>() .DesignTime<DesignTimeRouteEnginesViewModel>() .RunTime<RouteEnginesViewModel>(); builder.RegisterViewModelInterface<IProcessorViewModel>() .DesignTime<DesignTimeProcessorViewModel>() .RunTime<ProcessorViewModel>(); } }
Line 3 – 7 define the viewmodels that will be furnished on demand.
The override starting at line 9 configures the view model locator to map design-time and run-time concrete classes to the same interface. The locator will provide the correct concrete class depending upon the mode — design-time or run-time — in effect.
Using this in a UI element XAML file is as simple as this (details removed for clarity):
<UserControl x:Class="J4JSoftware.GeoProcessor.RouteDisplayTabItem" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" DataContext="{Binding RouteDisplayViewModel, Source={StaticResource ViewModelLocator}}"> <Grid>
Line 6 links everything together and makes the correct implementation of IRouteDisplayViewModel available to the DataContext.
that’s what the “d:” in “d:DataContext=…” means when you see it in XAML code: “use an instance of this object during design-time” ↩