Silverlight MVVM + dependency injection
Update 2009/03/04: Here are 2 links that may help you with your understanding of MVVM
first 2 videos about implementing MVVM, one in WPF and the other in Silverlight http://weblogs.asp.net/craigshoemaker/archive/2009/02/26/hands-on-model-view-viewmodel-mvvm-for-silverlight-and-wpf.aspx and an article that delves into creating a fully functional app http://www.codeproject.com/KB/smart/Sonic.aspx
I attended a talk Jonas did last week on MVVM and it got me excited enough to explore it myself. His blog has a wealth of information, and it has all been summarised in his latest post. By reading his blog and many other bits and pieces around the web i was able to get this working myself. I'm going to put it all together into one blog post to give a complete look at how to incorporate all of it together. I aim to show how it all hooks in together and not just show a diagram saying that you should split things out.
Presumably you already have an idea of what Model/View/ViewModel is. The background and reasoning behind it is beyond the scope of this article, but here is an article I recommend. And also all of Jonas's articles. But basically MVVM + Dependency injection allows your applications to be loosely coupled with the GREAT advantage that all of your code can now be unit tested, since the UI is just databinding to your classes.
Architecture
The application is going to look like the diagram. It will be split up into the usual Model/View/ViewModel components, but I aim to show all of the supporting code that is needed to get it all working.
The application will be a HR system which just show a list of all the employees. I am just going to implement this single method to try and keep this example as simple as possible
View
Page.Xaml. This is a normal silverlight XAML page with our UI components. However we are going to get it databind to our ViewModel and have it auto refresh itself when the data changes rather than doing this by ourselves manually
Model
This is our underlying data that we want to our application to somehow show. Here it will be our HR system. This worries about how to retrieve/add/modify the data.
ViewModel
This sits between the Model and the View. The point of this is to give a specific 'View and state' of our Model to the view. The View just wants to display data, it doesn't want to care about how to retrieve things, so the ViewModel handles all of this for us.
Dependency Injection
Rather than hard coding all of the connections between the Model, View & ViewModel. We can use 'Dependency Injection' to insert the class for us. This allows us to be able to use other classes when unit testing, or at designtime vs. runtime.
We will use DI to connect the ViewModel to the View(Page). And to connect the Model to our ViewModel. I have created a few versions of the model which we can swap around.
Implementation
Employee
Just a simple class that contains age/name/etc.
public class Employee { public int Id { get; set; } public string Name { get; set; } public string Occupation { get; set; } public int Age { get; set; } }
IHRSystem
We are programming against an interface, this way we can swap the class that does the actual work.
public interface IHRSystem { ObservableCollection<Employee> GetEmployees(); }
This is going to be implemented 3 times as:
- HRSystemSQL
- HRSystemMemory
- MockHRSystem
They all should be self explanatory. But HRSystemSQL retrieves the results from a SQL database. HRSystemMemory gets it from a list in memory. And MockHRSystem is used for sending through precanned data for testing purposes or for our designtime experience.
So we can see that we can switch out the actual implementation of our Model and it will still work as we are programming against an interface. We don't care if we are retriving from a database, or from a webservice, etc.
Here is the implementation of the mock class that will be used to display data for us at design time in blend, or when we are running unit tests
public class MockHRSystem : IHRSystem { public ObservableCollection<Employee> GetEmployees() { return new ObservableCollection<Employee>{ new Employee{Id=1, Name="John Smith", Age=31, Occupation="IT Manager"}, new Employee{Id=2, Name="Jane Smith", Age=43, Occupation="Help desk"}}; } }
PageViewModel
The this is the 'ViewModel' for Page.Xaml, hence the name PageViewModel... This needs to do 3 things.
- Expose as properties the data that we want the view to databind against
- Have the constructor setup to pass in the implementation of the model class that we want to use (by using dependency injection)
- make our properties raise PropertyChanged events to tell the View to update when the data changes
public class PageViewModel : INotifyPropertyChanged { private IHRSystem hrSystem; //Where do we get the data from? private ObservableCollection<Employee> employees; //The data we are holding so that the View can be databound public ObservableCollection<Employee> Employees //It is exposed as a property that fires property changed events { get{return employees;} set { if (value != employees) { employees = value; RaisePropertyChanged("Employees"); } } } //Using dependency injection to tell us which implementation of the model we are using [Inject] public PageViewModel(IHRSystem HRSystem) { hrSystem = HRSystem; employees = hrSystem.GetEmployees(); } public event PropertyChangedEventHandler PropertyChanged; //Raise events when our data changes so that the view knows to update itself. protected void RaisePropertyChanged(string propertyName) { PropertyChangedEventHandler propertyChanged = this.PropertyChanged; if (propertyChanged != null) { propertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } }
MVVMTestModule
We're done with most of the code required to actually get the data that we want. Now we need to say how to wire it all up, since we haven't used any code in the code behind files. First we are going to define how the dependency injection is setup in the application. I am using Ninject to handle this, so it will look like this
public class MVVMTestModule : StandardModule { public override void Load() { bool isBrowser = HtmlPage.IsEnabled; bool isBlend = !isBrowser; Bind<IHRSystem>().To<HRSystemMemory>().OnlyIf(c => (isBrowser)); Bind<IHRSystem>().To<MockHRSystem>().OnlyIf(c => (isBlend)); Bind<PageViewModel>().ToSelf(); } }
To simplify things, I'm avoiding using the SQL implementation and will just be holding a list in memory by using the HRSystemMemory implementation.
ServiceLocator
We are using dependency injection, but we still need to tell silverlight once where to look for the class that will handle all of the dependency injection niceness for us. Jonas does a great job explaining why we have one, but here is what we will use in this example
Silverlight is now able to get Ninject to pass to us our new PageViewModelClass that has been setup with the correct Model being passed in (Mock or a proper implementation) based on if we are in blend or running the app.
public class ServiceLocator { private static IKernel kernel; public PageViewModel PageViewModel { get{return kernel.Get<PageViewModel>();} } public static T Get<T>() { return kernel.Get<T>(); } static ServiceLocator() { if (kernel == null) { kernel = new StandardKernel(new MVVMTestModule()); } } }
App.Xaml
The ServiceLocator class we just created, we need to set it up as an application wide resource, so that any part of our application can access the DI framework.
<Application .... xmlns:MVVMTest="clr-namespace:SilverlightMVVMTest1"> <Application.Resources> <MVVMTest:ServiceLocator x:Key="serviceLocator"/> </Application.Resources>
Page.Xaml
Finally we are at the end!
Create our View using a ListBox to display our List of data. Format it how we like with a datatemplate. but now we bind our entire view to our PageViewModel class and have the UI display it for us.
<UserControl ... DataContext="{Binding Path=PageViewModel, Source={StaticResource serviceLocator}}"> <Grid x:Name="LayoutRoot" Background="White"> <ListBox ItemsSource="{Binding Path=Employees}">
Further work
This was just a quick rundown on how to create a complete MVVM application that is fully testable. New concepts weren't meant to be introduced here, just showing how it all works together. As shown in the initial diagram, converters can be used to do more complex mappings between the ViewModel and the View. We still haven't said how to send events through (like clicking on a button to refresh the data, or adding new data). If there is enough interest, I'll tie that into this example in another blog post