Windows Template Studio — Extending MVVM Basic with Dependency Injection
Windows Template Studio is a great tool for starting a UWP application. One of the options you are presented with when setting up the project is deciding which design pattern to follow. You have 5 options to choose from.
Code Behind is good for a small, simple application. MVVM Basic is a better approach to make your code more testable by separating the view from the code logic. And there is also currently support for 3 different open source frameworks built on the MVVM principal: MVVM Light, Caliburn.Micro and Prism.
In this article I am going to show you how to start with MVVM Basic and add an important feature found in these other frameworks: Dependency Injection.
Why Not Just Use a Framework?
The three frameworks listed are all somewhat popular MVVM frameworks with different history behind them and different futures ahead of them. Check out their respective repositories on GitHub for stars, activity and road maps. It can be hard to decide which one is best for your project, especially if you have not used the MVVM pattern before. Each one has a slightly different way of doing things, with its own pros and cons.
A good way to help you decide is to get a better understanding of a simple implementation of MVVM.
That’s what the MVVM Basic implementation in Windows Template Studio is designed to do. It is missing a couple of features that make it a more robust MVVM framework. The first is Dependency Injection, which I will cover here. The other, less frequently needed, but important for more complex applications is messaging. I’ll cover that in my next article.
What is Dependency Injection?
Okay, just the high level here. When you use the new keyword to initialize a variable inside of a class, you are creating a dependency between those two classes. Or as Steve Smith would say, “New is glue.” If instead you pass that variable into the class, say in a constructor, or the method as an interface, you remove the dependency. Later you can pass a different implementation of that interface without changing the code in the class.
This will help a lot with automated testing. For example, you can create an ISampleService interface that is passed into a class, and implement a class called SampleService that talks to a real web API in the app, and a class called FakeSampleService that uses hard coded data for your tests.
What is Dependency Injection C#? goes into more detail.
Starting From Scratch
I will be using Visual Studio 2019 for this example and version 3.5.19310.1 of Windows Template Studio.
- Create a new project and choose Windows Template Studio (Universal Windows) C# as the template.
- I will be naming the project MvvmBasicPlus, but of course you can use your own project name.
- I will be leaving the project type as Navigation Pane.
4. For Design pattern you must pick MVVM Basic.
5. Add a page type of Content Grid. That is where we will be focusing our example.
6. There is no need to add additional features for this example. Just select Create.
Changing the Sample Data Service
For dependency injection, we don’t want to use a static service class.
Remove the static keyword from the SampleDataService class and the GetContentGridDataAsync method.
public class SampleDataService
{
... public async Task<IEnumerable<SampleOrder>> GetContentGridDataAsync()
{
...
Then in the ContentGridViewModel replace the static call with an instance defined call.
public ObservableCollection<SampleOrder> Source
{
get
{
// TODO WTS: Replace this with your actual data
return new SampleDataService().GetContentGridDataAsync();
}
}
And also the ContentGridDetailViewModel. This will fix these for now and get it to compile and run.
public void Initialize(long orderId)
{
var data = new SampleDataService().GetContentGridDataAsync();
Item = data.First(i => i.OrderId == orderId);
}
Adding an Interface
Add a interface called ISampleDataService. In the SampleDataService.cs file is fine.
public interface ISampleDataService
{
Task<IEnumerable<SampleOrder>> GetContentGridDataAsync();
}
And update the SampleDataService class to implement this interface.
public class SampleDataService : ISampleDataService
In the ContentGridViewModel class we need to define a private variable for sample data service.
public class ContentGridViewModel : Observable
{
private ISampleDataService _sampleDataService;
...
}
We are not using dependency injection yet, but we are setup to do so now.
Dependency Injection
Update the view model to accept an ISampleDataService variable in the constructors instead of instantiating a new one.
public ContentGridViewModel(ISampleDataService sampleDataService)
{
_sampleDataService = sampleDataService;
}
And update the page to pass in the implementation of the interface.
public sealed partial class ContentGridPage : Page
{
public ContentGridViewModel ViewModel { get; } = new ContentGridViewModel(new SampleDataService());
...
We now have achieved dependency injection and we can instantiate a view model with different implementations of the service from pages and from tests.
But we can go one step farther and define the SampleDataService as the implementation we always want to use throughout the application when we pass in the ISampleDataService interface.
Which brings us to …
Inversion of Control
To make this as easy as possible, and not use any third party libraries I am going to use the Microsoft.Extensions.DependencyInjection NuGet library. So start by adding that to the MvvmBasicPlus (Universal Windows) project.
Next we are going to update the App.xaml.cs file, since this is where the application starts and that is when we want to define our dependencies.
Below is the complete update with code changes highlighted. I will explain these changes next.
public sealed partial class App : Application
{
private Lazy<ActivationService> _activationService;
private ActivationService ActivationService
{
get { return _activationService.Value; }
}
public App()
{
InitializeComponent();
// Deferred execution until used. Check https://msdn.microsoft.com/library/dd642331(v=vs.110).aspx for further info on Lazy<T> class.
_activationService = new Lazy<ActivationService>(CreateActivationService);
}
protected override async void OnLaunched(LaunchActivatedEventArgs args)
{
Container = RegisterServices();
if (!args.PrelaunchActivated)
{
await ActivationService.ActivateAsync(args);
}
}
protected override async void OnActivated(IActivatedEventArgs args)
{
await ActivationService.ActivateAsync(args);
}
private ActivationService CreateActivationService()
{
return new ActivationService(this, typeof(Views.MainPage), new Lazy<UIElement>(CreateShell));
}
private UIElement CreateShell()
{
return new Views.ShellPage();
}
public IServiceProvider Container { get; private set; }
private IServiceProvider RegisterServices()
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddTransient<ContentGridDetailViewModel>();
serviceCollection.AddTransient<ContentGridViewModel>();
serviceCollection.AddSingleton<ISampleDataService, SampleDataService>();
return serviceCollection.BuildServiceProvider();
}
}
The RegisterServices method is basically adding each view model as a transient service (new with each request) and each service as a singleton (one for the lifetime of the app). This service provider is defined on the App class so that it is available throughout the entire application.
Now we don’t need to define the constructor parameters, the service provider takes care of that for us.
Update the content grid page …
using Microsoft.Extensions.DependencyInjection;
using MvvmBasicPlus.ViewModels;
using Windows.UI.Xaml.Controls;
namespace MvvmBasicPlus.Views
{
public sealed partial class ContentGridPage : Page
{
public ContentGridViewModel ViewModel { get; } = (App.Current as App).Container.GetService<ContentGridViewModel>();
public ContentGridPage()
{
InitializeComponent();
}
}
}
and the content grid detail page.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Toolkit.Uwp.UI.Animations;
using MvvmBasicPlus.Services;
using MvvmBasicPlus.ViewModels;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Navigation;
namespace MvvmBasicPlus.Views
{
public sealed partial class ContentGridDetailPage : Page
{
public ContentGridDetailViewModel ViewModel { get; } = (App.Current as App).Container.GetService<ContentGridDetailViewModel>();
public ContentGridDetailPage()
{
InitializeComponent();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
if (e.Parameter is long orderId)
{
ViewModel.Initialize(orderId);
}
}
protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
{
base.OnNavigatingFrom(e);
if (e.NavigationMode == NavigationMode.Back)
{
NavigationService.Frame.SetListDataItemForNextConnectedAnimation(ViewModel.Item);
}
}
}
}
Now if you ever decide to support more than one type of sample data service, like REST or GraphQL, you only have to update it on one place.
A complete working example of this project can be found here on GitHub.