Page Navigation using MVVM in Store App

I don't really like when a ViewModel references Views to navigate to. So I prefer to a ViewModel-first approach. By using ContentControls, DataTemplates for ViewModel types & some kind of navigation pattern in my ViewModels.

My navigation looks like this:

[ImplementPropertyChanged]
public class MainNavigatableViewModel : NavigatableViewModel
{
    public ICommand LoadProfileCommand { get; private set; }

    public ICommand OpenPostCommand { get; private set; }

    public MainNavigatableViewModel ()
    {
        LoadProfileCommand = new RelayCommand(() => Navigator.Navigate(new ProfileNavigatableViewModel()));
        OpenPostCommand = new RelayCommand(() => Navigator.Navigate(new PostEditViewModel { Post = SelectedPost }), () => SelectedPost != null);
    }
}

My NavigatableViewModel looks like:

[ImplementPropertyChanged]
public class NavigatableViewModel
{
    public NavigatorViewModel Navigator { get; set; }

    public NavigatableViewModel PreviousViewModel { get; set; }

    public NavigatableViewModel NextViewModel { get; set; }

}

And my Navigator:

[ImplementPropertyChanged]
public class NavigatorViewModel
{
    public NavigatableViewModel CurrentViewModel { get; set; }

    public ICommand BackCommand { get; private set; }

    public ICommand ForwardCommand { get; private set; }

    public NavigatorViewModel()
    {
        BackCommand = new RelayCommand(() =>
        {
            // Set current control to previous control
            CurrentViewModel = CurrentViewModel.PreviousViewModel;
        }, () => CurrentViewModel != null && CurrentViewModel.PreviousViewModel != null);

        ForwardCommand = new RelayCommand(() =>
        {
            // Set current control to next control
            CurrentViewModel = CurrentViewModel.NextViewModel;
        }, () => CurrentViewModel != null && CurrentViewModel.NextViewModel != null);
    }

    public void Navigate(NavigatableViewModel newViewModel)
    {
        if (newViewModel.Navigator != null && newViewModel.Navigator != this)
            throw new Exception("Viewmodel can't be added to two different navigators");

        newViewModel.Navigator = this;

        if (CurrentViewModel != null)
        {
            CurrentViewModel.NextViewModel = newViewModel;
        }

        newViewModel.PreviousViewModel = CurrentViewModel;
        CurrentViewModel = newViewModel;
    }
}

My MainWindows.xaml:

<Window
        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:viewmodels="clr-namespace:MyApp.ViewModels"
        x:Class="MyApp.Windows.MainWindow"
        mc:Ignorable="d"
        Title="MainWindow" Height="389" Width="573" 
        d:DataContext="{d:DesignInstance {x:Type viewmodels:MyAppViewModel}, IsDesignTimeCreatable=True}">
    <Grid>
        <!-- Show data according to data templates as defined in App.xaml -->
        <ContentControl Content="{Binding Navigator.CurrentViewModel}"  Margin="0,32,0,0" />

        <Button Content="Previous" Command="{Binding Navigator.BackCommand}" Style="{DynamicResource ButtonStyle}" HorizontalAlignment="Left" Margin="10,5,0,0" VerticalAlignment="Top" Width="75" />
        <Button Content="Next" Command="{Binding Navigator.ForwardCommand}" Style="{DynamicResource ButtonStyle}" HorizontalAlignment="Left" Margin="90,5,0,0" VerticalAlignment="Top" Width="75" />
    </Grid>
</Window>

App.xaml.cs:

public partial class App
{
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);

        new MainWindow {DataContext = new MyAppViewModel()}.Show();
    }
}

MyAppViewModel:

[ImplementPropertyChanged]
public class MyAppViewModel
{
    public NavigatorViewModel Navigator { get; set; }

    public MyAppViewModel()
    {
        Navigator = new NavigatorViewModel();
        Navigator.Navigate(new MainNavigatableViewModel());
    }
}

App.xaml:

        <DataTemplate DataType="{x:Type viewmodels:MainNavigatableViewModel}">
            <controls:MainControl/>
        </DataTemplate>
        <DataTemplate DataType="{x:Type viewmodels:PostEditViewModel}">
            <controls:PostEditControl/>
        </DataTemplate>

The downside is that you have more ViewModel-code which manages the state of what you are looking at. But obviously that is also a huge advantage in terms of Testability. And of course your ViewModels do not need to depend on your Views.

Plus I use Fody/PropertyChanged, that's what the [ImplementPropertyChanged] is about. Keeps me from writing OnPropertyChanged code.


As Scott says you could use a NavigationService. I would firstly create an interface this is not needed in this example but will be useful if you use Dependency Injection (good solution with viewmodels and services) in the future :)

INavigationService:

public interface INavigationService
{
    void Navigate(Type sourcePage);
    void Navigate(Type sourcePage, object parameter);
    void GoBack();
}

NavigationService.cs will inherit INavigationService you will need the following namespaces

using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;


public sealed class NavigationService : INavigationService
{
    public void Navigate(Type sourcePage)
    {
        var frame = (Frame)Window.Current.Content;
        frame.Navigate(sourcePage);
    }

    public void Navigate(Type sourcePage, object parameter)
    {
        var frame = (Frame)Window.Current.Content;
        frame.Navigate(sourcePage, parameter);
    }

    public void GoBack()
    {
        var frame = (Frame)Window.Current.Content;
        frame.GoBack();
    }
}

Simple ViewModel to show RelayCommand example. NB I Navigate to another Page (Page2.xaml) using the DoSomething RelayCommand.

MyViewModel.cs

public class MyViewModel : INotifyPropertyChanged
{
    private INavigationService _navigationService;

