Getting Started with MVVM
This post accompanies my Getting Started with MVVM Visual Studio Toolbox episode. In that episode, In that episode, I showed how to get started with MVVM in XAML apps. I showed a simple Windows Store app calls a WCF service that manages customers and projects data in a SQL Server database. The base version of the app uses no MVVM. I then showed five iterations of the app, each adding a piece of MVVM. The sample code for this includes all six projects.
Step 1: Create ViewModels
Issue: Code behind makes it difficult to test. How do you test data code? You have to run the app. It would be nice to break out the data into a separate unit of work. We want a separation between the views and the code that works with the data.
In this lesson, we create a ViewModel for each View. Each ViewModel has a property that represents the data on the corresponding page, so you can just bind to the ViewModel.
All the data access code has been moved into the ViewModels so you could unit test the data methods without having to run the app or finish the UI.
Move all data access code into ViewModels.
Steps:
- Create a ViewModels folder.
- Add ViewModelBase to it.
- Create MainViewModel, CustomerViewModel, ProjectsViewModel, ProjectViewModel. This is 1 VM per view.
- Each ViewModel inherits from ViewModelBase.
- Each ViewModel has data access code.
- Modify MainPage
- Page DataContext is set to MainViewModel rather than DefaultViewModel.
- No use of CollectionViewSource
- ItemsSource set to Customers property of ViewModel
- Bind SelectedItem of CustomersGridView to SelectedCustomer, Mode=TwoWay
- Add code to initialize MainViewModel
- LoadState calls MainViewModel.GetCustomers rather than WCF service
- Edit button passes mainViewModel.SelectedCustomer.Id to Navigate. No need to set App.CurrentCustomerId. It is set in VM.
- Modify CustomerPage
- Page DataContext is set to CustomerViewModel rather than DefaultViewModel.
- StackPanel DataContext set to SelectedCustomer property of ViewModel
- Bind PageTitle.Text to Title
- Add code to initialize CustomerViewModel
- LoadState, SaveButton_Click and DeleteButton_Click call CustomerViewModel methods rather than WCF service
- SaveState reads CustomerViewModel. SelectedCustomer.Id
- Modify ProjectsPage
- Page DataContext is set to ProjectsViewModel rather than DefaultViewModel.
- No use ofCollectionViewSource
- ItemsSource set to Projects property of ViewModel
- Bind SelectedItem of ProjectsGridView to SelectedProject, Mode=TwoWay
- Add code to initialize ProjectsViewModel
- LoadState calls ProjectsViewModel.GetProjects rather than WCF service
- Edit button passes mainViewModel.SelectedProject.Id to Navigate. No need to set App.CurrentProjectId. It is set in VM.
- Modify ProjectPage
- Page DataContext is set to ProjectViewModel rather than DefaultViewModel.
- StackPanel DataContext set to SelectedProject property of ViewModel
- Bind PageTitle.Text to Title
- Add code to initialize ProjectViewModel
- LoadState, SaveButton_Click and DeleteButton_Click call ProjectViewModel methods rather than WCF service
- BackButton_Click and SaveState reads ProjectViewModel.SelectedProject.Id
LoadState and button clicks no longer have data code. They call methods in the VMs that have the data code.
This is the bare minimum to be MVVM.
Benefits
- Data code is now testable outside UI
Step 2: Create Model and add Data Service
Issue: ViewModels talk directly to WCF service. What happens if you want to swap the data source? Perhaps you want to use an Azure Mobile Service, or Web API, or a service that returns JSON. Or perhaps you decide to stop calling a service and switch to entirely local data.
Issue: Our model has classes for Customer and Project, but they are contained in the service proxy code. If the data model is simple enough, and it is in this example, that is not a problem. However,
In this lesson, we will remove the data access code from the ViewModels. We will create a service layer that talks to the WCF service. The ViewModel calls the service layer. That way, if we want to change the method of data access, we only have to change code in the service layer. This also gives us the ability to unit test the data calls.
Steps:
- Where are Customer and Project defined? We don’t want to use the generated proxy classes, so let’s create our own model.
- Create a Models folder and create Customer and Project classes in it.
- Create a Services folder. This is where we will create the service that talks to the data.
- Create an Interfaces folder. This is another layer of abstraction. We will define an interface that the service implements.
- Create ICustomerDataService.
- Create a CustomerDataService class that implements ICustomerDataService. Move the data access code from the ViewModels into this class.
- Change the ViewModels to call the CustomerDataService class instead of the WCF proxy service class.
- Change references to WCF service to CustomerDataService
- Modify the XAML code behind to reference CustomerDataService rather than the WCF service
Benefits
- Model contains the data you use in the app, not the data returns from the service.
- Data code is now reusable across projects.
- Can swap data sources without modifying the ViewModels. More abstraction.
- Can test not only how the VM calls data but also how the data service talks to the actual data.
Step 3: Use Commands instead of Click event handlers
- Issue
- Click event handlers are in the View code behind. We want code related to entities to be in the corresponding ViewModel.
- Navigation code is not testable outside UI
Use Commands instead of Button Click handlers.
We can use RelayCommand class in Common folder rather to wire up commands.
Bind SelectedItem of GridView to SelectedCustomer/SelectedProject properties of ViewModels
Steps:
- Add Command code to each ViewModel
- Main: Add, Edit
- Customer: Save, Delete
- Projects: Add, Edit, GoBack
- Project: Save, Delete, GoBack
- Change Click to Command in XAML
- Main: Add, Edit
- Customer: Save, Delete
- Projects: Add, Edit, Back
- Project: Save, Delete, Back
- Delete Click handlers in code behind
- Main: Add, Edit
- Customer: Save, Delete
- Projects: Add, Edit, Back
- Project: Save, Delete, Back
Benefits:
- Navigation code is now testable outside of UI.
- More abstraction. Closer to ideal of View having no code and ViewModel having all non-data access code.
- One less place where View knows about the Model.
Step 4: Separate the View from the Model
Issues
- View knows about the Model. ItemClick in MainPage and ProjectsPage cast e.ClickedItem to Customer and Project so you can know the id of the customer or project you clicked.
- Navigation code is not testable outside UI
Ideally, we would use a Command here. There is code out there to wire up the ItemClicked to a Command, but how do you know what customer/project was clicked?
Call a public method of the ViewModel and pass e.ClickedItem as an argument. Admittedly a hack.
Steps:
- Customer
- Create NavigateToProjectsPage method in MainViewModel and move MainPage ItemClick code into it.
- Call NavigateToProjectsPage from ItemClick.
- Remove Models reference.
- Project
- Create NavigateToProjectPage method in ProjectsViewModel and move ProjectsPage ItemClick code into it.
- Call NavigateToProjectPage from ItemClick.
- Remove Models reference.
Benefits:
- Navigation code is now testable outside of UI.
- View does not know about the Model.
- One more place where testable code is moved from the View to the ViewModel.
Step 5 Wire up ItemClicks using generic RelayCommands
Issues
- We still have code behind the item click. Can we do better?
The RelayCommand in the Common folder does not enable us to pass parameters to it. We need a generic RelayCommand. Then we need code that will wire up ItemClick to that generic RelayCommand.
Steps
- Create Helpers folder.
- Create RelayCommand class that supports generics.
- Create GridViewItemClickCommand class that can wire up ItemClick to command.
- Add Click Command code to MainViewModel and ProjectsViewModel.
- Delete NavigateToCustomerPage and NavigateToProjectPage methods.
- Change Click to Command in MainPage and ProjectsPage XAML.
- Delete Click handlers in code behind.
Benefits:
- Cleaner abstraction. Less code behind.
- Navigation code is now testable outside of UI.
- One more place where testable code is moved from the View to the ViewModel.
Next Steps: Areas to explore
- The LoadState of each page calls a method of the VM to get data. Can we make that happen automatically?
- Should state saving and restoring code be in the VM rather than in the Views?
- We can use a ViewModelLocater and have the ViewModels automatically load in each page.
- The LoadState of each page displays the progress ring while we want for data to come down. If we move the calls to get data into the VM, how do we display and turn off the progress ring? The VM will need to call into the View to turn it on and off. How do we do that?
- And if we do that, we can move the code to enable and disable app bar buttons into the VMs as well. We would then have no code behind in the Views.
- Do we want to use a NavigationService rather than code the nav ourselves? Does that offer another layer of abstraction?