
Applying page navigation is a fairly topical task for desktop WPF-MVVM applications.
There are plenty of useful guides for organizing such navigation in the network.
And , of course, Habrahabr is no exception (there are articles
one and
two ).
Looking at the first article you will learn about
NavigationService and the ability to use
Hyperlink .
If you follow the second link, you will learn how to use the
NavigationService in the so-called “Code Behind”.
Thus, the complete solution in these articles is not presented (in my opinion).
It seems to fill the gap and bring to your attention, it seems to me, quite a working solution.
And I absolutely do not pretend to a complete component for organizing page navigation.
I would be grateful for useful comments, amendments and additions.
I'm going to hell if my implementation of the navigator is useful to someone.
Introduction
Everyone who has already worked with WPF is probably familiar with the MVVM pattern (gave links at the end of the article). After all, the concept of MVVM is simple and, at a minimum, the intuitive benefit of its use should be clear. If you want it to show itself in all its glory, then put as little logic as possible into User Code Controls in “Code Behind” and in no case use direct links to the UI within the ViewModels. This approach will give you a big profit in the form of the ability to test ViewModels separately from controls. Another good practice is to minimize instantiation of ViewModels directly in controls. It is not great if the control creates for itself a ViewModel of a specific type - in this case, it will simply be more difficult to control a test doll. The situation will be different when some parental control is busy creating ViewModels for other screens, because then the code can turn into an untestable bunch of spaghetti. If other ViewModels are responsible for creating ViewModels, then testing will become much easier.
Let's imagine an application with a navigation bar, multiple screens and dialog boxes. Something like this is presented below.
')

