When consulting lots of other people about MVVM, I noticed that most people understand the basic concepts fairly easily. However, as soon as it becomes a little bit more complex, users immediately break the pattern and start making strange “solutions” to their problems.
The wrong “solution”
On of these problems is keeping the MVVM pattern in tact when a view does not support bindings. People don’t know how to handle the stuff, and basically come up with the following “solution” (it’s a real-world solution I’ve seen, but I’ve simplified it a bit and took the part where UI elements are used inside the view-model):
1: public class MainPageViewModel : ViewModelBase
2: {
3: public MainPageViewModel()
4: {
5: Navigate = new Command(OnNavigateCommand);
6: }
7:
8: public ICommand Navigate { get; private set; }
9:
10: private void OnNavigateCommand(string request)
11: {
12: FrameworkElement screenControl = GetScreenControl(request);
13:
14: if (screenControl == null)
15: {
16: screenControl = CreateScreenControl(request);
17:
18: DeskTopContent.Children.Add(screenControl);
19:
20: ObservableCollection<TaskBarItem> taskBarItemsList =
21: (ObservableCollection<TaskBarItem>)TaskBarItemsSource.Source;
22:
23: TaskBarItem item = new TaskBarItem(screenControl, request);
24: taskBarItemsList.Add(item);
25: TaskBarItems.MoveCurrentTo(item);
26:
27: ViewedControl = screenControl;
28: }
29: }
30:
31: /* other content left out for the sake of simplicity */
32: }
If you think the solution above is good, please keep reading (you actually need it).
When I see such code, I always ask two simple questions:
- What is the best argument for MVVM? (they probably answer: loosely coupled design and testability)
- How are you going to unit test the view model above? (they probably answer: not)
Exactly, the code above cannot be unit tested because it is actually just a code-behind instead of a view model (thus the developer decided to break the MVVM pattern for whatever reason he thought was acceptable). If you think this is valid MVVM, then stop calling it a view model and call it by its name: code-behind file.
The right solution
Sometimes, when using controls that do not support MVVM bindings, you want to implement something specific, but don’t break the pattern. An excellent example of this is to use the Image control as a button. There are other solutions such as the EventToCommand behavior to solve this specific case, but this example should be easy to understand for everyone. A different example (which is a bit more complex) is for example a GridView that returns IEnumerable<GridRow> instead of a model in the ItemsSource property of the grid. However, using this as an example would make it all a bit more complex than needed.
The Image class unfortunately has no Command property where we can bind a view model command to. So, how are we going to solve this problem without breaking the MVVM pattern? Let’s see how easy it is.
1) Fortunately, the Image does have MouseLeftButtonDown event. So, in our code-behind, we are going to subscribe to that event (or in xaml, whatever you prefer):
Xaml code (notice the OnLogoClicked for the Image):
1: <Grid>
2: <Grid.RowDefinitions>
3: <RowDefinition Height="Auto" />
4: <RowDefinition Height="Auto" />
5: </Grid.RowDefinitions>
6:
7: <Grid.ColumnDefinitions>
8: <ColumnDefinition Width="300" />
9: </Grid.ColumnDefinitions>
10:
11: <Label Grid.Row="0">
12: <TextBlock TextWrapping="Wrap">
13: This example shows how to apply the MVVM pattern in a valid way, even with
14: a non-bindable view such as using the Image control as a button (binding to a command).
15: </TextBlock>
16: </Label>
17:
18: <Image Grid.Row="1" Width="48" Height="48" HorizontalAlignment="Center"
19: Source="/RightMvvm;component/Resources/Images/Catel.png"
20: Cursor="Hand" MouseLeftButtonDown="OnLogoClicked" />
21: </Grid>
2) As soon as the event occurs, retrieve the view model from the DataContext and use that to invoke the command manually.
Code-behind:
1: private void OnLogoClicked(object sender, EventArgs e)
2: {
3: var viewModel = DataContext as MainWindowViewModel;
4: if (viewModel != null)
5: {
6: viewModel.ShowMessage.Execute();
7: }
8: }
In the code-behind, we retrieve the view model from the DataContext, and immediately invoke the command.
Be careful if you are not using Catel, there is probably a security leak in your implementation of the Command (DelegateCommand or RelayCommand). First check if CanExecute returns true before invoking Execute.
3) In our view model, we don’t rely on any UI aspect at all
1: public class MainWindowViewModel : ViewModelBase
2: {
3: #region Constructor & destructor
4: /// <summary>
5: /// Initializes a new instance of the <see cref="MainWindowViewModel"/> class.
6: /// </summary>
7: public MainWindowViewModel()
8: {
9: ShowMessage = new Command(OnShowMessageExecute);
10: }
11: #endregion
12:
13: #region Properties
14: /// <summary>
15: /// Gets the title of the view model.
16: /// </summary>
17: /// <value>The title.</value>
18: public override string Title { get { return "Non-bindable views in MVVM the right way!"; } }
19: #endregion
20:
21: #region Commands
22: /// <summary>
23: /// Gets the ShowMessage command.
24: /// </summary>
25: public Command ShowMessage { get; private set; }
26:
27: /// <summary>
28: /// Method to invoke when the ShowMessage command is executed.
29: /// </summary>
30: private void OnShowMessageExecute()
31: {
32: var messageService = GetService<IMessageService>();
33: messageService.Show("You just did it the right way!");
34: }
35: #endregion
36: }
Can you see how loosely coupled the view model still is? We can unit test it any way we want. We can mock the IMessageService, and test whether the command can be executed without relying on any view.
RightMvvm.zip (1.01 mb) [Downloads: 118]