In this application, views are hosted in master pages. These master pages contains a frame used to contain the view and to enable navigation between views.
There are two kind of master pages: the ShellView and the MainShellView.
The MainShellView is composed by a lateral menu, a main Frame
, and a status bar on the bottom:
This is the XAML representation of the view:
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<NavigationView x:Name="navigationView" MenuItemsSource="{x:Bind ViewModel.Items}" MenuItemTemplate="{StaticResource NavigationViewItem}"
SelectedItem="{x:Bind ViewModel.SelectedItem, Mode=TwoWay}" SelectionChanged="OnSelectionChanged"
IsPaneOpen="{x:Bind ViewModel.IsPaneOpen, Mode=TwoWay}" AlwaysShowHeader="False">
<Grid>
<ProgressRing IsActive="{x:Bind ViewModel.IsBusy}" />
<Frame x:Name="frame">
<Frame.ContentTransitions>
<TransitionCollection>
<NavigationThemeTransition/>
</TransitionCollection>
</Frame.ContentTransitions>
</Frame>
</Grid>
</NavigationView>
<Grid Grid.Row="1" Background="{ThemeResource SystemControlAccentAcrylicElementAccentMediumHighBrush}">
<Rectangle Fill="IndianRed" Visibility="{x:Bind ViewModel.IsError, Mode=OneWay}" />
<TextBlock Margin="6,4" Text="{x:Bind ViewModel.Message, Mode=OneWay}" Foreground="White" FontSize="12" />
</Grid>
</Grid>
The NavigationView
control represents the menu, and we can identify inside it the main Frame where our content will be changed when we select a new menú option. At the end of the main Grid we can identify the Status Bar where we will display the status of the user actions when interacts with the app.
It has the same structure of the MainShellView
but without the Lateral Menu:
The XMAL markup for this shell is:
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<ProgressRing IsActive="{x:Bind ViewModel.IsBusy}" />
<Frame x:Name="frame">
<Frame.ContentTransitions>
<TransitionCollection>
<NavigationThemeTransition/>
</TransitionCollection>
</Frame.ContentTransitions>
</Frame>
</Grid>
<Grid Grid.Row="1" Background="{ThemeResource SystemControlAccentAcrylicElementAccentMediumHighBrush}">
<Rectangle Fill="IndianRed" Visibility="{x:Bind ViewModel.IsError, Mode=OneWay}" />
<TextBlock Margin="6,4" Text="{x:Bind ViewModel.Message, Mode=OneWay}" Foreground="White" FontSize="12" />
</Grid>
</Grid>
The difference with the main shell is the lack of a NavigationView
control. The ShellView is the master page that we will use when a new window is displayed, i.e. calling the CreateNewViewAsync
of our NavigationService.
It's important to have clear the Shells that we are going to display in the app in order to understand how the Navigation of the app works.
The Main Frame defined in the prevoius Shells is the reponsable for the navigation in the app. We can check how the Frame control is used for navigate here.
When the MainShellView or the ShellView is loaded the Navigation Service has to be initialiaze in order to use the Frame as navigation control:
private void InitializeNavigation()
{
_navigationService = ServiceLocator.Current.GetService<INavigationService>();
_navigationService.Initialize(frame);
frame.Navigated += OnFrameNavigated;
CurrentView.BackRequested += OnBackRequested;
}
Once the Navigation Service is initialized from the Shell views. We are ready to navigate using the INavigationService
in our ViewModels to navigate.
As you may know, in UWP apps the pages are recreated everytime we navigate to them, unless we want to preserve them in memory setting the property NavigationCacheMode
to Required
or Enabled
.
With the purpose of having the best performance we can, we have decided not to cache the pages and save the state of them in the Navigation process. This page state will be passed as an argument when we navigate to a page, and retreived when we navigate back.
So, the first thing to do is defined an argument per ViewModel. Let's take for example the CustomerListViewModel
. We will have a class defined for the arguments of this CustomerListArgs
:
public class CustomerListArgs
{
static public CustomerListArgs CreateEmpty() => new CustomerListArgs { IsEmpty = true };
public CustomerListArgs()
{
OrderBy = r => r.FirstName;
}
public bool IsEmpty { get; set; }
public string Query { get; set; }
public Expression<Func<Customer, object>> OrderBy { get; set; }
public Expression<Func<Customer, object>> OrderByDesc { get; set; }
}
As we can see, we are saving the possible values of the actions that the user can do over a list.
If we have a look at the Navigate
method implemented of the Navigation Service, we have a nullable parameter
in addition of the type of the ViewModel we want to navigate to.
public bool Navigate(Type viewModelType, object parameter = null)
{
if (Frame == null)
{
throw new InvalidOperationException("Navigation frame not initialized.");
}
return Frame.Navigate(GetView(viewModelType), parameter);
}
We will pass the in this parameter
the CustomerListArgs
previously defined:
NavigationService.Navigate(viewModel, new CustomerListArgs());
Now we need to reflect the passed state in the navigation into the View. To accomplish that, we need to overrride the View method OnNavigatedTo
and get the state in the Parameter
property of the NavigationEventArgs
received:
protected override async void OnNavigatedTo(NavigationEventArgs e)
{
ViewModel.Subscribe();
await ViewModel.LoadAsync(e.Parameter as CustomerListArgs);
}
For those ViewModels we want to save the state, we are creating a public method in them to be called from our Views, the LoadAsync
method. This method, just simple receive the status saved in the CustomerListArgs
object, and load it in the ViewModel:
public async Task LoadAsync(CustomerListArgs args)
{
ViewModelArgs = args ?? CustomerListArgs.CreateEmpty();
Query = args.Query;
StartStatusMessage("Loading customers...");
await RefreshAsync();
EndStatusMessage("Customers loaded");
}