We can see several entities: the main window, the navigation bar with buttons, the current page and the dialog above this page. In our paging application, HyperLink could be used, instead of using TextBlock buttons with HyperLink as content. HyperLink has a property that specifies the name of the Frame in which to navigate to the new page. And everything seems to be fine, but using HyperLink it is difficult to transfer the page to the desired ViewModel'and.
I saw on the net a couple of solutions to this problem:
- In the Frame.Navigated event in the main application window, through the Code Behind, you can access the frame-loaded content and put the created in the same place in the Code Behind ViewModel there. Thus, the creation of ViewModels for all pages will be concentrated in a single handler using the long if… else if… or switch. About the fact that testing such a “Hard Coded” navigation process is extremely difficult to automate, I am silent.
- Another solution is to create an instance of Page and ViewModel'and under it, putting ViewModel'i in the DataContext of the Page instance and calling Navigate on the frame with the transfer of the created Page instance. This solution is slightly better than the previous one, but still not at all the “MVVM-way”.
- The third solution is the use of PRISM libraries. It is used in the sector of large enterprise applications for implementing composite UI. If you are familiar with AngularJS, then understand what it is. Some RegionManager is implemented in which parts of the UI are registered. Then, through the created manager, the control is instantiated by a certain alias, as well as the assignment of the necessary data context. This functionality is similar to what is already implemented in the WPF NavigationService.
The first two solutions are a clear crutch. PRISM is a whole UI composition framework. It is certainly worth investing in learning about it, but for small applications (proof of concept, for example), the use of such things as IoC and PRISM may be impractical.
What is the simplest solution that could more or less smoothly fit into the MVVM context? The Page class in Silverlight has an overloaded OnNavigatedTo method. In this method, it would be convenient to accept the ViewModel passed to the NavigationService.Navigate (Uri uri, object navigationContext) as the second parameter. However, in WPF, Page has no such method. At least I have not found it or something equivalent. We need some kind of intermediary or, if you wish, a manager who will control page navigation and shift the desired ViewModel from the method parameter to the DataContext. The implementation of such a navigation manager will be discussed in this article.
In the next section, I will talk about the implementation of the solution kernel, the navigation manager. Then, it will be told about what needs to be implemented on the UI and ViewModel layers. To save time, you can read the section “Navigation Manager”, and think out the rest while solving your tasks.
Who is interested in looking at the code right away can go to the
repository on GitHub .
Navigation manager
This manager is implemented as a singleton with double instance checking for null (the so-called
Double-Check Locking Singleton , a multi-threaded version of the singleton). Using singleton is my preference. So it's easier for me to control the life cycle. You might also have a simple static class.
Singleton implementation code see below.
Singleton#region Singleton private static volatile Navigation instance; private static object syncRoot = new Object(); private Navigation() { } private static Navigation Instance { get { if (instance == null) { lock (syncRoot) { if (instance == null) instance = new Navigation(); } } return instance; } } #endregion
In the code above, you can see that I made the
Instance property private. This is done for simplicity so that nothing extra looks out. In practice, you may need to make it available publicly. Instead of a private property of a singleton instance, I created a public property of the Service navigation
service (of type NavigationService), which transmits calls through a private instance of a singleton. It was possible to do the opposite, but then all the calls from the outside would have to be done through an instance, i.e.
Navigation.Instance.Service
instead
Navigation.Service
Choose the option that you like best. I think the last option is simpler, but it requires additional implementation of static properties and methods. Therefore, with the implementation of the new functionality, it may be more profitable to open the instance property (Navigation.Instance).
The
Service property in this singleton will store a link to the
NavigationService of the Frame instance in which you want to perform page transitions. You can assign the current value of this link both at the start of the application (in the handler of the
Loaded event of the main window), and at any other later point before calling one of the navigation methods.
Example public MainWindow() { InitializeComponent(); Loaded += MainWindow_Loaded; } void MainWindow_Loaded(object sender, RoutedEventArgs e) { Navigation.Navigation.Service = MainFrame.NavigationService; DataContext = new MainViewModel(new ViewModelsResolver()); }
In the example above, we assign our main window to our NavigationService Frame navigator. Instead of the main window there could be any control, but the NavigationService should be taken in the Loaded event of this control. Before this event, you can get
null . I have not studied the life cycle of controls and NavigationService in more detail.
As an alternative scenario, I could suggest using ChildWindow from
WPF Toolkit Extended , which has another Frame embedded. In this case, you can temporarily replace the NavigationService in our navigator to make the transition within such a dialogue. This will allow automating the loading of various screens into dialog boxes through binding. But the scenario of such use seems very exotic, because I will not paint in detail. If such a scenario is interesting, then I will write a separate article.
In the current implementation, the manager works extremely simply. In the navigation service setter, in addition to assigning a new value to a private field, a reply and a subscription to the event Navigated service is made.
Navigation service property public static NavigationService Service { get { return Instance._navService; } set { if (Instance._navService != null) { Instance._navService.Navigated -= Instance._navService_Navigated; } Instance._navService = value; Instance._navService.Navigated += Instance._navService_Navigated; } }
In an amicable way, the setter (and the manager’s public methods too) lack
lock . But in general, if you have an application in parallel with calling any method of navigation, the NavigationService will be replaced, then, most likely, something is implemented incorrectly. For the sake of simplicity, we can do without
lock , but I warned you.
Below are public navigation methods.
Navigation methods #region Public Methods public static void Navigate(Page page, object context) { if (Instance._navService == null || page == null) { return; } Instance._navService.Navigate(page, context); } public static void Navigate(Page page) { Navigate(page, null); } public static void Navigate(string uri, object context) { if (Instance._navService == null || uri == null) { return; } var page = Instance._resolver.GetPageInstance(uri); Navigate(page, context); } public static void Navigate(string uri) { Navigate(uri, null); } #endregion
In the code above, you may notice the use of "
_resolver ". In the section IoC I will tell about it. In short, this is the simplest implementation of the
Container for Inversion Control .
The navigation manager implements a subset of the navigation methods from the
NavigationService , which is quite enough for most simple cases. It remains only to enclose the transmitted ViewModel in the
DataContext property of the target page. This is done in the
Navigated event handler (see code below).
Handling the Navigated Event #region Private Methods void _navService_Navigated(object sender, NavigationEventArgs e) { var page = e.Content as Page; if (page == null) { return; } page.DataContext = e.ExtraData; } #endregion
In the event handler for the Navigated event, an attempt is made to convert the
Frame content to the
Page type. Thus, only the transitions to the Page will be processed. All others will be filtered. If you want, you can remove this “iron curtain”. In the case of a successful type conversion, the instance of the ViewModel 'passed in the ExtraData property of the event will be placed in the DataContext of the target page. This is all about navigation manager.
It remains to create an assembly with the implementation of the pages and the assembly of the ViewModels. I also implemented the Helpers assembly, in which I placed the
RelayCommand implementation
code for ViewModels. If you have time and effort, proceed to the following sections with a description of the implementation of the UI and ViewModels. If not, then summarize what else needs to be implemented.
For each page you should create a separate ViewModel. These “private” ViewModels are instantiated in their parent
MainViewModel using the “
Inversion of Control ” (see the
IoC section). The main ViewModel is placed in the DataContext of the main window, but it could as well be instantiated as a static XAML resource in the resource dictionary of the main window or even at the level of the entire application. In this case, you have to specify something like Source = {StaticResource MainViewModelDataSourceKey} in the DataContext binding. But you can not worry about whether the DataContext of the logical parent is inherited in the right place.
In
MainViewModel, I created several commands. One to navigate to the string alias of the page specified in the
CommandParameter (the transition without transferring the data context). Other commands contain in their Delegate Execute a transition to some specific alias of the landing page with data context received via the
CommandParameter . For details, go
to GitHub or continue reading this article.
Build ViewModels
This assembly introduces the base ViewModel, which implements
INotifyPropertyChanged .
BaseViewModel public class BaseViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected void RaisePropertyChanged(string propertyName) { if (string.IsNullOrWhiteSpace(propertyName)) { return; } if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } }
The rest of the ViewModels are inherited from it and are currently extremely simple. They contain one string property with a unique name (see example below).
public class Page1ViewModel : BaseViewModel { public string Page1Text { get { return "Hello, world!\nSent from my iPage 1..."; } } }
NoteNotice that here the property is read-only and without calling RaisePropertyChanged (...) anywhere. In this case, it was done for simplicity. In practice, these properties ViewModel'ey meet, but rarely, because Binding on such properties will work only once. Even if I add a setter without RaisePropertyChanged (...), the Binding will still be “one-time”.
MainViewModel is much more complicated. As I wrote briefly in the previous section, it will store private ViewModels and implement navigation commands. In my case, private ViewModels are created only once using the so-called "
Resolver ", when the MainViewModel is initialized. Therefore, I have implemented only the getters of these ViewModels.
public Page1ViewModel Page1ViewModel { get { return _p1ViewModel; } } public Page2ViewModel Page2ViewModel { get { return _p2ViewModel; } } public Page3ViewModel Page3ViewModel { get { return _p3ViewModel; } }
Fields are initialized in the MainViewModel constructor:
_p1ViewModel = _resolver.GetViewModelInstance(Page1ViewModelAlias); _p2ViewModel = _resolver.GetViewModelInstance(Page2ViewModelAlias); _p3ViewModel = _resolver.GetViewModelInstance(Page3ViewModelAlias);
Note" _resolver " in this case is another Management Inversion Container, which will be discussed in the appropriate section. At the moment, this Resolver simply gets a delegate from the dictionary that corresponds to the alias of the ViewModel. It is also worth noting that in practice you may need to make a full implementation of the fields and properties for private ViewModels. This is already done elementary.
Commands in my case are implemented with the instructions
get and
set , and the initialization of their instances is placed in a separate function. Having the command setters allows me to replace each command outside the current ViewModel. Such an approach allows, for example, to change the reaction of the dialog box by clicking the “OK” button, if it is linked via Binding to the corresponding command in its (dialogue) internal ViewModel. However, such a scenario is quite exotic and can be implemented without command setters.
Command implementation public ICommand GoToPathCommand { get { return _goToPathCommand; } set { _goToPathCommand = value; RaisePropertyChanged("GoToPathCommand"); } } public ICommand GoToPage1Command { get { return _goToPage1Command; } set { _goToPage1Command = value; RaisePropertyChanged("GoToPage1Command"); } } private void InitializeCommands() { GoToPathCommand = new RelayCommand<string>(GoToPathCommandExecute); GoToPage1Command = new RelayCommand<Page1ViewModel>(GoToPage1CommandExecute); GoToPage2Command = new RelayCommand<Page2ViewModel>(GoToPage2CommandExecute); GoToPage3Command = new RelayCommand<Page3ViewModel>(GoToPage3CommandExecute); } private void GoToPathCommandExecute(string path) { if (string.IsNullOrWhiteSpace(path)) { return; } var uri = new Uri(path); Navigation.Navigate(uri); } private void GoToPage1CommandExecute(Page1ViewModel viewModel) { Navigation.Navigate(Navigation.Page1Alias, Page1ViewModel); }
Note that the alias of the target page is passed as the path. I put these aliases in the form of constants in the navigation manager, but in general the best place for them is in the XML configuration file or just in some kind of text dictionary.
After the
GoToPage1Command command is executed, the
page will be
navigated to the specified alias, and the
Page1ViewModel will be put in the DataContext of the page. Thus, we do not need to implement additional logic to get data back from the landing page. It will work with the repository inside our MainViewModel, so we will automatically receive all changes before we go back.
It seems to be all with ViewModel'ami. Go to UI.
Main window and Pages assembly
I will give again for convenience a type of the test application.

