Friday, July 20, 2018

Interrogating the navigation service

This is my first post for some time. I've been on vacation and busy working to drag an accounts payable application into the 21st century before we implement it.

I have a requirement to allow users to drill down into related documents, then return. For example, a purchase order may have several payments. While editing a payment, the user wants to be able to open the purchase order as a new page and then return to the payment. Furthermore the user wants to be able to be looking at a payment, then drill down to the purchase order, then drill further down into any of the purchase order's payments - potentially into the payment they had open in the first place.

This is fine except the second instance of the payment must be opened in read-only mode to avoid concurrency issues.

The frame's NavigationService has BackStack property which contains a JournalEntry object for each page on the back stack. Unfortunately the JournalEntry object has very few useful public properties. What I really want is to get access to the object that was the frame's Content.

Let's get a sample project started. Create a new WPF Application project called InterrogateNavigation.


We will start by adding some pages that navigate to each other creating a stack of journal entries. Modify MainWindow to look like this.


<Window x:Class="InterrogateNavigationService.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:InterrogateNavigationService"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525"
        Loaded="MainWindow_Loaded">
    <Frame Initialized="Frame_Initialized" NavigationUIVisibility="Hidden">
    </Frame>
</Window>


using System.Windows;
using System.Windows.Controls;

namespace InterrogateNavigationService
{
    public partial class MainWindow : Window
    {
        public Frame theFrame;
        public MainWindow()
        {
            InitializeComponent();
        }

        public void MainWindow_Loaded(object Sender, RoutedEventArgs e)
        {
            theFrame.Navigate(new Page1(theFrame));
        }

        private void Frame_Initialized(object sender, System.EventArgs e)
        {
            theFrame = (Frame)sender;
        }
    }
}

When the application loads it will immediately attempt to navigate to Page1 (which is a user control).


<local:BaseUserControl x:Class="InterrogateNavigationService.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"
      xmlns:local="clr-namespace:InterrogateNavigationService"
      mc:Ignorable="d">
    <local:BaseUserControl.Resources>
        <RoutedCommand x:Key="Page2"/>
        <RoutedCommand x:Key="GoBack"/>
    </local:BaseUserControl.Resources>
    <local:BaseUserControl.CommandBindings>
        <CommandBinding Command="{StaticResource Page2}" Executed="Page2_Executed"/>
        <CommandBinding Command="{StaticResource GoBack}" CanExecute="GoBack_CanExecute" Executed="GoBack_Executed"/>
    </local:BaseUserControl.CommandBindings>
    <StackPanel Orientation="Vertical">
        <TextBlock Text="I am page 1"/>
        <Button Content="Take me to Page 2" Command="{StaticResource Page2}"/>
        <Button Content="Go Back" Command="{StaticResource GoBack}"/>
    </StackPanel>
</local:BaseUserControl>



using System.Windows.Controls;
using System.Windows.Input;

namespace InterrogateNavigationService
{
    public partial class Page1 : BaseUserControl
    {
        public Page1(Frame Parent)
        {
            myParent = Parent;
            myParam = "Page1";
            InitializeComponent();
        }

        private void Page2_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            myParent.Navigate(new Page2(myParent));
        }
    }
}

Page1 uses a base class.


using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Navigation;

namespace InterrogateNavigationService
{
    public class BaseUserControl : UserControl
    {
        public Frame myParent;
        public string myParam;

        protected void GoBack_CanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            e.CanExecute = (myParent.CanGoBack);
        }
        protected void GoBack_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            myParent.GoBack();
        }
    }
}


Page1 can also navigate to Page2 which looks remarkably similar.


<local:BaseUserControl x:Class="InterrogateNavigationService.Page2"
      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:InterrogateNavigationService"
      mc:Ignorable="d">
    <local:BaseUserControl.Resources>
        <RoutedCommand x:Key="Page1"/>
        <RoutedCommand x:Key="GoBack"/>
    </local:BaseUserControl.Resources>
    <local:BaseUserControl.CommandBindings>
        <CommandBinding Command="{StaticResource Page1}" Executed="CommandBinding_Executed"/>
        <CommandBinding Command="{StaticResource GoBack}" CanExecute="GoBack_CanExecute" Executed="GoBack_Executed"/>
    </local:BaseUserControl.CommandBindings>
    <StackPanel>
        <TextBlock Text="I am page 2"/>
        <Button Content="Take me to page 1" Command="{StaticResource Page1}"/>
        <Button Content="Go Back" Command="{StaticResource GoBack}"/>
    </StackPanel>
</local:BaseUserControl>




using System.Windows.Controls;
using System.Windows.Input;

namespace InterrogateNavigationService
{
    public partial class Page2 : BaseUserControl
    {
        public Page2(Frame Parent)
        {
            myParent = Parent;
            myParam = "Page2";
            InitializeComponent();
        }

        private void CommandBinding_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            myParent.Navigate(new Page1(myParent));
        }
    }
}

Page 2 can navigate forward to Page1 creating a stack of pages. Our task is to be able to examine the contents of that stack to see if the page currently being loaded is already in the stack. My original requirement is to force the new page into view only mode if it is already on the stack. This is to prevent concurrency issues within the same instance of the application. This example will update the MainWindow's title bar.

In the BaseUserControl class we will execute some code in the Loaded event handler. The modified constructor and event handler look like this.

public BaseUserControl()
{
       Loaded += CheckForDuplicate;
}

void CheckForDuplicate(object sender, RoutedEventArgs e)
{
    Application.Current.MainWindow.Title = "";
    if (myParent.BackStack != null)
    {
        foreach (JournalEntry je in myParent.BackStack)
        {
            PropertyInfo piKeepAliveRoot = je.GetType().GetProperty("KeepAliveRoot", BindingFlags.NonPublic | BindingFlags.Instance);
            BaseUserControl uc = (BaseUserControl)piKeepAliveRoot.GetValue(je, null);
            if (uc.GetType() == this.GetType())
                        Application.Current.MainWindow.Title = "Duplicate of " + uc.myParam;
        }
    }
}

We are looking at the Frame's BackStack. I pass around a reference to the frame in myParent to simplify the example. The BackStack is an Enumerable of JournalEntries or derived classes. The JournalEntry has a Friend property called KeepAliveRoot. We can access that property using reflection. The property stores the Content of the frame before we navigated away from it. All we have to do is cast it (because we know it inherits BaseUserControl) and then we can examine it.

After navigating from Page1 to Page2 then on to Page1


This is yet another argument for using base classes for all your pages.

No comments:

Post a Comment