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:
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:
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:
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.
We can now write the factory method described above:
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()
:
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.
Our constructor gains an additional parameter, allowing the factory to be injected.
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.
To finish, we have a method that updates CurrentScreen
, using our factory to create the appropriate view model to suit the active screen.
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.
Next time, we’ll wire up dependency injection for our views. This will allow us to substantially simplify the code in Main()
.
Comments
blog comments powered by Disqus