Sunday, October 29, 2017

Frames within frames

In WPF the Frame class contains a navigator class that tracks past history like a web browser. I recently built a page that is contained in a frame and has it's own frame. The navigator was not behaving as I expected it to. As I navigated within the inner frame, the outer frame was handling the navigating. I browsed the Microsoft documentation and found the reason in the Note at the bottom of this page.

Here's a clear(ish) example. It's written in C# with the viewmodels in the code behind so the demo can better focus on the issue.

Start a new WPF project in C# and call it FrameInFrame.


MainWindow simply contains a frame with its content Uri set to MainPage.xaml. It has no code behind.

<Window x:Class="FrameInFrame.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:FrameInFrame"
        Title="Main Window">
    <Frame Source="MainPage.xaml"/>
</Window>

Add a new WPF page called MainPage. This will contain a button and another frame. We will initially load the frame from one page, and when the user clicks the button we will load it from a different page. The XAML looks like this.

<Page x:Class="FrameInFrame.MainPage"
      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:FrameInFrame"
      DataContext="{Binding RelativeSource={RelativeSource Self}}">
    <Page.Resources>
        <RoutedCommand x:Key="LoadCommand"/>
    </Page.Resources>
    <Page.CommandBindings>
        <CommandBinding Command="{StaticResource LoadCommand}" Executed="LoadCommand_Executed"/>
    </Page.CommandBindings>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <Button Grid.Column="0" Content="Load Subpage 2" Command="{StaticResource LoadCommand}"/>
        <Frame Grid.Column="1" Source="{Binding FrameSource}"/>
    </Grid>
</Page>

The code behind implements INotifyPropertyChanged, provides some properties and handles the command.

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

namespace FrameInFrame
{
    public partial class MainPage : Page, INotifyPropertyChanged
    {
        private string _FrameSource = "SubPage1.xaml";

        public string FrameSource
        {
            get { return _FrameSource; }
            set
            {
                if (_FrameSource != value)
                {
                    _FrameSource = value;
                    NotifyPropertyChanged("FrameSource");
                }
            }
        }

        public MainPage()
        {
            InitializeComponent();
        }

        private void LoadCommand_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            FrameSource = "SubPage2.xaml";
        }

        public event PropertyChangedEventHandler PropertyChanged;
        private void NotifyPropertyChanged(String info)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(info));
            }
        }
    }
}

Lastly add a page called SubPage1 and one called SubPage2. They don't have any code behind. The XAML for SubPage1 is below. I think you can guess what the XAML for SubPage2 looks like.

<Page x:Class="FrameInFrame.SubPage1"
      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:FrameInFrame">
    <TextBlock Text="Sub Page 1"/>
</Page>

Now run the project and click the button. SubPage2 is loaded and the text changes to reflect this. The frame defined in MainWindow now shows navigation buttons. Even though the MainPage frame did the navigation, the MainWindow frame is handling the navigation. This is the default behavior.


Now change the MainPage frame to look like this, and run the program again.

<Frame Grid.Column="1" Source="{Binding FrameSource}" JournalOwnership="OwnsJournal"/>

Now the inner frame uses it's own navigation service and you can navigate the inner frame without affecting the outer frame's navigation service.

No comments:

Post a Comment