I'd rather go with a ViewModelFirst Navigation Service.
In my mind it's easier to use and induce way less code to add when creating a new pair of View/ViewModel.
For this you need a few things :
First a NavigableViewModel abstract class with some methods to handle navigation in both ways. All your viewModels are going to inherit from this class :
NavigableViewModel.cs
public abstract class NavigableViewModel : ViewModelBase
{
public abstract void OnNavigatedTo(object parameter = null);
public abstract void OnNavigatingTo(object parameter = null);
}
A MainWindow containing the Frame where the navigation happens, just think to hide the default navigation controls with NavigationUIVisibility="Hidden" :
MainWindow.xaml
<Window x:Class="YourProject.Views.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:SS3DViewModelFirstMvvmLightProject"
mc:Ignorable="d"
DataContext="{Binding Main, Source={StaticResource Locator}}"
Title="MainWindow" Height="350" Width="525">
<-- Just remeber to replace x:Class="YourProject.Views.MainWindow" with your actual project path-->
<Frame x:Name="Frame" NavigationUIVisibility="Hidden">
</Frame>
</Window>
And some code behind to handle the ViewModels change (alowing us to notify each page of its viewModel) :
MainWindow.xaml.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
((MainViewModel)this.DataContext).ShowFirstView(); // we need to have our view loaded to start navigating
Frame.LoadCompleted += (s, e) => UpdateFrameDataContext();
Frame.DataContextChanged += (s, e) => UpdateFrameDataContext();
}
private void UpdateFrameDataContext()
{
Page view = (Page)Frame.Content;
if (view != null)
{
view.DataContext = Frame.DataContext;
}
}
}
And in your MainViewModel, this little method to Navigate to the your First ViewModel (here LoginViewModel) :
MainViewModel.cs
public class MainViewModel : ViewModelBase
{
public MainViewModel()
{
}
public void ShowFirstView()
{
ServiceLocator.Current.GetInstance<ViewModelFirstNavigationService>().NavigateTo<LoginViewModel>();
//To navigate wherever you want you just need to call this method, replacing LoginViewModel with YourViewModel
}
}
For this ServiceLocator call to work we need to nicely add a few things to our ViewModelLocator :
ViewModelLocator.cs
public class ViewModelLocator
{
public ViewModelLocator()
{
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
SimpleIoc.Default.Register<MainViewModel>();
ViewModelFirstNavigationService navService = new ViewModelFirstNavigationService(Main);
SimpleIoc.Default.Register<LoginViewModel>();
navService.AddNavigableElement(SimpleIoc.Default.GetInstance<LoginViewModel>);
// so whenever you want to add a new navigabel View Model just add these lines here
// SimpleIoc.Default.Register<YourViewModel>();
// navService.AddNavigableElement(SimpleIoc.Default.GetInstance<YourViewModel>);
SimpleIoc.Default.Register<ViewModelFirstNavigationService>(() => navService);
}
public MainViewModel Main
{
get
{
return ServiceLocator.Current.GetInstance<MainViewModel>();
}
}
public static void Cleanup()
{
}
}
And now that you have every thing in place lets add the core of the system, the Navigation Service (that's the tricky part):
ViewModelFirstNavigationService
public class ViewModelFirstNavigationService
{
private Dictionary<Type, Uri> _registeredViews;
private Dictionary<Type, Func<NavigableViewModel>> _registeredViewModels;
private List<string> _allXamlPages;
private MainViewModel _mainContainerViewModel;
public NavigableViewModel CurrentViewModel;
public ViewModelFirstNavigationService(MainViewModel mainContainerViewModel)
{
_mainContainerViewModel = mainContainerViewModel;
_registeredViews = new Dictionary<Type, Uri>();
_registeredViewModels = new Dictionary<Type, Func<NavigableViewModel>>();
_allXamlPages = GetAllXamlPages();
}
private List<string> GetAllXamlPages()
{
// this part is a bit tricky. We use it to find all xaml pages in the current project.
// so you need to be sure that all your pages you want to use with your viewmodles need to end with page.xaml
// Example : LoginPage.xaml will work fine. Parameters.xaml won't.
System.Reflection.Assembly viewModelFirstProjectAssembly;
viewModelFirstProjectAssembly = System.Reflection.Assembly.GetExecutingAssembly();
var stream = viewModelFirstProjectAssembly.GetManifestResourceStream(viewModelFirstProjectAssembly.GetName().Name + ".g.resources");
var resourceReader = new ResourceReader(stream);
List<string> pages = new List<string>();
foreach (DictionaryEntry resource in resourceReader)
{
Console.WriteLine(resource.Key);
string s = resource.Key.ToString();
if (s.Contains("page.baml"))
{
pages.Add(s.Remove(s.IndexOf(".baml")));
}
}
return pages;
}
private Type ResolveViewModelTypeFromSingletonGetterFunc<T>(Func<T> viewModelSingletonGetterFunc)
{
MethodInfo methodInfo = viewModelSingletonGetterFunc.Method;
return methodInfo.ReturnParameter.ParameterType;
}
private Uri ResolvePageUriFromViewModelType(Type viewModelType)
{
string pageName = String.Empty;
int index = viewModelType.Name.IndexOf("ViewModel");
pageName = viewModelType.Name.Remove(index);
string pagePath = String.Format("{0}.xaml", _allXamlPages.Where(page => page.Contains(pageName.ToLower())).FirstOrDefault());
string cleanedPath = pagePath.Remove(0, "views/".Length); //obviously for this to work you need to have your views in a Views folder at the root of the project. But you are alowed yo reat sub folders in it
return new Uri(cleanedPath, UriKind.Relative);
}
public void AddNavigableElement(Func<NavigableViewModel> viewModelSingletonGetter)
{
//Where the magic happens !
//If your are wondering why a Func, it's because we want our viewmodels to be instantiated only when we need them via IOC.
//First we ge the type of our viewmodel to register for the Func.
Type vmType = ResolveViewModelTypeFromSingletonGetterFunc(viewModelSingletonGetter);
Uri uriPage = ResolvePageUriFromViewModelType(vmType);
_registeredViews.Add(vmType, uriPage);
_registeredViewModels.Add(vmType, viewModelSingletonGetter);
}
public void NavigateTo<GenericNavigableViewModelType>(object parameter = null)
{
Type key = typeof(GenericNavigableViewModelType);
NavigateTo(key, parameter);
}
public void NavigateTo(Type key, object parameter = null)
{
CurrentViewModel?.OnNavigatingTo(parameter);
CurrentViewModel = _registeredViewModels[key].Invoke();
Uri uri = _registeredViews[key];
((MainWindow)Application.Current.MainWindow).Frame.Source = uri;
((MainWindow)Application.Current.MainWindow).Frame.DataContext = CurrentViewModel;
CurrentViewModel.OnNavigatedTo(parameter);
}
}
And now you have everything working ! Hurray ! Let's demonstrate with our example LoginViewModel (who only contains a beautiful helloworld in black square):
LoginPage.xaml
<Page x:Class="YourProject.Views.LoginPage"
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:SS3DViewModelFirstMvvmLightProject.Views"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300"
Title="LoginPage">
<Grid Background="Gray">
<Label Content="{Binding HelloWorld}" Foreground="White" Background="Black" Width="150" Height="150"></Label>
</Grid>
</Page>
And its viewmodel :
LoginViewModel.cs
public class LoginViewModel : NavigableViewModel
{
private string _helloWorld;
public string HelloWorld
{
get
{
return _helloWorld;
}
set
{
_helloWorld = value;
RaisePropertyChanged(() => HelloWorld);
}
}
public LoginViewModel()
{
HelloWorld = "Hello World";
}
public override void OnNavigatedTo(object parameter = null)
{
// whatever you want to happen when you enter this page/viewModel
}
public override void OnNavigatingTo(object parameter = null)
{
// whatever you want to happen when you leave this page/viewmodel
}
}
I admit you need some code to begin with. But when everything is working you endup with a very easy to use system.
Want to navigate to some viewModel ?
Just use myNavigationService.NavigateTo(someParam);
Want to add a new pair View / ViewModel ?
Just add your viewModel in some IOC container (in my projects i use my own ioc, wich allows me to unload my viewmodels whenever i want and give some fine navigation stack) and give it your navigation service.
new NavigationService()
which is an internal constructor, unavailable for me – GarettMVVM Light doesn’t provide an implementation of INavigationService for WPF because this platform doesn’t have a standard navigation system.
in one comment of your article. But if WPF doesn't have a navigation system, how are we supposed to make a transition from an usercontrol to another? – Garett