How to prevent the navigation off a page in WPF

Colonial navigation...
Colonial navigation…
There’s nothing special in this article, but it’s just a tip for those like me are looking for a way to mimic the Windows Store look-and-feel by using WPF.
Among the tons of features there’s a basic one, which allows to properly manage the page navigation, when hosted within a Frame element.
The aim is detecting when the user requests any kind of navigation off the current page viewed, so that the application can intercept and, if applies, prevent the navigation and keeps on the original page.
My deal is to leverage the Modern-UI/Store-styling best ideas, but keeping sticky to WPF. There are many reasons for keep going to code on WPF, rather Store: I really hope the Microsoft guys will think seriously to merge these two technologies in a near future.

The typical scenario.

The “old-school” UI-style mostly mimics a transaction: a dialog/window will open, and it presents a series of controls (textboxes, checkboxes, etc). As long the user won’t press the “OK” button (or the “Apply” one), any modification to the controls is cached by the “form” hosted. That is, if the user presses the “Cancel” button, it’s much like as the dialog weren’t open at all.
However, if you own any Windows Phone device, you’ll notice that most of the settings are applied as the user changes them: there’s no any confirmation, nor a way to “cancel” any modification. The same behavior is present on the “Modern-UI” sections of Windows 8 (i.e. off the Desktop). Same as the phones is WinRT.

Typical Modern-UI style
Typical Modern-UI style

My goal is having the beauty of the page navigation, typical of the Store/Win8 app, but with the ability to give the app some important features such the form cancellation. Since an user might flip to another page in any time, the code needs some (reliable) trick for detecting when the page is being left, and prevent it.
Indeed, the NavigationService object does almost everything for getting you the life easy, but still exists some extras that you have to manage by yourself.

Let’s start from a basic window, which embeds a frame and the navigation bar.

<Window x:Class="WpfApplication2.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" 
        Height="350" Width="525"
        WindowStartupLocation="CenterScreen"
        >
    
    <Grid>
        <Frame
            NavigationUIVisibility="Visible"
            x:Name="f1"
            />
    </Grid>
</Window>

In the code behind, as the window loads, a “Page1” instance is created, then placed as content for the frame.

namespace WpfApplication2
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            this.Loaded += MainWindow_Loaded;
        }

        void MainWindow_Loaded(object sender, RoutedEventArgs e)
        {
            this.f1.Navigate(new Page1());
        }
    }
}

The “Page1” class looks as follows.

<Page x:Class="WpfApplication2.Page1"
      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" 
      mc:Ignorable="d" 
      d:DesignHeight="300" d:DesignWidth="300"
	Title="Page1">

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        
        <TextBlock 
            Text="{Binding Path=Title, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Page}}" 
            FontSize="40" 
            Grid.Row="0"
            Margin="20,5"
            />
        
        <CheckBox
            Content="Prevent back browsing"
            x:Name="ChkBack"
            Grid.Row="1"
            HorizontalAlignment="Left"
            VerticalAlignment="Center"
            Margin="10,5"
            />
        
        <Button
            Content="Next page"
            Click="Button_Click_1"
            Grid.Row="1"
            Width="100"
            Height="30"
            HorizontalAlignment="Right"
            Margin="10,5"
            />
    </Grid>
</Page>

…and its code behind…

namespace WpfApplication2
{
    /// <summary>
    /// Interaction logic for Page1.xaml
    /// </summary>
    public partial class Page1 : Page
    {
        public Page1()
        {
            InitializeComponent();
            this.Loaded += Page1_Loaded;
            this.Unloaded += Page1_Unloaded;
        }

        void Page1_Loaded(object sender, RoutedEventArgs e)
        {
            Console.WriteLine("{0} is loaded", this.Title);
        }

        void Page1_Unloaded(object sender, RoutedEventArgs e)
        {
            Console.WriteLine("{0} is unloaded", this.Title);
        }

