MVVM Light 5.0: How to use the Navigation service
Asked Answered
G

3

26

In the latest release of MVVM Light note, it has been indicated that MVVM Light now provides a "Navigation Service".

But myself and my friend google are unable to find how to use it.

I can see that I can ask a INavigationService to the ServiceLocator, so I see how I can ask to go to another page, but:

  1. I created a new windows, where I expect to reserve a specific zone for the "page", how do I specify this?
  2. How do I specify all the available pages? Is there something I should call?
  3. What would be the format of the parameters given to the INavigationService

Is there any official documentation for this library? Because currently I find it nicely coded and working fine, but when I've to search how to use it, I never find a documentation/sample showing how to, except his blog which has some entry. This is very frustrating. The only documentation I found is this, I'm not very familiar with Pluralsight, but it seems that it's mandatory to take a monthly subscription(which as an individual, which is trying to make an application on my free time, isn't possible).

Garett answered 10/3, 2015 at 14:48 Comment(5)
Did you see this marcominerva.wordpress.com/2014/10/10/… ?Ecospecies
Not at all. But currently they are doing a new NavigationService() which is an internal constructor, unavailable for meGarett
I just saw this MVVM 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
Use a ContentControl in wpf and bind the 'content' property to a viewmodel propertyEcospecies
So you confirm there is nothing in MVVM Light to implement this navigation? Can you maybe create an Answers with more details? Because I see how to do the binding, but of what type? How another viewmodel would know what it has to set to the "ContentControl" ?Garett
C
46

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.

Cassel answered 10/3, 2015 at 16:6 Comment(13)
Very interesting. So if I understand correctly, we expect to find a Frame named MainFrame on the MainWindow and we will put the content in it. It's also the responsability of the ViewModel to get its potential parameter from the service. But currently, if we go back, how will we now what parameter we should assign to the service?Garett
Yes exactly, by using _navigationService.Parameter you can get the passed object between ViewModels just pass an IFrameNavigationService in all ViewModel's Ctors. you could change the GoBack method to accept a parameter to GoBack(Object parameter) that will be passed to the previous View ..Cassel
I managed to make it work very nicely with your example. But currently I've my actions( = Notes in your example) that are displayed in the previous/Next button. Is there a way to display another text(localizable for each of those actions)?Garett
i am not sure what you are asking for! if you meant custom the Frame NavigationUI, i usually hide the default one, and build a new more customization one !Cassel
[NotifyPropertyChangedInvocator] is just a Resharper attribute, right?Garges
Yes, exactly part of the resharper Annotations codeCassel
Excellent!, question: How can I change the MainWindow title based on the current Page/View?Nib
SO question for the above: #40683132Nib
Good stuff! Thank you!Earle
Please how do i get the returning object/value from the receiving ViewModel for _navigationService.NavigateTo("Notes",data);Jerid
I've been following your code, Usama. I've just added the SetupNavigation() method. After doing that I got an error that reads, "There is already a factory registerd for IFrameNavigationService". How do you handle that?Goatsucker
Hi @Rod, i did end up building a more explicit sample to answer someone's questions, you might find this useful ;)) github.com/SamTheDev/SampleMvvmLightNavigationCassel
Thank you very much, @Usama! Definitely will check it out.Goatsucker
T
4

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.

Tellurion answered 22/12, 2016 at 10:53 Comment(1)
how can you use this example to create new windows on top of the main one?Engird
E
2

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.

Ecospecies answered 10/3, 2015 at 15:26 Comment(5)
But how the content control knows which user control it has to use to display this model?Garett
Ok I see, but should we not use something coming from the NavigationWindows ( msdn.microsoft.com/en-us/library/… ) ? Or this is only for silver light?Garett
I don't know. I found this solution and implemented it. I find it quite smart and it fits quite well in mvvm pattern.Ecospecies
And how do you ask from a ViewModel to go on another ViewModel?Garett
You assign the viewmodel you want to display to the DisplayedDetailViewModel propertyEcospecies

© 2022 - 2024 — McMap. All rights reserved.