Tuesday, February 14, 2017

Using the NavigationService

This entry targets Framework 4.0

My purchasing application has a main page that contains a frame. Users can navigate pages in that frame. I provide a back button that allows them to navigate back. I want to put a tooltip on that button that lets them know what they are going to navigate back to. This is more difficult than I had expected.

I solved the problem by populating and reading the Name property of the Navigation.JournalEntry. The Name property is automatically populated by the Framework as you navigate according to the following rules.

  • The attached Name attribute.
  • Title.
  • WindowTitle and the uniform resource identifier (URI) for the current page
  • The uniform resource identifier (URI) for the current page
You can populate the Name or Title attribute in XAML like this...
<Page
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    JournalEntry.Name="JournalEntry Name"
    Title="Title"
    >
  <!--Page Content-->
</Page>

But if you want to make the tooltip dynamic it is easiest to bind to a property. Lets walk through the process of doing this. We will create a project in Visual Studio 2015 that has a main page, an "EnterName" page where the user enters their name and navigates to a second page called "Hello". The second page will show a back button with a tool tip that shows the title from the prior page.

I will use a modified version of MVVM to build the project.

Start a new WPF application in Visual Basic (File -> New -> Project) and call it BackButtonToolTip.

We will have a MainWindow.xaml and xaml.vb. Replace MainWindow.xaml with this XAML which defines a Title textblock for the application, a back button, and a frame.
<Window x:Class="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"
 mc:Ignorable="d"
 Height="350" Width="525"
 DataContext="{Binding RelativeSource={RelativeSource Self}}">
    <Window.CommandBindings>
        <CommandBinding Command="PreviousPage" CanExecute="PreviousPage_CanExecute" Executed="PreviousPage_Executed"></CommandBinding>
    </Window.CommandBindings>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"></RowDefinition>
            <RowDefinition Height="*"></RowDefinition>
        </Grid.RowDefinitions>
        <StackPanel Orientation="Horizontal" Grid.Row="0">
            <TextBlock Margin="10,0,10,0" FontSize="20" VerticalAlignment="Center" Text="{Binding MyTitle}"/>
            <Button Name="PreviousButton" Command="PreviousPage" VerticalAlignment="Center" Cursor="Hand" ToolTip="{Binding Path=PreviousButtonToolTip}">
                <Button.Template>
                    <ControlTemplate>
                        <TextBlock FontFamily="Webdings" Text="3" FontSize="24">
                            <TextBlock.RenderTransform>
                                <ScaleTransform ScaleX="1.5"/>
                            </TextBlock.RenderTransform>
                        </TextBlock>
                    </ControlTemplate>
                </Button.Template>
                <Button.Style>
                    <Style TargetType="Button">
                        <Setter Property="Visibility" Value="Visible"></Setter>
                        <Style.Triggers>
                            <Trigger Property="IsEnabled" Value="false">
                                <Setter Property="Visibility" Value="Collapsed"></Setter>
                            </Trigger>
                        </Style.Triggers>
                    </Style>
                </Button.Style>
            </Button>
        </StackPanel>
        <Frame Name="PageFrame" Grid.Row="1" NavigationUIVisibility="Hidden"></Frame>
    </Grid>
</Window>

Now we need to put a couple of dummy event handlers in our code behind to make it look like this...
Class MainWindow
    Private Sub PreviousPage_CanExecute(sender As System.Object, e As System.Windows.Input.CanExecuteRoutedEventArgs)
    End Sub

    Private Sub PreviousPage_Executed(sender As System.Object, e As System.Windows.Input.ExecutedRoutedEventArgs)
    End Sub
End Class

When we run the application we see nothing, which is what we expect at this point. Let's add our first child page. Right-click on the project name in the solution explorer and select "Add Page". Then call the new page "EnterName".

Now we have an EnterName we can reference it in the Frame tag of the MainWindow. Change it to look like this...

<Frame Name="PageFrame" Grid.Row="1" NavigationUIVisibility="Hidden" Source="EnterName.xaml"></Frame>

