In this part we will build the basic view model system, so we can display pages and start with a simple unit test.
The idea of a view model layer is to move the logic from the view layer (the XAML files) to a new layer to make it testable (and potentially reusable – even in other frameworks like ASP.NET for example).
And this idea comes with a new suggestion:
Suggestion #3: Avoid code in code behind files (xaml.cs).
Because the xaml files (and the code behind files) don’t exist in a unit testing environment, everything that resides in the XAMLs will not be unit-testable (and not portable)! (But you will see in a couple of minutes that there can be reasons why one should put code in code behind files.)
To be able to automatically test complete use cases involving several windows, we need to make sure that every window, every control that the application creates, is actually created in the view model layer. The view (XAMLs/WPF) only reacts to newly created view models and in turn creates the corresponding windows or controls.
This leads to the next suggestion:
Suggestion #4: Every UI element must actually be created in the view model.
WPF has a very nice mechanism built in that makes this task very simple. With DataTemplates you can assign a view element (actually any XAML Code) to a view model class.
Let’s start with a very simple example. At the moment the main window includes the MainWindowControl directly:
<Window x:Class="SimpleMVVM.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" SizeToContent="WidthAndHeight" xmlns:View="clr-namespace:SimpleMVVM" WindowStartupLocation="CenterScreen" Title="MainWindow" Height="350" Width="525"> <Border Style="{StaticResource ContentBorder}"> <View:MainWindowControl /> </Border> </Window>
For this MainWindowControl we’ll first need a view model class. Since we will be having a couple of view model classes, we should think about a design.
View model class hierarchy
All those view models cry for a base class (ViewModelBase). And since we have pages we also should have a page class (PageVM). That leads us to the following class hierarchy:
Create a new folder („ViewModels“) in the solution where we put all the view model classes. Then create the ViewModelBase class and the PageVM class:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace SimpleMVVM.ViewModels { public class ViewModelBase { } }
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace SimpleMVVM.ViewModels { public class PageVM : ViewModelBase { } }
The Page Frame
You might have recognized that all pages (except the main menu page) have a „MainMenu“ button which takes the user back to the main menu. Actually this leads to a concept of a controller or a page frame. Speaking UI wise, we have the main window which contains a page frame which in turn hosts the pages:
MainWindow
|
The page frame is static and responsible for the handling of the main menu button and of course it is derived from ViewModelBase. And it will have a member to hold the current page:
So, create the PageFrameVM class and add a property for the current page:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace SimpleMVVM.ViewModels { public class PageFrameVM : ViewModelBase { private PageVM _currentPage; public PageVM CurrentPage { get { return _currentPage; } set { _currentPage = value; } } } }
Finally create the view model class of the main menu page:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace SimpleMVVM.ViewModels { public class MainMenuPageVM : PageVM { } }
That’s enough for now. Now we’ll create the class that holds everything together.
Application View Model
In most cases it is a good idea to create a „application view model class“ (or a „System“ class). This class represents the application and is the entry point of the whole system. This is also the class that will be initially created in a unit test. Create a new class named SimpleMVVMApp and add a public Init() method to it:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace SimpleMVVM.ViewModels { public class SimpleMVVMApp { public void Init() { } } }
(If you want to be more general and stay away from the idea of an „App“, you could call it SimpleMVVMSystem.)
Since this class is supposed to act as the main container holding the page frame it needs a property for the frame:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace SimpleMVVM.ViewModels { public class SimpleMVVMApp { private PageFrameVM _pageFrame = new PageFrameVM(); public PageFrameVM PageFrame { get { return _pageFrame; } set { _pageFrame = value; } } public void Init() { } } }
Since the application actually starts with this class, it is supposed to open the first window – our MainMenuPage. This basically means that we will have to create an object of our view model class and display it by making it the CurrentPage of the PageFrame:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace SimpleMVVM.ViewModels { public class SimpleMVVMApp { private PageFrameVM _pageFrame = new PageFrameVM(); public PageFrameVM PageFrame { get { return _pageFrame; } set { _pageFrame = value; } } public void Init() { MainMenuPageVM mainMenuPage = new MainMenuPageVM(); PageFrame.CurrentPage = mainMenuPage; } } }
View-ViewModel-Connection
Now it’s time to connect the view layer to our view model layer. Basically we have to create an object of type SimpleMVVMApp „somewhere“. There are a couple of spots where you can put this: the constructor of the App class, or the constructor of the main window etc. To make sure that WPF (and the main window) is fully initialized, we will put it in the event handler of the Loaded event of the main window:
using System.Windows; using SimpleMVVM.ViewModels; namespace SimpleMVVM { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); Loaded += new RoutedEventHandler(MainWindow_Loaded); } private void MainWindow_Loaded(object sender, RoutedEventArgs e) { SimpleMVVMApp app = new SimpleMVVMApp(); app.Init(); Content = app.PageFrame; } } }
When you start the application now, you will see this:
What happens is, that WPF does not know how to display this class and so it simply uses ToString() – and that’s what you see: the class name.
If you now add two DataTemplates to the App.xaml file – one for the page frame and one for the first page…:
<Application x:Class="SimpleMVVM.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:View="clr-namespace:SimpleMVVM" xmlns:VM="clr-namespace:SimpleMVVM.ViewModels" StartupUri="MainWindow.xaml"> <Application.Resources> <DataTemplate DataType="{x:Type VM:PageFrameVM}"> <Border Style="{StaticResource ContentBorder}"> <ContentPresenter Content="{Binding CurrentPage}" /> </Border> </DataTemplate> <DataTemplate DataType="{x:Type VM:MainMenuPageVM}"> <View:MainMenuPage /> </DataTemplate> </Application.Resources> </Application>
…and start the application again, you will see this:
WPF picked the control in the DataTemplate to display it inside the main window. The content is actually not our view model object but the control! And the DataContext of the control has automatically been set to the view model object. That means that bindings in the XAML file will automatically be connected to view model properties. This is the basic setup of a view/view model connection.
Note: You should also remove any content from the MainWindow.xaml file:
<Window x:Class="SimpleMVVM.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" SizeToContent="WidthAndHeight" xmlns:View="clr-namespace:SimpleMVVM" WindowStartupLocation="CenterScreen" mc:Ignorable="d" Title="MainWindow" > </Window>
Next step: SimpleMVVM, Part 4: The first test