Silverlight - code behind back to MVVM
/Philosophical difference between code-behind and the ViewModel
Because VS.NET is firstly a visual tool, it tries very hard to give developers an easy starting point where you can create your layout and easily add code to various events.
The problem this creates, is that the end result code in the events is a mixture of two type of code:
- Data access, relating to service calls, maintaining in memory state of the object.
- Updates to the UI.
The former is what we’re trying to test, whereas the UI is always difficult to test, especially if compounded with animation or frequent changes to the UI.
Why do we need to do this?
- Code-behind is part of the View, application logic doesn't belong here
- Prolonged mixing of application logic WITH UI login in the code-behind leads to spaghetti code, the main issue is maintenance cost
- Unable to easily separate and test functionality
Refactor and take control - bring Code Behind back into MVVM
I tackle this problem in stages, without stages the problem is too big and you may not be able to stop halfway.
Stage 1 - creating a ViewModel
A ViewModel is a model for a view. So in your ViewModel you should have any information that is necessary for the UI (view) to render properly. Remember to implement INotifyPropertyChanged
public class MainViewModel : INotifyPropertyChanged { }
Add the ViewModel to your View - this can be done in the XAML
<UserControl.Resources> <ViewModel:MainViewModel x:Key="MainViewModelInstance"/> </UserControl.Resources>
You can reference the ViewModel in your code behind this way:
var resourceMainViewModel = this.Resources["MainViewModelInstance"]; _mainViewModel = resourceMainViewModel as MainViewModel;
Stage 2 - moving application state variables to ViewModel
A typical sign of a lot of code-behind is when we have a lot of member variables in the View.
- A majority of them can be moved directly into the ViewModel.
- Provide property get/set on the ViewModel and re-wire the Code-behind logic to use the ViewModel for now.
Exercise some judgement here - not all variables should go to the ViewModel, references to UI elements for example, should remain in the code-behind (view).
Stage 3 - moving application methods to ViewModel, hooking UI events
Once you've moved your variables to the ViewModel - then you can start looking at moving the methods to the ViewModel.
The trickiest part here, if you have started with code-behind, is that there will be methods that have a mixture of application logic along with UI update calls.
An easy, intermediate way to handle these UI calls is to have the VM raise an event at the specific time, and have the View bind to those events and make the UI updates.
Change any UI update code to UI binding syntax - this should be the normal for most VM entity or collections.
Stage 4 - you can start creating Silverlight Unit Tests
By stage 3 - you should have more and more application logic moved back to the ViewModel. Add a Silverlight Unit Testing project and start instantiating the ViewModel and test the collections and entities can be populated correctly.
From this point onwards because you have the unit tests available you can start to do a lot more refactoring with the methods without worrying that you'll be breaking stuff.
Stage 5 - calling application methods via Commands
Create a DelegateCommand class - unfortunately one isn't provided with Silverlight (I don't know why)
public class DelegateCommand : ICommand { Func<object, bool> canExecute; Action<object> executeAction; bool _canExecuteCache; public DelegateCommand(Action<object> executeAction, Func<object, bool> canExecute) { this.executeAction = executeAction; this.canExecute = canExecute; } public DelegateCommand(Action<object> executeAction) : this(executeAction, null) { } #region ICommand Members public bool CanExecute(object parameter) { if (canExecute == null) { return true; } bool temp = canExecute(parameter); if (_canExecuteCache != temp) { _canExecuteCache = temp; if (CanExecuteChanged != null) { CanExecuteChanged(this, new EventArgs()); } } return _canExecuteCache; } public event EventHandler CanExecuteChanged; public void Execute(object parameter) { executeAction(parameter); } #endregion }
In the ViewModel - change the direct method call from the view into a property DelegateCommand, then in the View, change the Click event handler to Command property via Binding to the ViewModel.DelegateCommand
Stage 6 - break UI event hooks by binding storyboards via triggers
This is by far the most trickiest - depending on what sort of UI updates you had in the code-behind, it may be very tricky to convert them into storyboards.
Do your best, but don't go overboard on this one - I don't have a good all-round-fix for this one yet.