The EnterName page will have a prompt, a textbox, and a navigation button. I altered the background so you can see the navigation more clearly. Note the use of "KeepAlive". You need this to persist your page's properties when you navigate back to it. When the project is complete, check out the effect of setting this false. Note the line Title="{Binding PageTitle}". By modifying the PageTitle property we can control the Name property of the NavigationJournalEntry object that defines this page in the Frame's BackStack, because it will be populated from the Title property of this page. Don't worry, it all becomes clearer later.

Here is your XAML for EnterName.xaml...
<Page x:Class="EnterName"
      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:BackButtonToolTip"
      mc:Ignorable="d" 
      d:DesignHeight="300" d:DesignWidth="300"
      DataContext="{Binding RelativeSource={RelativeSource self}}"
      Background="AliceBlue"
      KeepAlive="true"
      Title="{Binding PageTitle}">
    <Page.CommandBindings>
        <CommandBinding Command="NextPage" CanExecute="NextPage_CanExecute" Executed="NextPage_Executed"></CommandBinding>
    </Page.CommandBindings>
    <Grid Height="30">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="auto"></ColumnDefinition>
            <ColumnDefinition Width="80"></ColumnDefinition>
            <ColumnDefinition Width="auto"></ColumnDefinition>
        </Grid.ColumnDefinitions>
        <TextBlock Grid.Column="0" Text="Enter Name:" FontSize="18"/>
        <TextBox Grid.Column="1" Text="{Binding UsersName, UpdateSourceTrigger=PropertyChanged}"></TextBox>
        <Button Grid.Column="2" Command="NextPage" Content="Navigate"/>

    </Grid>
</Page>

We need to create two more dummy event handlers in EnterName.xaml.vb.
Class EnterName
    Private Sub NextPage_CanExecute(sender As Object, e As CanExecuteRoutedEventArgs)
    End Sub

    Private Sub NextPage_Executed(sender As Object, e As ExecutedRoutedEventArgs)
    End Sub
End Class

Now when we run the application we can see something. Not much, but something.

Now we can build the backing store for the textbox, add the INotifyPropertyChanged code, and wire up the [Navigate] button. Change the EnterName.xaml.vb to look like this...

Imports System.ComponentModel
Class EnterName
    Implements INotifyPropertyChanged

    Private _UsersName As String = ""

    Public Property UsersName As String
        Get
            Return _UsersName
        End Get
        Set(value As String)
            If _UsersName <> value Then
                _UsersName = value
                NotifyPropertyChanged("UsersName")
                PageTitle = "Back to 'Enter Users Name (" & _UsersName & ")'"
            End If
        End Set
    End Property

    Private _PageTitle As String = ""

    Public Property PageTitle As String
        Get
            Return _PageTitle
        End Get
        Set(value As String)
            If _PageTitle <> value Then
                _PageTitle = value
                NotifyPropertyChanged("PageTitle")
            End If
        End Set
    End Property

    Private Sub NextPage_CanExecute(sender As Object, e As CanExecuteRoutedEventArgs)
        e.CanExecute = (UsersName.Length > 0)
    End Sub

    Private Sub NextPage_Executed(sender As Object, e As ExecutedRoutedEventArgs)
        Dim Frame As Frame = DirectCast(Application.Current.MainWindow.FindName("PageFrame"), Frame)
        Frame.Navigate(New Hello(UsersName))
    End Sub

    Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged

    Private Sub NotifyPropertyChanged(PropertyName As String)
        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(PropertyName))
    End Sub

    Private Sub EnterName_Loaded(sender As Object, e As RoutedEventArgs) Handles Me.Loaded
        DirectCast(Application.Current.MainWindow, MainWindow).MyTitle = "Get Name"
    End Sub
End Class

The NextPage_CanExecute event handler has been modified to only allow navigation if the user has entered something in the UsersName textbox. 

The NextPage_Executed event handler has been modified to navigate to a different page. The act of navigation puts the current page on the Frame's BackStack where it can be interrogated later.

We add the standard get/set handlers for our two properties but we also modify the PageTitle property whenever the user modifies the UsersName property.

At this point we have some errors because we have not yet defined our Hello page or the MyTitle property on the MainWindow. Don't panic.

The EnterName_Loaded event handler has been added. It simply reaches to the application's main window and updates a property. We need to enhance MainPage to define and use that property. Modify MainWindow.xaml.vb to look like this...

