Monday, October 30, 2017

Odd behavior in NavigationService

There appears to be a bug in the Navigation service. When you execute the GoBack method, the frame's Source is updated, but the binding source is not.

Start a new WPF project using C# targeting any framework. Call the project GoBack.


The MainWindow will contain some buttons, some text blocks, and a frame. The buttons allow us to navigate the frame and the text blocks will monitor some properties. The xaml looks like this.

<Window x:Class="GoBack.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:GoBack"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <RoutedCommand x:Key="LoadCommand"/>
        <RoutedCommand x:Key="BackCommand"/>
    </Window.Resources>
    <Window.CommandBindings>
        <CommandBinding Command="{StaticResource LoadCommand}" Executed="Load_Executed"/>
        <CommandBinding Command="{StaticResource BackCommand}" CanExecute="Back_CanExecute" Executed="Back_Executed"/>
    </Window.CommandBindings>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <StackPanel Grid.Row="0" Orientation="Horizontal">
            <Button Content="Load Page 2" Command="{StaticResource LoadCommand}" CommandParameter="Page2.xaml"/>
            <Button Content="Go Backward" Command="{StaticResource BackCommand}"/>
        </StackPanel>
        <TextBlock Grid.Row="1" Text="{Binding FrameSource, StringFormat='FrameSource={0}'}"/>
        <TextBlock Grid.Row="2" Text="{Binding ElementName=MainFrame, Path=Source, StringFormat ='MainFrame.Source={0}'}"/>
        <Frame Name="MainFrame" Grid.Row="3" Source="{Binding FrameSource}" Background="AliceBlue"/>
    </Grid>
</Window>

The code behind implements the commands and defines a property that is bound to the Frame's Source property.

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

namespace GoBack
{
    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        private string _FrameSource = "Page1.xaml";
        public string FrameSource
        {
            get { return _FrameSource; }
            set
            {
                if (_FrameSource != value)
                {
                    _FrameSource = value;
                    NotifyPropertyChanged("FrameSource");
                }
            }
        }
        public MainWindow()
        {
            InitializeComponent();
        }

        public event PropertyChangedEventHandler PropertyChanged;
        public void NotifyPropertyChanged(string PropertyName)
        {
            if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(PropertyName));
        }

        private void Load_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            FrameSource = e.Parameter.ToString();
        }

        private void Back_CanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            if (MainFrame != null && MainFrame.NavigationService != null)
                e.CanExecute = (MainFrame.NavigationService.CanGoBack);
        }

        private void Back_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            MainFrame.NavigationService.GoBack();
        }
    }
}

Note we move forward by setting the Frame's Source property via the binding but we navigate back by using the NavigationService's GoBack method.

Add a couple of trivial Page1.xaml and Page2.xaml pages. They have no code behind and the XAML for Page1 looks like this. You can figure out Page2 on your own.

<Page x:Class="GoBack.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:GoBack">
    <Grid>
        <TextBlock Text="Page 1"/>
    </Grid>
</Page>

When you run the program, note the Frame's Source and its bound property both point to Page 1.



Now click [Load Page 2] and note both the Frame's Source and its bound property both point to Page 2. Navigating by setting the bound property keeps them both in sync.



Now click [Go Backward]. Although you move to the correct page, note the bound property is still pointing to Page 2. If you navigate using the NavigationService the bound property does not get updated although the Frame's Source property is updated correctly.



Now change the Frame's XAML definition to explicitly set the binding mode to TwoWay like this.

<Frame Name="MainFrame" Grid.Row="3" Source="{Binding FrameSource, Mode=TwoWay}" Background="AliceBlue"/>

Run the program again and perform the same steps. Now the bound property is updated correctly. Clearly the default Mode for a Frame's Source is OneWay. I have no idea why that would be.



No comments:

Post a Comment