    public event PropertyChangedEventHandler PropertyChanged;

    public void OnPropertyChanged(string propertyName)
    {
        var handler = PropertyChanged;
        if (handler != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    public MyViewModel(INavigationService navigationService)
    {
        _navigationService = navigationService;
    }

    private ICommand _doSomething;

    public ICommand DoSomething
    {
        get
        {
            return _doSomething ??
                new RelayCommand(() =>
                    {
                        _navigationService.Navigate(typeof(Page2));
                    });
        }
    }}

In simple example Ive created the viewmodel in MainPage.cs and added the NavigationService but you can do this elsewhere depending on what your MVVM setup is like.

MainPage.cs

public sealed partial class MainPage : Page
{
    public MainPage()
    {
        this.InitializeComponent();

        var vm = new MyViewModel(new NavigationService());
        this.DataContext = vm;
    }
}

MainPage.xaml (binds to the command DoSomething)

 <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Button Width="200" Height="50" Content="Go to Page 2"
             Command="{Binding DoSomething}"/>
</Grid>

Hope that helps.


There are 2 ways to do this, a simple way is to pass a relay command action from the view to the view model.

public MainPage()
{
  var vm = new MyViewModel();
  vm.GotoPage2Command = new RelayCommand(()=>{ Frame.Navigate(typeof(Page2)) });
  this.DataContext = vm;
}

<Button Command={Binding GoToPage2Command}>Go to Page 2</Button>

Another way is by using an IocContainer and DependencyInjection. This one is a more losely coupled approach.

We will need an interface for navigation page so that we don't need to reference or Know anything about PageX or any UI element assuming that your viewmodel is in a separate project that doesn't know anything about the UI.

ViewModel Project:

  public interface INavigationPage
  {
    Type PageType { get; set; }
  }

  public interface INavigationService
  {
    void Navigate(INavigationPage page) { get; set; }
  }



public class MyViewModel : ViewModelBase
  {
    public MyViewModel(INavigationService navigationService, INavigationPage page)
    {
      GotoPage2Command = new RelayCommand(() => { navigationService.Navigate(page.PageType); })
    }

    private ICommand GotoPage2Command { get; private set; }
  }

UI Project:

  public class NavigationService : INavigationService
    {
       //Assuming that you only navigate in the root frame
       Frame navigationFrame = Window.Current.Content as Frame;
       public void Navigate(INavigationPage page)
       {
          navigationFrame.Navigate(page.PageType);
       }
    }

public abstract class NavigationPage<T> : INavigationPage
{
   public NavigationPage()
   {
      this.PageType = typeof(T);
   }
}

public class NavigationPage1 : NavigationPage<Page1> { }


public class MainPage : Page
{
   public MainPage()
   {
      //I'll just place the container logic here, but you can place it in a bootstrapper or in app.xaml.cs if you want. 
      var container = new UnityContainer();
      container.RegisterType<INavigationPage, NavigationPage1>();
      container.RegisterType<INavigationService, NavigationService>();
      container.RegisterType<MyViewModel>();

      this.DataContext = container.Resolve<MyViewModel>();       
   }
}

Here is another way to implement the NavigationService, without using an abstract class and without referencing view types in your view model.

Assuming that the view model of the destination page is something like this:

public interface IDestinationViewModel { /* Interface of destination vm here */ }
class MyDestinationViewModel : IDestinationViewModel { /* Implementation of vm here */ }

Then your NavigationService can simply implement the following interface:

public interface IPageNavigationService
{
    void NavigateToDestinationPage(IDestinationViewModel dataContext);
}

In your main window ViewModel you need to inject the navigator and the view model of the destination page:

class MyViewModel1 : IMyViewModel
{
    public MyViewModel1(IPageNavigationService navigator, IDestinationViewModel destination)
    {
        GoToPageCommand = new RelayCommand(() => 
                navigator.NavigateToDestinationPage(destination));
    }

    public ICommand GoToPageCommand { get; }
}

The implementation of the NavigationService encapsulates the view type (Page2) and the reference to the frame which is injected through the constructor:

class PageNavigationService : IPageNavigationService
{
    private readonly Frame _navigationFrame;

    public PageNavigationService(Frame navigationFrame)
    {
        _navigationFrame = navigationFrame;
    }

    void Navigate(Type type, object dataContext)
    {
        _navigationFrame.Navigate(type);
        _navigationFrame.DataContext = dataContext;
    }

    public void NavigateToDestinationPage(IDestinationViewModel dataContext)
    {
        // Page2 is the corresponding view of the destination view model
        Navigate(typeof(Page2), dataContext);
    }
}

To get the frame simply name it in your MainPage xaml:

<Frame x:Name="RootFrame"/>

In the code behind of the MainPage initialize your bootstrapper by passing the root frame:

public sealed partial class MainPage : Page
{
    public MainPage()
    {
        this.InitializeComponent();
        var bootstrapper = new Bootstrapper(RootFrame);
        DataContext = bootstrapper.GetMainScreenViewModel();
    }
}

Finally here is the bootstrapper implementation for completeness ;)

class Bootstrapper
{
    private Container _container = new Container();

    public Bootstrapper(Frame frame)
    {
        _container.RegisterSingleton(frame);
        _container.RegisterSingleton<IPageNavigationService, PageNavigationService>();
        _container.Register<IMyViewModel, MyViewModel1>();
        _container.Register<IDestinationViewModel, IDestinationViewModel>();

#if DEBUG
        _container.Verify();
#endif
    }

    public IMyViewModel GetMainScreenViewModel()
    {
        return _container.GetInstance<IMyViewModel>();
    }
}