MVVM Light 5.0: How to use the Navigation service
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.
Yes, MvvmLight
introduced the NavigationService
in their last version but they did't offer any implementation regarding Wpf
(you can use the Implemented NavigationService
in WP, Metroapps, ..) but unfortunately not Wpf
, you need to implement that by your self,
here how i am currently doing it (credit)
first create you navigation interface that Implements the MvvmLight
INavigationService
public interface IFrameNavigationService : INavigationService
{
object Parameter { get; }
}
the Parameter
is used to pass objects between ViewModels
, and the INavigationService
is part of GalaSoft.MvvmLight.Views
namespace
then implemente that interface like so
class FrameNavigationService : IFrameNavigationService,INotifyPropertyChanged
{
#region Fields
private readonly Dictionary<string, Uri> _pagesByKey;
private readonly List<string> _historic;
private string _currentPageKey;
#endregion
#region Properties
public string CurrentPageKey
{
get
{
return _currentPageKey;
}
private set
{
if (_currentPageKey == value)
{
return;
}
_currentPageKey = value;
OnPropertyChanged("CurrentPageKey");
}
}
public object Parameter { get; private set; }
#endregion
#region Ctors and Methods
public FrameNavigationService()
{
_pagesByKey = new Dictionary<string, Uri>();
_historic = new List<string>();
}
public void GoBack()
{
if (_historic.Count > 1)
{
_historic.RemoveAt(_historic.Count - 1);
NavigateTo(_historic.Last(), null);
}
}
public void NavigateTo(string pageKey)
{
NavigateTo(pageKey, null);
}
public virtual void NavigateTo(string pageKey, object parameter)
{
lock (_pagesByKey)
{
if (!_pagesByKey.ContainsKey(pageKey))
{
throw new ArgumentException(string.Format("No such page: {0} ", pageKey), "pageKey");
}
var frame = GetDescendantFromName(Application.Current.MainWindow, "MainFrame") as Frame;
if (frame != null)
{
frame.Source = _pagesByKey[pageKey];
}
Parameter = parameter;
_historic.Add(pageKey);
CurrentPageKey = pageKey;
}
}
public void Configure(string key, Uri pageType)
{
lock (_pagesByKey)
{
if (_pagesByKey.ContainsKey(key))
{
_pagesByKey[key] = pageType;
}
else
{
_pagesByKey.Add(key, pageType);
}
}
}
private static FrameworkElement GetDescendantFromName(DependencyObject parent, string name)
{
var count = VisualTreeHelper.GetChildrenCount(parent);
if (count < 1)
{
return null;
}
for (var i = 0; i < count; i++)
{
var frameworkElement = VisualTreeHelper.GetChild(parent, i) as FrameworkElement;
if (frameworkElement != null)
{
if (frameworkElement.Name == name)
{
return frameworkElement;
}
frameworkElement = GetDescendantFromName(frameworkElement, name);
if (frameworkElement != null)
{
return frameworkElement;
}
}
}
return null;
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
the MainFrame
in the above code is the x:Name of a simple Frame
control Defined in Xaml
used to navigate between pages (customize based on your needs)
Second: In the viewmodellocator
, init your navigation service (SetupNavigation()
), so you can use it in your viewmodels:
static ViewModelLocator()
{
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
SetupNavigation();
SimpleIoc.Default.Register<MainViewModel>();
SimpleIoc.Default.Register<LoginViewModel>();
SimpleIoc.Default.Register<NoteViewModel>();
}
private static void SetupNavigation()
{
var navigationService = new FrameNavigationService();
navigationService.Configure("LoginView", new Uri("../Views/LoginView.xaml",UriKind.Relative));
navigationService.Configure("Notes", new Uri("../Views/NotesView.xaml", UriKind.Relative));
SimpleIoc.Default.Register<IFrameNavigationService>(() => navigationService);
}
Third: finaly, use the service, for example
public LoginViewModel(IFrameNavigationService navigationService)
{
_navigationService = navigationService;
}
...
_navigationService.NavigateTo("Notes",data);
..
EDIT
An explicit sample can be found at this repo.
I don't know if a navigation feature is available in mvvm light. I implemented it with a contentControl binding:
<xcad:LayoutDocumentPane>
<xcad:LayoutDocument x:Name="DetailDoc" CanClose="False">
<ContentControl Content="{Binding DisplayedDetailViewModel}"/>
</xcad:LayoutDocument>
</xcad:LayoutDocumentPane>
And then the viewmodel property. It inherits from the mvvm light ViewModelBase class.
public ViewModelBase DisplayedDetailViewModel
{
get
{
return displayedDetailViewModel;
}
set
{
if (displayedDetailViewModel == value)
{
return;
}
displayedDetailViewModel = value;
RaisePropertyChanged("DisplayedDetailViewModel");
}
}
For the content control to knows which user control it has to use, you define DataTemplates in app.xaml :
<Application.Resources>
<ResourceDictionary>
<!--
We define the data templates here so we can apply them across the
entire application.
The data template just says that if our data type is of a particular
view-model type, then render the appropriate view. The framework
takes care of this dynamically. Note that the DataContext for
the underlying view is already set at this point, so the
view (UserControl), doesn't need to have it's DataContext set
directly.
-->
<DataTemplate DataType="{x:Type viewModel:LoggerViewModel}">
<views:LogView />
</DataTemplate>
</ResourceDictionary>
</Application.Resources>
The LogView is the UserControl. You just have to assign LoggerViewModel to DisplayedDetailViewModel, and the Framework will do the work.