On the left are four buttons. The first button is bound to the GoToPathCommand command and transitions to Page1 without a data context. After navigating to a page without a data context, the value from the FallbackValue parameter of the Binding object will be substituted for the actual value from the ViewModel. The rest of the buttons are tied to “private” teams with the alias of the required page of the page indicated in the team delegate.
Layout and code of the main window <Window x:Class="Navigator.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="480" Width="640"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition /> </Grid.ColumnDefinitions> <ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Hidden"> <StackPanel> <Button Content="P 1 w/o data" Command="{Binding GoToPathCommand}" CommandParameter="pack://application:,,,/Pages;component/Page1.xaml"/> <Button Content="Page 1" Command="{Binding GoToPage1Command}" CommandParameter="{Binding Page1ViewModel}"/> <Button Content="Page 2" Command="{Binding GoToPage2Command}" CommandParameter="{Binding Page2ViewModel}"/> <Button Content="Page 3" Command="{Binding GoToPage3Command}" CommandParameter="{Binding Page3ViewModel}"/> </StackPanel> </ScrollViewer> <Frame x:Name="MainFrame" Grid.Column="1" Background="#CCCCCC"/> </Grid> </Window>
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); Loaded += MainWindow_Loaded; } void MainWindow_Loaded(object sender, RoutedEventArgs e) { Navigation.Service = MainFrame.NavigationService; DataContext = new MainViewModel(); } }
The Pages assembly contains four pages: Page1, Page2, Page3, Page404. The first two simply contain text blocks attached to the property of the corresponding private ViewModel. I made the third one a bit more complicated to implement another MVVM problem, namely the task of binding ListBox.SelectedItems to ViewModel. This is a separate topic, which in my opinion deserves a separate article. For interest, you can look under the spoiler markup below.
Page3 markup <Page x:Class="Pages.Page3" 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:tkx="http://schemas.xceed.com/wpf/xaml/toolkit" mc:Ignorable="d" d:DesignHeight="400" d:DesignWidth="400"> <Grid> <tkx:ChildWindow WindowState="Open" Caption="My Dialog" IsModal="True" WindowStartupLocation="Center"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition /> </Grid.RowDefinitions> <TextBlock Text="{Binding Page3Text, FallbackValue='No Data'}" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="24" FontWeight="Bold"/> <StackPanel Grid.Row="1"> <TextBlock Text="Category:" Margin="5"/> <ComboBox Text="Select..." Margin="5"> <ComboBoxItem Content="Category 1"/> <ComboBoxItem Content="Category 2"/> <ComboBoxItem Content="Category 3"/> </ComboBox> <TextBlock Text="Items:" Margin="5 10 5 5"/> <ListBox Margin="5" SelectionMode="Multiple"> <ListBoxItem Content="Item 1"/> <ListBoxItem Content="Item 2"/> <ListBoxItem Content="Item 3"/> <ListBoxItem Content="Item 4"/> <ListBoxItem Content="Item 5"/> <ListBoxItem Content="Item 6"/> <ListBoxItem Content="Item 7"/> <ListBoxItem Content="Item 8"/> </ListBox> </StackPanel> </Grid> </tkx:ChildWindow> </Grid> </Page>
On this page, I posted a simple dialogue with the selection of items from a specific category. This is an example close to reality. The dialogue view was shown in the screenshot above. Notice that the text block appears to be in the dialog box, but it binds to the DataContext of the page directly, without any tweaks. This is the merit of ChildWindow from WPF Toolkit Extended. This control actually only simulates the behavior of a dialog box and is a direct descendant of its parent in the XAML markup. Thus, the DataContext is inherited in the ChildWindow from the Grid into which I placed it.
Briefly about the problem of binding multiple selections in a ListBox. To return the list of selected ListBox elements to the ViewModel, I cannot use the binding directly, because The ListBox.SelectedItems property does not support binding. To solve this problem, you can inherit from the ListBox your control in which to add a DependencyProperty. However, there is a more flexible approach in the context of MVVM, which I am going to write about in a separate article, if you find this interesting.
IoC (Inversion Control)
Unfortunately, I cannot describe this approach in detail in this article. The volume is so great. But you can learn the necessary knowledge, for example,
from articles on Habré . Also a lot of resources you can google. In short, “Inversion of Control” is a way to eliminate direct links in one assembly to another assembly. Injection of dependencies is performed by special "
Containers ", which from the configuration files will learn which classes specifically and from which assemblies to initialize for the specified interface and the section name in the config. It is necessary to admit that in my code IoC is not fully implemented. To be honest, there was no such goal. Of course, I tried to reflect the concept of IoC in the code and tried to show how to make the code less connected.
Below are the interfaces of containers and their implementation.
namespace ViewModels.Interfaces { public interface IViewModelsResolver { INotifyPropertyChanged GetViewModelInstance(string alias); } } namespace Navigator.Navigation.Interfaces { public interface IPageResolver { Page GetPageInstance(string alias); } }
These interfaces play the role of certain contracts for various implementations of page containers and ViewModels. At the moment I have made two implementations that you
should not use in real projects in any way .
Implementations of test containers namespace Navigator.Navigation { public class PagesResolver : IPageResolver { private readonly Dictionary<string, Func<Page>> _pagesResolvers = new Dictionary<string, Func<Page>>(); public PagesResolver() { _pagesResolvers.Add(Navigation.Page1Alias, () => new Page1()); _pagesResolvers.Add(Navigation.Page2Alias, () => new Page2()); _pagesResolvers.Add(Navigation.Page3Alias, () => new Page3()); _pagesResolvers.Add(Navigation.NotFoundPageAlias, () => new Page404()); } public Page GetPageInstance(string alias) { if (_pagesResolvers.ContainsKey(alias)) { return _pagesResolvers[alias](); } return _pagesResolvers[Navigation.NotFoundPageAlias](); } } } namespace ViewModels { public class ViewModelsResolver : IViewModelsResolver { private readonly Dictionary<string, Func<INotifyPropertyChanged>> _vmResolvers = new Dictionary<string, Func<INotifyPropertyChanged>>(); public ViewModelsResolver() { _vmResolvers.Add(MainViewModel.Page1ViewModelAlias, () => new Page1ViewModel()); _vmResolvers.Add(MainViewModel.Page2ViewModelAlias, () => new Page2ViewModel()); _vmResolvers.Add(MainViewModel.Page3ViewModelAlias, () => new Page3ViewModel()); _vmResolvers.Add(MainViewModel.NotFoundPageViewModelAlias, () => new Page404ViewModel()); } public INotifyPropertyChanged GetViewModelInstance(string alias) { if (_vmResolvers.ContainsKey(alias)) { return _vmResolvers[alias](); } return _vmResolvers[MainViewModel.NotFoundPageViewModelAlias](); } } }
These are just “dolls” of containers that need to be replaced with something manageable, for example from the
Unity library. As interfaces, it would also be better to use something like
IUnityContainer , but I didn’t want to burden the solution with an additional reference and complicate the perception of my navigator implementation. Moreover, you may prefer any other IoC library instead of Unity.
additional literature
About singleton on wikipediaAbout singleton on HabrahabrAbout MVVM Pattern on WikipediaAbout MVVM pattern on Habrahabr