I have long been searching for a hard and fast way of navigating between views in an MVVM / WPF environment.
For some time I have been using ways that either did not completely avoid code-behind or had violated the spirit of the MMVM pattern somewhat.
Thanks to Rachel Lim’s excellent blog post, I think I have nailed down a way of accomplishing this in a way that I am happy with.
I have created a minimalist application application that utilises Rachel Lim’s MVVM based pattern in conjunction with the Mediator pattern to enable us to switch from one view to any other view as and when desired.
Update: A more recent post dispenses with the need to use a mediator pattern as a means of passing data between screens and navigating between them. This version uses dependency injection instead, which I consider to be a huge improvement when it comes to maintaining SOLID principles, scalability, ease of unit testing etc.:
Step 1: Create a new WPF application
Step 2: Add classes for event handling, RelayCommand etc
RelayCommand.cs
public class RelayCommand<T> : ICommand { private readonly Predicate<T> _canExecute; private readonly Action<T> _execute; public RelayCommand(Action<T> execute) : this(execute, null) { _execute = execute; } public RelayCommand(Action<T> execute, Predicate<T> canExecute) { if (execute == null) { throw new ArgumentNullException("execute"); } _execute = execute; _canExecute = canExecute; } public bool CanExecute(object parameter) { return _canExecute == null || _canExecute((T)parameter); } public void Execute(object parameter) { _execute((T)parameter); } public event EventHandler CanExecuteChanged { add { CommandManager.RequerySuggested += value; } remove { CommandManager.RequerySuggested -= value; } } } public class RelayCommand : ICommand { private readonly Predicate<object> _canExecute; private readonly Action<object> _execute; public RelayCommand(Action<object> execute) : this(execute, null) { _execute = execute; } public RelayCommand(Action<object> execute, Predicate<object> canExecute) { if (execute == null) { throw new ArgumentNullException("execute"); } _execute = execute; _canExecute = canExecute; } public bool CanExecute(object parameter) { return _canExecute == null || _canExecute(parameter); } public void Execute(object parameter) { _execute(parameter); } // Ensures WPF commanding infrastructure asks all RelayCommand objects whether their // associated views should be enabled whenever a command is invoked public event EventHandler CanExecuteChanged { add { CommandManager.RequerySuggested += value; CanExecuteChangedInternal += value; } remove { CommandManager.RequerySuggested -= value; CanExecuteChangedInternal -= value; } } private event EventHandler CanExecuteChangedInternal; public void RaiseCanExecuteChanged() { CanExecuteChangedInternal.Raise(this); } }
EventRaiser.cs
public static class EventRaiser { public static void Raise(this EventHandler handler, object sender) { handler?.Invoke(sender, EventArgs.Empty); } public static void Raise<T>(this EventHandler<EventArgs<T>> handler, object sender, T value) { handler?.Invoke(sender, new EventArgs<T>(value)); } public static void Raise<T>(this EventHandler<T> handler, object sender, T value) where T : EventArgs { handler?.Invoke(sender, value); } public static void Raise<T>(this EventHandler<EventArgs<T>> handler, object sender, EventArgs<T> value) { handler?.Invoke(sender, value); } }
EventArgs.cs
public class EventArgs<T> : EventArgs { public EventArgs(T value) { Value = value; } public T Value { get; private set; } }
Step 3: Create a common ViewModel interface
All user control based ViewModels inherit from an IPageViewModel interface so they can have some common properties:
namespace MvvmNavigation { public interface IPageViewModel { } }
Step 4: Create the Views
Here are the MainWindow, UserControl1, UserControl2 XAML views:
MainWindow.xaml
<Window x:Class="MvvmNavigation.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" xmlns:local="clr-namespace:MvvmNavigation" mc:Ignorable="d" Title="MainWindow" Height="350" Width="525"> <Grid> <ContentControl Content="{Binding CurrentPageViewModel}" /> </Grid> </Window>
UserControl1.xaml
<UserControl x:Class="MvvmNavigation.UserControl1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:MvvmNavigation" mc:Ignorable="d" d:DesignHeight="280" d:DesignWidth="280"> <Grid> <Button Content="Go to View 2" Command="{Binding GoTo2}" Width="90" Height="30" /> </Grid> </UserControl>
UserControl2.xaml
<UserControl x:Class="MvvmNavigation.UserControl2" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:MvvmNavigation" mc:Ignorable="d" d:DesignHeight="280" d:DesignWidth="280"> <Grid> <Button Content="Go to View 1" Command="{Binding GoTo1}" Width="90" Height="30" /> </Grid> </UserControl>
Step 5: Create the ViewModel classes
The classes created are a base ViewModel class implementing INotifyPropertyChanged, the MainWindow ViewModel and ViewModels for the user controls.
BaseViewModel.cs
using System; using System.ComponentModel; using System.Diagnostics; namespace MvvmNavigation { public abstract class BaseViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged(string propertyName) { VerifyPropertyName(propertyName); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } [Conditional("DEBUG")] private void VerifyPropertyName(string propertyName) { if (TypeDescriptor.GetProperties(this)[propertyName] == null) throw new ArgumentNullException(GetType().Name + " does not contain property: " + propertyName); } } }
MainWindowViewModel.cs
using System.Collections.Generic; using System.Linq; namespace MvvmNavigation { public class MainWindowViewModel : BaseViewModel { private IPageViewModel _currentPageViewModel; private List<IPageViewModel> _pageViewModels; public List<IPageViewModel> PageViewModels { get { if (_pageViewModels == null) _pageViewModels = new List<IPageViewModel>(); return _pageViewModels; } } public IPageViewModel CurrentPageViewModel { get { return _currentPageViewModel; } set { _currentPageViewModel = value; OnPropertyChanged("CurrentPageViewModel"); } } private void ChangeViewModel(IPageViewModel viewModel) { if (!PageViewModels.Contains(viewModel)) PageViewModels.Add(viewModel); CurrentPageViewModel = PageViewModels .FirstOrDefault(vm => vm == viewModel); } private void OnGo1Screen(object obj) { ChangeViewModel(PageViewModels[0]); } private void OnGo2Screen(object obj) { ChangeViewModel(PageViewModels[1]); } public MainWindowViewModel() { // Add available pages and set page PageViewModels.Add(new UserControl1ViewModel()); PageViewModels.Add(new UserControl2ViewModel()); CurrentPageViewModel = PageViewModels[0]; Mediator.Subscribe("GoTo1Screen", OnGo1Screen); Mediator.Subscribe("GoTo2Screen", OnGo2Screen); } } }
UserControl1ViewModel.cs
using System.Windows.Input; namespace MvvmNavigation { public class UserControl1ViewModel : BaseViewModel, IPageViewModel { private ICommand _goTo2; public ICommand GoTo2 { get { return _goTo2 ?? (_goTo2 = new RelayCommand(x => { Mediator.Notify("GoTo2Screen", ""); })); } } } }
UserControl2ViewModel.cs
using System.Windows.Input; namespace MvvmNavigation { public class UserControl2ViewModel : BaseViewModel, IPageViewModel { private ICommand _goTo1; public ICommand GoTo1 { get { return _goTo1 ?? (_goTo1 = new RelayCommand(x => { Mediator.Notify("GoTo1Screen", ""); })); } } } }
Step 6: Add the class implementing the Mediator pattern
Mediator.cs
using System; using System.Collections.Generic; namespace MvvmNavigation { public static class Mediator { private static IDictionary<string, List<Action<object>>> pl_dict = new Dictionary<string, List<Action<object>>>(); public static void Subscribe(string token, Action<object> callback) { if (!pl_dict.ContainsKey(token)) { var list = new List<Action<object>>(); list.Add(callback); pl_dict.Add(token, list); } else { bool found = false; foreach (var item in pl_dict[token]) if (item.Method.ToString() == callback.Method.ToString()) found = true; if (!found) pl_dict[token].Add(callback); } } public static void Unsubscribe(string token, Action<object> callback) { if (pl_dict.ContainsKey(token)) pl_dict[token].Remove(callback); } public static void Notify(string token, object args=null) { if (pl_dict.ContainsKey(token)) foreach (var callback in pl_dict[token]) callback(args); } } }
Step 7: Add resources to the App.XAML
<Application x:Class="MvvmNavigation.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:MvvmNavigation"> <Application.Resources> <DataTemplate DataType="{x:Type local:UserControl1ViewModel}"> <local:UserControl1 /> </DataTemplate> <DataTemplate DataType="{x:Type local:UserControl2ViewModel}"> <local:UserControl2 /> </DataTemplate> </Application.Resources> </Application>
Note: remove the startupUri property that gets automatically generated to that the Main Window view is not fired up twice. This bit:
StartupUri="MainWindow.xaml"
Step 8: Add the startup event in app.xaml.cs
App.xaml.cs
using System.Windows; namespace MvvmNavigation { /// <summary> /// Interaction logic for App.xaml /// </summary> public partial class App : Application { protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); MainWindow app = new MainWindow(); MainWindowViewModel context = new MainWindowViewModel(); app.DataContext = context; app.Show(); } } }
On running the program see that the View defaults to the UserControl1 View:
And on clicking the button it then navigates to UserControl2 View:
And on clicking the button again we are navigated back to the original UserControl1 View: