This article is a reprint
Original author: RyzenAdorer
Original title: Navigation System of .NET Core 3 WPF MVVM Framework Prism Series
Original link: https://www.cnblogs.com/ryzen/p/12703914.html
This article will introduce how to use the MVVM framework Prism's region-based navigation system in a .NET Core 3 environment.
Before explaining the Prism navigation system, let's look at an example. I created a login interface in the previous demo project:

Here we might initially think of using WPF's built-in navigation system, using Frame and Page for page navigation, and then using GoBack and GoForward from the navigation journal to go back and forward. In fact, this is implemented using Prism's navigation framework. Below we will see how to achieve this functionality in Prism's MVVM mode.
1. Region Navigation
In the previous article, we introduced Prism's region management. Prism's navigation system is also based on regions. First, let's look at how to navigate using regions.
1.1. Registering a Region
LoginWindow.xaml:
<Window
x:Class="PrismMetroSample.Shell.Views.Login.LoginWindow"
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:PrismMetroSample.Shell.Views.Login"
xmlns:region="clr-namespace:PrismMetroSample.Infrastructure.Constants;assembly=PrismMetroSample.Infrastructure"
mc:Ignorable="d"
xmlns:prism="http://prismlibrary.com/"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
Height="600"
Width="400"
prism:ViewModelLocator.AutoWireViewModel="True"
ResizeMode="NoResize"
WindowStartupLocation="CenterScreen"
Icon="pack://application:,,,/PrismMetroSample.Infrastructure;Component/Assets/Photos/Home, homepage, menu.png"
>
<i:Interaction.Triggers>
<i:EventTrigger EventName="Loaded">
<i:InvokeCommandAction Command="{Binding LoginLoadingCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
<Grid>
<ContentControl
prism:RegionManager.RegionName="{x:Static region:RegionNames.LoginContentRegion}"
Margin="5"
/>
</Grid>
</Window>
1.2. Registering Navigation
App.cs:
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
containerRegistry.Register<IMedicineSerivce, MedicineSerivce>();
containerRegistry.Register<IPatientService, PatientService>();
containerRegistry.Register<IUserService, UserService>();
//Register global commands
containerRegistry.RegisterSingleton<IApplicationCommands, ApplicationCommands>();
containerRegistry.RegisterInstance<IFlyoutService>(Container.Resolve<FlyoutService>());
//Register navigation
containerRegistry.RegisterForNavigation<LoginMainContent>();
containerRegistry.RegisterForNavigation<CreateAccount>();
}
1.3. Region Navigation
LoginWindowViewModel.cs:
public class LoginWindowViewModel:BindableBase
{
private readonly IRegionManager _regionManager;
private readonly IUserService _userService;
private DelegateCommand _loginLoadingCommand;
public DelegateCommand LoginLoadingCommand =>
_loginLoadingCommand ?? (_loginLoadingCommand = new DelegateCommand(ExecuteLoginLoadingCommand));
void ExecuteLoginLoadingCommand()
{
//Navigate to LoginMainContent in the LoginContentRegion region
_regionManager.RequestNavigate(RegionNames.LoginContentRegion, "LoginMainContent");
Global.AllUsers = _userService.GetAllUsers();
}
public LoginWindowViewModel(IRegionManager regionManager, IUserService userService)
{
_regionManager = regionManager;
_userService = userService;
}
}
LoginMainContentViewModel.cs:
public class LoginMainContentViewModel : BindableBase
{
private readonly IRegionManager _regionManager;
private DelegateCommand _createAccountCommand;
public DelegateCommand CreateAccountCommand =>
_createAccountCommand ?? (_createAccountCommand = new DelegateCommand(ExecuteCreateAccountCommand));
//Navigate to CreateAccount
void ExecuteCreateAccountCommand()
{
Navigate("CreateAccount");
}
private void Navigate(string navigatePath)
{
if (navigatePath != null)
_regionManager.RequestNavigate(RegionNames.LoginContentRegion, navigatePath);
}
public LoginMainContentViewModel(IRegionManager regionManager)
{
_regionManager = regionManager;
}
}
The effect is as follows:

Here we can see that we call the RequestNavigate method of RegionManager. In fact, this doesn't clearly demonstrate that it is region-based. It might be easier to understand if we write it as follows:
//Navigate to LoginMainContent in the LoginContentRegion region
_regionManager.RequestNavigate(RegionNames.LoginContentRegion, "LoginMainContent");
Replace with:
//Navigate to LoginMainContent in the LoginContentRegion region
IRegion region = _regionManager.Regions[RegionNames.LoginContentRegion];
region.RequestNavigate("LoginMainContent");
In fact, the implementation of RegionManager.RequestNavigate is roughly the same — it calls the RequestNavigate method of the Region. The navigation of a Region implements the INavigateAsync interface:
public interface INavigateAsync
{
void RequestNavigate(Uri target, Action<NavigationResult> navigationCallback);
void RequestNavigate(Uri target, Action<NavigationResult> navigationCallback, NavigationParameters navigationParameters);
}
We can see that the RequestNavigate method has three parameters:
target: The URI of the page to navigate to.navigationCallback: The callback method after navigation.navigationParameters: The parameters passed during navigation (detailed below).
So we add the callback method to the above:
//Navigate to LoginMainContent in the LoginContentRegion region
IRegion region = _regionManager.Regions[RegionNames.LoginContentRegion];
region.RequestNavigate("LoginMainContent", NavigationCompelted);
private void NavigationCompelted(NavigationResult result)
{
if (result.Result==true)
{
MessageBox.Show("Navigation to LoginMainContent page succeeded");
}
else
{
MessageBox.Show("Navigation to LoginMainContent page failed");
}
}
The effect is as follows:

2. View and ViewModel Participating in the Navigation Process
2.1. INavigationAware
We often need to handle some logic when navigating between two pages. For example, when navigating from the LoginMainContent page to the CreateAccount page, we need to save the page data when exiting LoginMainContent, and handle logic (e.g., obtaining information from the LoginMainContent page) when navigating to the CreateAccount page. Prism's navigation system provides the INavigationAware interface:
public interface INavigationAware : Object
{
Void OnNavigatedTo(NavigationContext navigationContext);
Boolean IsNavigationTarget(NavigationContext navigationContext);
Void OnNavigatedFrom(NavigationContext navigationContext);
}
OnNavigatedFrom: Triggered before navigation, generally used to save the data of the current page.OnNavigatedTo: Triggered on the destination page after navigation, generally used for initialization or receiving parameters passed from the previous page.IsNavigationTarget: IfTrue, the View instance is reused; ifFalse, a new instance is created each time you navigate to that page.
Let's demonstrate these three methods with code:
LoginMainContentViewModel.cs:
public class LoginMainContentViewModel : BindableBase, INavigationAware
{
private readonly IRegionManager _regionManager;
private DelegateCommand _createAccountCommand;
public DelegateCommand CreateAccountCommand =>
_createAccountCommand ?? (_createAccountCommand = new DelegateCommand(ExecuteCreateAccountCommand));
void ExecuteCreateAccountCommand()
{
Navigate("CreateAccount");
}
private void Navigate(string navigatePath)
{
if (navigatePath != null)
_regionManager.RequestNavigate(RegionNames.LoginContentRegion, navigatePath);
}
public LoginMainContentViewModel(IRegionManager regionManager)
{
_regionManager = regionManager;
}
public bool IsNavigationTarget(NavigationContext navigationContext)
{
return true;
}
public void OnNavigatedFrom(NavigationContext navigationContext)
{
MessageBox.Show("Exited LoginMainContent");
}
public void OnNavigatedTo(NavigationContext navigationContext)
{
MessageBox.Show("Navigated from CreateAccount to LoginMainContent");
}
}
CreateAccountViewModel.cs:
public class CreateAccountViewModel : BindableBase,INavigationAware
{
private DelegateCommand _loginMainContentCommand;
public DelegateCommand LoginMainContentCommand =>
_loginMainContentCommand ?? (_loginMainContentCommand = new DelegateCommand(ExecuteLoginMainContentCommand));
void ExecuteLoginMainContentCommand()
{
Navigate("LoginMainContent");
}
public CreateAccountViewModel(IRegionManager regionManager)
{
_regionManager = regionManager;
}
private void Navigate(string navigatePath)
{
if (navigatePath != null)
_regionManager.RequestNavigate(RegionNames.LoginContentRegion, navigatePath);
}
public bool IsNavigationTarget(NavigationContext navigationContext)
{
return true;
}
public void OnNavigatedFrom(NavigationContext navigationContext)
{
MessageBox.Show("Exited CreateAccount");
}
public void OnNavigatedTo(NavigationContext navigationContext)
{
MessageBox.Show("Navigated from LoginMainContent to CreateAccount");
}
}
The effect is as follows:

Change IsNavigationTarget to false:
public class LoginMainContentViewModel : BindableBase, INavigationAware
{
public bool IsNavigationTarget(NavigationContext navigationContext)
{
return false;
}
}
public class CreateAccountViewModel : BindableBase,INavigationAware
{
public bool IsNavigationTarget(NavigationContext navigationContext)
{
return false;
}
}
The effect is as follows:

We notice that the data on the LoginMainContent and CreateAccount pages disappears. This is because when IsNavigationTarget is false and you navigate to the page a second time, the View is re-instantiated, causing the ViewModel to reload as well, thus clearing all data.
2.2. IRegionMemberLifetime
Additionally, Prism can control the lifecycle of views in a region through the KeepAlive boolean property of the IRegionMemberLifetime interface. In the previous article on region management, we mentioned that when a view is added to a region, for a ContentControl that displays only one active view at a time, you can use the Activate and Deactivate methods of the Region to activate and deactivate views. For an ItemsControl that can display multiple active views simultaneously, you can use the Add and Remove methods to add and remove active views. When a view's KeepAlive is false, and the Region activates another view, the instance of that view is removed from the region. Why didn't we introduce this interface in the region management section? Because during navigation, it also triggers Activate and Deactivate of the Region. When the IRegionMemberLifetime interface is implemented, it triggers the Add and Remove methods of the Region. You can refer to the Prism source code for RegionMemberLifetimeBehavior for more details.
Let's implement the IRegionMemberLifetime interface in LoginMainContentViewModel and set KeepAlive to false, while keeping IsNavigationTarget set to true.
LoginMainContentViewModel.cs:
public class LoginMainContentViewModel : BindableBase, INavigationAware,IRegionMemberLifetime
{
public bool KeepAlive => false;
private readonly IRegionManager _regionManager;
private DelegateCommand _createAccountCommand;
public DelegateCommand CreateAccountCommand =>
_createAccountCommand ?? (_createAccountCommand = new DelegateCommand(ExecuteCreateAccountCommand));
void ExecuteCreateAccountCommand()
{
Navigate("CreateAccount");
}
private void Navigate(string navigatePath)
{
if (navigatePath != null)
_regionManager.RequestNavigate(RegionNames.LoginContentRegion, navigatePath);
}
public LoginMainContentViewModel(IRegionManager regionManager)
{
_regionManager = regionManager;
}
public bool IsNavigationTarget(NavigationContext navigationContext)
{
return true;
}
public void OnNavigatedFrom(NavigationContext navigationContext)
{
MessageBox.Show("Exited LoginMainContent");
}
public void OnNavigatedTo(NavigationContext navigationContext)
{
MessageBox.Show("Navigated from CreateAccount to LoginMainContent");
}
}
The effect is as follows:

We find that the result is the same as not implementing the IRegionMemberLifetime interface and setting IsNavigationTarget to false. When KeepAlive is false, through debugging, we know that when navigating back to the LoginMainContent page, the IsNavigationTarget method is not triggered. Therefore, the order of evaluation is: KeepAlive -> IsNavigationTarget.
2.3. IConfirmNavigationRequest
Prism's navigation system also supports the ability to ask for confirmation before navigation. Here, after registering a user in CreateAccount, we ask whether to navigate back to the LoginMainContent page. The code is as follows:
CreateAccountViewModel.cs:
public class CreateAccountViewModel : BindableBase, INavigationAware,IConfirmNavigationRequest
{
private DelegateCommand _loginMainContentCommand;
public DelegateCommand LoginMainContentCommand =>
_loginMainContentCommand ?? (_loginMainContentCommand = new DelegateCommand(ExecuteLoginMainContentCommand));
private DelegateCommand<object> _verityCommand;
public DelegateCommand<object> VerityCommand =>
_verityCommand ?? (_verityCommand = new DelegateCommand<object>(ExecuteVerityCommand));
void ExecuteLoginMainContentCommand()
{
Navigate("LoginMainContent");
}
public CreateAccountViewModel(IRegionManager regionManager)
{
_regionManager = regionManager;
}
private void Navigate(string navigatePath)
{
if (navigatePath != null)
_regionManager.RequestNavigate(RegionNames.LoginContentRegion, navigatePath);
}
public bool IsNavigationTarget(NavigationContext navigationContext)
{
return true;
}
public void OnNavigatedFrom(NavigationContext navigationContext)
{
MessageBox.Show("Exited CreateAccount");
}
public void OnNavigatedTo(NavigationContext navigationContext)
{
MessageBox.Show("Navigated from LoginMainContent to CreateAccount");
}
//Register account
void ExecuteVerityCommand(object parameter)
{
if (!VerityRegister(parameter))
{
return;
}
MessageBox.Show("Registration successful!");
LoginMainContentCommand.Execute();
}
//Ask before navigation
public void ConfirmNavigationRequest(NavigationContext navigationContext, Action<bool> continuationCallback)
{
var result = false;
if (MessageBox.Show("Do you need to navigate to the LoginMainContent page?", "Navigate?", MessageBoxButton.YesNo) == MessageBoxResult.Yes)
{
result = true;
}
continuationCallback(result);
}
}
The effect is as follows:

3. Passing Parameters During Navigation
Prism provides the NavigationParameters class to help specify and retrieve navigation parameters. During navigation, you can pass navigation parameters by accessing:
- The
NavigationParametersproperty of theNavigationContextobject in theIsNavigationTarget,OnNavigatedFrom, andOnNavigatedTomethods of theINavigationAwareinterface. - The
NavigationParametersproperty of theNavigationContextobject in theConfirmNavigationRequestmethod of theIConfirmNavigationRequestinterface. - Assigning the
navigationParametersparameter of theRequestNavigatemethod of theINavigateAsyncinterface during region navigation. - The
Parametersproperty of typeNavigationParametersof theCurrentEntryproperty of theIRegionNavigationJournalinterface (introduced below in the navigation journal section).
Here, after registering a user in the CreateAccount page, we ask whether to use the currently registered user as the login ID to demonstrate passing navigation parameters. The code is as follows:
CreateAccountViewModel.cs (modified part):
private string _registeredLoginId;
public string RegisteredLoginId
{
get { return _registeredLoginId; }
set { SetProperty(ref _registeredLoginId, value); }
}
public bool IsUseRequest { get; set; }
void ExecuteVerityCommand(object parameter)
{
if (!VerityRegister(parameter))
{
return;
}
this.IsUseRequest = true;
MessageBox.Show("Registration successful!");
LoginMainContentCommand.Execute();
}
public void ConfirmNavigationRequest(NavigationContext navigationContext, Action<bool> continuationCallback)
{
if (!string.IsNullOrEmpty(RegisteredLoginId) && this.IsUseRequest)
{
if (MessageBox.Show("Do you want to log in with the currently registered user?", "Navigate?", MessageBoxButton.YesNo) == MessageBoxResult.Yes)
{
navigationContext.Parameters.Add("loginId", RegisteredLoginId);
}
}
continuationCallback(true);
}
LoginMainContentViewModel.cs (modified part):
public void OnNavigatedTo(NavigationContext navigationContext)
{
MessageBox.Show("Navigated from CreateAccount to LoginMainContent");
var loginId = navigationContext.Parameters["loginId"] as string;
if (loginId != null)
{
this.CurrentUser = new User() { LoginId = loginId };
}
}
The effect is as follows:

4. Navigation Journal
Prism's navigation system, like WPF's navigation system, supports a navigation journal. Prism provides the IRegionNavigationJournal interface to implement region navigation journal functionality.
public interface IRegionNavigationJournal
{
bool CanGoBack { get; }
bool CanGoForward { get; }
IRegionNavigationJournalEntry CurrentEntry { get; }
INavigateAsync NavigationTarget { get; set; }
void GoBack();
void GoForward();
void RecordNavigation(IRegionNavigationJournalEntry entry, bool persistInHistory);
void Clear();
}
We will add navigation journal functionality to the login interface. The code is as follows:
LoginMainContent.xaml (part of the forward arrow code):
<TextBlock Width="30" Height="30" HorizontalAlignment="Right" Text="" FontWeight="Bold" FontFamily="pack://application:,,,/PrismMetroSample.Infrastructure;Component/Assets/Fonts/#iconfont" FontSize="30" Margin="10" Visibility="{Binding IsCanExcute,Converter={StaticResource boolToVisibilityConverter}}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseLeftButtonDown">
<i:InvokeCommandAction Command="{Binding GoForwardCommand}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#F9F9F9"/>
</Trigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
BoolToVisibilityConverter.cs:
public class BoolToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value==null)
{
return DependencyProperty.UnsetValue;
}
var isCanExcute = (bool)value;
if (isCanExcute)
{
return Visibility.Visible;
}
else
{
return Visibility.Hidden;
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
LoginMainContentViewModel.cs (modified part):
IRegionNavigationJournal _journal;
private DelegateCommand<PasswordBox> _loginCommand;
public DelegateCommand<PasswordBox> LoginCommand =>
_loginCommand ?? (_loginCommand = new DelegateCommand<PasswordBox>(ExecuteLoginCommand, CanExecuteGoForwardCommand));
private DelegateCommand _goForwardCommand;
public DelegateCommand GoForwardCommand =>
_goForwardCommand ?? (_goForwardCommand = new DelegateCommand(ExecuteGoForwardCommand));
private void ExecuteGoForwardCommand()
{
_journal.GoForward();
}
private bool CanExecuteGoForwardCommand(PasswordBox passwordBox)
{
this.IsCanExcute = _journal != null && _journal.CanGoForward;
return true;
}
public void OnNavigatedTo(NavigationContext navigationContext)
{
//MessageBox.Show("Navigated from CreateAccount to LoginMainContent");
_journal = navigationContext.NavigationService.Journal;
var loginId = navigationContext.Parameters["loginId"] as string;
if (loginId != null)
{
this.CurrentUser = new User() { LoginId = loginId };
}
LoginCommand.RaiseCanExecuteChanged();
}
CreateAccountViewModel.cs (modified part):
IRegionNavigationJournal _journal;
private DelegateCommand _goBackCommand;
public DelegateCommand GoBackCommand =>
_goBackCommand ?? (_goBackCommand = new DelegateCommand(ExecuteGoBackCommand));
void ExecuteGoBackCommand()
{
_journal.GoBack();
}
public void OnNavigatedTo(NavigationContext navigationContext)
{
//MessageBox.Show("Navigated from LoginMainContent to CreateAccount");
_journal = navigationContext.NavigationService.Journal;
}
The effect is as follows:

4.1 Opting Out of the Navigation Journal
If you do not want a page to be added to the navigation journal during navigation, for example, the LoginMainContent page, you can implement the IJournalAware interface and return false from PersistInHistory():
public class LoginMainContentViewModel : IJournalAware
{
public bool PersistInHistory() => false;
}
5. Summary
Prism's navigation system can be used in parallel with WPF navigation, as also supported by the Prism official documentation. Because Prism's navigation system is based on regions and does not depend on WPF, it is recommended to use Prism's navigation system alone, as it is more flexible in MVVM mode, supports dependency injection, and allows better management of views through the region manager, making it more suitable for complex application requirements. WPF's navigation system does not support dependency injection and relies on the Frame element, and during navigation, it easily creates strong dependencies on the View part. The next article will cover Prism's dialog service.
6. Source Code
Finally, the source code for the entire demo is attached: PrismDemo Source Code