In my last post . I talked about navigation techniques in MVVM and how I didn’t like them too much. I also talked about learning Asp.NET Core is and how good the navigation / routing system is in there.
So, following the pain, I decided to create a lightweight navigation WPF framework similar to the one in Asp.NET Core. We’re still using MVVM, but adding controllers which makes it MVVMC, Model-View-ViewModel-Controller (This is how the library is called as well).
The code is available on GitHub .
Update 21/6/2018: I updated some features in the framework, added a NuGet package and added a thorough documentation on GitHub .
The following article is about using the navigation framework and how it implements the principles of MVVMC.
Getting started
To start with the MVVMC framework, simply install the NuGet Wpf.MVVMC to your WPF app.
Install-Package Wpf.MVVMC
Adding a Region
A Region is a Control with dynamic content. This is the part whose content you want changing when navigating. In my MainWindow:
<Window x:Class="MainApp.MainWindow"
...
xmlns:mvvmc="clr-namespace:MVVMC;assembly=MVVMC">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Border >
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" >
<Button>Home</Button>
<Button>About</Button>
</StackPanel>
</Border>
<mvvmc:Region ControllerID="MainOperation" Grid.Row="1"/>
</Grid>
</Window>
The bottom of my window is the Region. This part is dynamic. The content of the Region can be changed and defined by a Controller (MainOperationController). A Controller controls only one Region and a Region is controlled by only one Controller.
The top part of the window, with two buttons is static and never changes.
File structure
A Controller and the Views & ViewModels connected to it should be in one namespace. You might want to have a separate folder for each Controller, like this:
Views
A view should be a FrameworkElement. So any User Control or a Custom Control will work. No need to derive from anything.
The only rule is that the name of the view should end with “View”.
A View and ViewModel pair is called a Page. So “AboutView” and “AboutViewModel” together make the page “About”.
ViewModels
A ViewModel should derive from MVVMCViewModel. Like this:
public class InitialViewModel : MVVMCViewModel
{
void NavigateToAbout()
{
Controller controller = GetController();
controller.Navigate("About", null);
}
}
Another option is to derive from MVVMCViewModel<TController>. Like this:
public class InitialViewModel : MVVMCViewModel<MainOperationController>
{
void NavigateToAbout()
{
MainOperationController controller = GetExactController();
controller.About();
}
}
Controllers
Here’s the MainOperationController example:
namespace MainApp.Pages.MainOperation
{
public class MainOperationController : Controller
{
public override void Initial()
{
ExecuteNavigation();
}
public void About()
{
ExecuteNavigation();
}
public void AllEmployees(object parameter)
{
ExecuteNavigation();
}
public void AddEmployee(object parameter)
{
ExecuteNavigation();
}
}
}
- A controller class should be called [ControllerID]Controller
- A Controller should derive from MVVMC.Controller abstract class.
- A Controller is connected to a single Region. When this Region is loaded, the controller is created and navigation to Initial is requested. Each controller should implement InitialI() method.
- Each navigation request invokes a Method with the same name in the controller. So when navigation to “About” controller.Navigate(“About”, parameter: null);
The “About()” method will be invoked.
“About” is called the Action. - Optionally a navigation method can accept an object as a parameter.
- When calling “ExecuteNavigation” from a method, the controller will create a View and ViewModel with the same name as the method. ```
public void About() { ExecuteNavigation(); } ``` This code will: 1\) look for AboutView and AboutViewModel in the same namespace as the controller 2\) Create instances of them 3\) Set View.DataContext = ViewModel 4\) Change the Content of the relevant Region to be the View
Navigation options
From View:
You can use a special type of Command called NavigationCommand. Like this:
...
<Button Margin="5" Command="{mvvmc:NavigateCommand ControllerID='MainOperation'}">Home</Button>
<Button Margin="5" Command="{mvvmc:NavigateCommand ControllerID='MainOperation', Action='About'}">About</Button>
</StackPanel>
</Border>
<mvvmc:Region ControllerID="MainOperation" Grid.Row="1" Margin="5"/>
...
Pressing on About will find a controller called “MainApp.Pages.MainOperation.MainOperationController” and invoke the method About().
As you see, the Home Command doesn’t have an Action property set. This will bring us to Initial action.
From ViewModel:
Each ViewModel derives from MVVMCViewModel base class which exposes several methods for navigation:
- GetController() will return the current control the ViewModel is related to. For example: ```
public class InitialViewModel : MVVMCViewModel { void NavigateToAbout() { Controller controller = GetController(); controller.Navigate("About", null); } } ```
- NavigationService is a singleton and you can use it from each ViewModel: ```
INavigationService svc = NavigationServiceProvider.GetNavigationServiceInstance(); AddWizardController controller = svc.GetController
(); controller.Next(); ``` Using NavigationService this way is available from anywhere in the application.
Navigating with parameter
All navigation methods allow to pass a parameter of type object. Here’s an example:
In ViewModel:
public class SelectEmployeeViewModel : MVVMCViewModel<AllEmployeesController>
{
public ICommand _selectEmployeeCommand;
public ICommand SelectEmployeeCommand
{
get
{
if (_selectEmployeeCommand == null)
_selectEmployeeCommand = new DelegateCommand(() =>
{
GetExactController().Info(SelectedEmployee);//SelectedEmployee is the parameter
},
()=>
{
return SelectedEmployee != null;
});
return _selectEmployeeCommand;
}
}
In Controller:
public void Info(object parameter)
{
var viewBag = new Dictionary<string, object>();
var employee = parameter as Employee;
viewBag.Add("EmployeeFirstName", employee.FirstName);
ExecuteNavigation(parameter, viewBag);
}
ViewBag
A controller can populate the navigation target ViewModel with data in 2 ways: A parameter and a ViewBag. The ViewModel base MVVMCViewModel holds them as properties:
public abstract class MVVMCViewModel : BaseViewModel
{
public Dictionary<string, object> ViewBag { get; set; }
public object NavigationParameter { get; set; }
public virtual void Initialize()
{
//This is called after NavigationParameter and ViewBag are set
}
We can use the “NavigationParameter” and the “ViewBag” in Initialize() method. Or, there’s a special kind of binding available ViewBagBinding which automatically binds to the ViewBag. But it’s a simple “OneTime” binding (for now).
In View:
<StackPanel Orientation="Horizontal">
<TextBlock Text="First Name: "/>
<TextBlock Text="{mvvmc:ViewBagBinding Path=EmployeeFirstName}"/>
</StackPanel>
Regions inside Regions
We might have hundreds of screens in our App and we might want different controllers to control these screen. For example we can have a “main” controller and a controller for a “mini-flow”. This can be achieved by adding another Region in a View. In the sample application we have a “MainOperationController” and one of the views introduces another Region which is controlled by a different Controller:
AllEmployeesView in MainOperation:
<UserControl x:Class="MainApp.Pages.MainOperation.AllEmployeesView"
xmlns:mvvmc="clr-namespace:MVVMC;assembly=MVVMC">
<mvvmc:Region ControllerID="AllEmployees" />
</UserControl>
The View contains another Region. When loaded, AllEmployeesController will be called with Initial()
public class AllEmployeesController : Controller
{
public override void Initial()
{
SelectEmployee();
}
public void SelectEmployee()
{
ExecuteNavigation();
}
Since Initial calls SelectEmployee(), the Region’s content will become “SelectEmployeeView”
This finishes the technical overview. If you are curious, download the sample app from GitHub and run it. I’d love to hear your thoughts.
What more is missing?
There are still some important navigation features missing. Some are:
- GoBack and GoForward navigation.
- CanNavigate and CanNavigateBack functionality.
- Better support for Testability – Have everything work with interfacs and dependency injection.
If there’s interest in this project I promise to add the missing functionality!
Is the MVVMC design pattern already being used?
I found out MVVMC (the design pattern, not my project) is already out there and some development teams are adopting it – Check out this blog post for example MVVM is dead, long live MVVMC! . I couldn’t find any WPF frameworks, so everyone who uses it probably wrote their own custom code. However, I did find a WinRT framework for MVVMC – ControllerRT.
ControllerRT is available on GitHub . I didn’t run it but I did look over the code. It looks good and somewhat similar to what I did. But also with many differences. For example there’s one controller for each View and ViewModel.
Summary
We saw how we can use a lightweight MVVMC framework for navigation in a WPF project. This provides a nice abstraction. Now, the View and ViewModel of a specific screen aren’t responsible for the navigation logic to a different screen.
I’ll use this in my future WPF projects, and hope some of you will try it as well. If you do try it, I’d love to hear feedback at michaels9876@gmail.com or by comments in this post.
I’d like to thank Aleksey Kravetz for sitting with me on the project and helping me with some of the concepts of this project!