Imports System.ComponentModel

Class MainWindow
    Implements INotifyPropertyChanged

    Private _MyTitle As String = ""
    Public Property MyTitle As String
        Get
            Return _MyTitle
        End Get
        Set(value As String)
            If _MyTitle <> value Then
                _MyTitle = value
                NotifyPropertyChanged("MyTitle")
            End If
        End Set
    End Property

    Private _PreviousButtonToolTip As String = ""
    Public Property PreviousButtonToolTip As String
        Get
            Return _PreviousButtonToolTip
        End Get
        Set(value As String)
            If _PreviousButtonToolTip <> value Then
                _PreviousButtonToolTip = value
                NotifyPropertyChanged("PreviousButtonToolTip")
            End If
        End Set
    End Property

    Private Sub PreviousPage_CanExecute(sender As System.Object, e As System.Windows.Input.CanExecuteRoutedEventArgs)
    End Sub

    Private Sub PreviousPage_Executed(sender As System.Object, e As System.Windows.Input.ExecutedRoutedEventArgs)
    End Sub

    Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged

    Private Sub NotifyPropertyChanged(PropertyName As String)
        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(PropertyName))
    End Sub
End Class

Before we can continue we need to write the Hello page that we will navigate to (you need at least two pages to demonstrate navigation).

Add another page and call it "Hello". It will display the user name entered in the EnterName page. The XAML looks like this...
<Page x:Class="Hello"
      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:BackButtonToolTip"
      mc:Ignorable="d" 
      d:DesignHeight="300" d:DesignWidth="300"
      DataContext="{Binding RelativeSource={RelativeSource self}}"
      Background="AntiqueWhite">
    <StackPanel Orientation="Horizontal">
        <TextBlock Text="Hello "/>
        <TextBlock Text="{Binding UsersName}"></TextBlock>
    </StackPanel>
</Page>

The code behind is fairly trivial too...
Imports System.ComponentModel
Class Hello
    Implements INotifyPropertyChanged

    Private _UsersName As String = ""
    Public Property UsersName As String
        Get
            Return _UsersName
        End Get
        Set(value As String)
            If _UsersName <> value Then
                _UsersName = value
                NotifyPropertyChanged("UsersName")
            End If
        End Set
    End Property

    Public Sub New(UsersName As String)
        InitializeComponent()
        Me.UsersName = UsersName
    End Sub

    Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged

    Private Sub NotifyPropertyChanged(PropertyName As String)
        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(PropertyName))
    End Sub

    Private Sub Hello_Loaded(sender As Object, e As RoutedEventArgs) Handles Me.Loaded
        DirectCast(Application.Current.MainWindow, MainWindow).MyTitle = "Hello"
    End Sub
End Class

Note the Hello_Loaded event handler reaches through to the parent page and updates the title property to "Hello". As this is something every page in our application would do, I would refactor it into a function, but I'll let you do that. The Hello page also has a different background color.

If we run the application now we will be able to enter a name and navigate to the "Hello" page.



The back button is still not visible because it's visibility is bound to its enabled state and we haven't told it when can be executed. Let's do that now. Modify the PreviousPage event handlers in MainWindow to look like this...

    Private Sub PreviousPage_CanExecute(sender As System.Object, e As System.Windows.Input.CanExecuteRoutedEventArgs)
        If PageFrame IsNot Nothing AndAlso PageFrame.NavigationService.CanGoBack() Then
            e.CanExecute = True
            PreviousButtonToolTip = PageFrame.BackStack.Cast(Of Navigation.JournalEntry).FirstOrDefault.Name
        End If
    End Sub

    Private Sub PreviousPage_Executed(sender As System.Object, e As System.Windows.Input.ExecutedRoutedEventArgs)
        If PageFrame.NavigationService.CanGoBack Then
            PageFrame.NavigationService.GoBack()
        End If
    End Sub

What we did here is to enable the back button (and consequently make it visible) whenever the frame has a page to go back to. We also find the first element of the BackStack and pull the name from it to assign to the tooltip. When the back button is clicked we execute GoBack.

Now we can see the entire functionality of the solution.




No comments:

Post a Comment