        private void Button_Click_1(object sender, RoutedEventArgs e)
        {
            this.NavigationService.Navigate(new Page2());
        }
    }
}

When run, the app clearly shows the window with the hosted page.

page1

NOTE: For sake of simplicity, in this demo there are three pages almost identical, except for the title’s name, just for distinguish them. For this reason, in some context a control might not have sense. An evident example is about the “prevent back browsing” checkbox for the first page.

Now, the “next page” button should take us to a “Page2”, by creating a brand new instance of this page. This function is supposed having any restriction.

Once the second page has shown, maybe we would like to get back the first one. However, the code should be aware of this, and to demonstrate this, we’ll use the checkbox: when checked the navigation back should be canceled.

page2

What happens to a page when a navigation takes place?

That’s really interesting, because is something different than usually happens for a normal window.
We know that the Loaded event is fired once, when the window completes its creation process and is ready for the interaction. This concept is appliable to any FrameworkElement.
Similarly, the Unloaded event is fired when the instance is being destroyed, or is no more useful.

NOTE: never rely too much on the Unloaded event, because not in any circumstance may be fired.

For a certain Page instance, the Loaded and the Unloaded event may be fired many times along the page life-cycle. Of course, the very first Loaded event is fired whenever the page loads at all, as any other element, but it will be called also any time we navigate back to that page instance, when cached in the journal.
The same consideration applies for the Unloaded event.
So, we can’t rely on the Loaded/Unloaded events for keeping track of the page instance life-cycle: the same instance will survive until the journal trash it, but the events are fired many times.
This is very important, because may lead easily to unpredictable behaviors, when not properly managed.

The “Navigation” event.

As mentioned, the Frame exposes a very useful NavigationService, which manages the actual page flipping, the journal, and also a kind of notification about what is going under the hood. It’s worthwhile noting that the real value of this engine is when the page is loaded asynchronously, for example by an URI.
Here is very simple, and the page is fed directly as element.
As said, the navigation service yields a notification event for alerting any subscriber of the incoming new action requested by the user. This is the Navigating event.

The most straightforward approach is subscribing the event on the Loaded event handler, then unsubscribe it on the Unloaded one. However, seems that there’s no more NavigationService available at the unloading time. So, we must keep a reference in order to properly remove the event callback.

namespace WpfApplication2
{
    /// <summary>
    /// Interaction logic for Page1.xaml
    /// </summary>
    public partial class Page1 : Page
    {
        public Page1()
        {
            InitializeComponent();
            this.Loaded += Page1_Loaded;
            this.Unloaded += Page1_Unloaded;
        }

        private NavigationService _navsvc;

        void Page1_Loaded(object sender, RoutedEventArgs e)
        {
            Console.WriteLine("{0} is loaded", this.Title);
            this._navsvc = this.NavigationService;
            this._navsvc.Navigating += NavigationService_Navigating;
        }

        void Page1_Unloaded(object sender, RoutedEventArgs e)
        {
            this._navsvc.Navigating -= NavigationService_Navigating;
            this._navsvc = null;
            Console.WriteLine("{0} is unloaded", this.Title);
        }

        void NavigationService_Navigating(object sender, NavigatingCancelEventArgs e)
        {
            Console.WriteLine("{0} is navigating: {1}", this.Title, e.NavigationMode);
            if (this.ChkBack.IsChecked == true && e.NavigationMode == NavigationMode.Back)
            {
                e.Cancel = true;
            }
        }

        private void Button_Click_1(object sender, RoutedEventArgs e)
        {
            this.NavigationService.Navigate(new Page2());
        }
    }
}

Honestly, I would have loved to use the weak event pattern, which has a wonderful helper on the latest .Net Framework 4.5 edition. However, for some (unknown) reason, the weak event subscription is not working for this case. I tried to inspect what’s happening behind, but the framework’s code is looking particularly complex.

For those are interested, here is the code of the sample application above described.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s