Based on the foundation from last time, we can now turn our attention to our view-models. How can we use our dependency injection framework to construct each view model on demand?

As a starting point, our CreateContainer() method needs to register all our ViewModelBase implementations:

// Register ViewModels
container.Collection.Register<ViewModelBase>(desktopAssembly);

Given a state instance, we want a ViewModel instance that wraps that state. In order to achieve this, we need a factory method with a signature like this:

public ViewModelBase Create<T>(T state) { ... }

How does this factory method know which view model to create? I’ve decided to go with an approach based on a simple naming convention.

The names of the model type and view model type are related by a simple transformation: the suffix ViewModel will be added to the name of the model type. Optionally, the last word will be removed from the name of the model type before the suffix is applied.

Model Type Transformation View Model Type
VocabularyWord + "ViewModel" VocabularyWordViewModel
VocabularyBrowserScreen - "Screen" + "ViewModel" VocabularyBrowserViewModel
AddVocabularyWordScreen - "Screen" + "ViewModel" AddVocabularyWordViewModel

Let’s create a service class called ViewModelFactory that we’ll inject anywhere we need to be able to create view models. We begin with a method to find the appropriate view model class for a given model type:

public static Type FindViewModelType(Type modelType)
{
    var desktopAssembly = typeof(ViewModelBase).Assembly;
    var modelTypeName = modelType.Name;
    var viewModelTypeNames = new HashSet<string>()
    {
        modelTypeName+"ViewModel",
        modelTypeName.ReplaceSuffix("ViewModel")
    };

The set viewModelTypeNames contains all of the candidate type names that we’re looking for. Using a set in this fashion allows us to easily extend the naming convention in the future.

We now search for one of those types. Using SingleOrDefault() ensures that we’ll throw an exception if multiple matches are found.

    var viewModelType = (
        from type in desktopAssembly.GetExportedTypes()
        where type.IsClass && !type.IsAbstract
        where viewModelTypeNames.Contains(type.Name)
        select type
    ).SingleOrDefault();

    return viewModelType;
}

We can now write the factory method described above:

public ViewModelBase Create<T>(T state)
    where T : class
{
    var modelType = state.GetType();
    viewModelType = FindViewModelType(modelType);

    if (viewModelType == null)
    {
        throw new InvalidOperationException(
            $"Failed to find ViewModel class for model type '{modelType.Name}'.");
    }

    return (ViewModelBase)_container.GetInstance(viewModelType);
}

In the interests of brevity, and to focus on the actual implementation logic, I’ve elided both parameter checking and caching from the method above.

Note that we don’t have to explicitly register this factory class with our container; automatic registration would take care of this for us. However, the documentation for SimpleInjector reads:

Tip: Even though Simple Injector can create a concrete type even if it hasn’t been registered explicitly in the container, best practice is to register all types explicitly in the container.

So we’ll add the following into CreateContainer():

// Register Factories
container.RegisterSingleton<ViewModelFactory>();

Being explicit about having just one shared instance of ViewModelFactory across the whole application is useful - not least because that maximizes the usefulness of caching the results of our type lookup.

Our next step is to create a view model to represent the entire application - so far we’ve bypassed that, pushing the view model for a particular screen into place manually.

public class WordTutorViewModel : ViewModelBase
{
    private readonly IReduxStore<WordTutorApplication> _store;
    private readonly ViewModelFactory _factory;

Our constructor gains an additional parameter, allowing the factory to be injected.

    public WordTutorViewModel(
        IReduxStore<WordTutorApplication> store,
        ViewModelFactory factory)
    {
        _store = store;
        _factory = factory;

        UpdateFromStore();
    }

Our CurrentScreen property is a little unusual. The various view models used for different screens don’t implement IEquatable<T>, so we can’t use our existing UpdateProperty() method. There’s no value in adding IEquatable<T> because we’ll only be changing screens when our underlying application state changes; so we implement the property ourselves.

    private ViewModelBase _currentScreen;

    public ViewModelBase CurrentScreen
    {
        get => _currentScreen;
        set
        {
            if (!ReferenceEquals(value, _currentScreen))
            {
                _currentScreen = value;
                OnPropertyChanged(nameof(CurrentScreen));
            }
        }
    }

To finish, we have a method that updates CurrentScreen, using our factory to create the appropriate view model to suit the active screen.

    private void UpdateFromStore()
    {
        CurrentScreen = _factory.Create(_store.State.CurrentScreen);
    }
}

Our application entry point Main() also gets an update; it now uses the container to create our global view model. We hook it up to the screen by assigning it to the DataContext property of the main window.

var mainModel = container.GetInstance<WordTutorViewModel>();
var mainWindow = new MainWindow();
mainWindow.DataContext = mainModel;

Next time, we’ll wire up dependency injection for our views. This will allow us to substantially simplify the code in Main().

Prior post in this series:
Dependency Injection: Core
Next post in this series:
Dependency Injection: Views

Comments

blog comments powered by Disqus