Partager via


Structured Navigation Overview

In WPF, content that can be navigated is typically constructed using pages (Page) and hyperlinks (Hyperlink), and is navigated using uniform resource identifiers (URIs) (Pack URIs in Windows Presentation Foundation). These elements support a basic navigation topology that is unstructured; when one page navigates to a second page, there are no mechanisms that allow the first page to detect when the user has finished with second page. In part, this is because Web applications naturally support a stateless navigation. As this topic will demonstrate, this makes it difficult to create task-based user interfaces (UIs), where one page "calls" another page to perform task and, possibly, get data "returned" from the called page.

Instead, a structured style of navigation is required, one in which one page calls another page and can both detect and handle when the second page returns. For this purpose, WPF implements page functions, which this topic provides an introduction to.

This topic contains the following sections.

  • Structured Navigation
  • Pseudo-Structured Navigation with Page
  • Structured Navigation with PageFunction
  • Related Topics

Structured Navigation

Consider the following:

<Page xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation" WindowTitle="Page1">
  <Hyperlink NavigateUri="Page2.xaml">Navigate to Page 2</Hyperlink>
</Page>
<Page xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation" WindowTitle="Page2">
  <Hyperlink NavigateUri="Page1.xaml">Navigate to Page 1</Hyperlink>
</Page>

In this example, two pages are related only by hyperlinks; neither page needs to pass data to the other page and, likewise, neither page needs data returned from the other page. As is common for Web applications, there are scenarios when two or more pages are used together to perform a single logical task, such as ordering a book. What Web applications lack, however, is the native ability to control the flow of navigation through the pages of a task. This means there is no easy way to:

  • Detect when a task completes.

  • Detect how a task completes.

  • Pass data between pages in a consistent, predictable, and dependable way.

  • Prevent users from navigating back to the middle of a task and re-processing it, even if it already completed.

For standalone applications, tasks are typically processed using dialog boxes that naturally support a structured call/return style of programming. Web application developers, however, are forced to create their own call/return infrastructure. For a non-trivial task, this involves writing code to do the following:

  • Start a task by navigating to the first task page, optionally passing initial task state.

  • When the task completes:

    • Returning a task result to the calling page, to allow the calling page to determine how the task completed.

    • If the task completed successfully, return the data that was collected by the task.

    • Removing task pages from navigation history when a task completes; this is important when each instance of one type of task is isolated from all other instances of that task, as is often the case for tasks that operate over data.

Essentially, these mechanisms combine to create a control flow layer over an unstructured foundation.

Pseudo-Structured Navigation with Page

It is possible to build a structured navigation implementation using Page and Hyperlink, as this topic shows.

Consider a calling page that calls a task that contains only one page. The calling page is shown here:

<Page 
    x:Class="CSharp.CallingPage"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    WindowTitle="Calling Page" WindowWidth="250" WindowHeight="150">
</Page>
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Navigation;
namespace CSharp
{
    public partial class CallingPage : Page
    {
        public CallingPage()
        {
            InitializeComponent();
        }
    }
}

The task page is shown here:

<Page 
    x:Class="TaskPage"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    WindowTitle="Task Page"  WindowWidth="250" WindowHeight="150">
  
  <Grid Margin="10">
    
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="Auto" />
      <ColumnDefinition />
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
      <RowDefinition Height="Auto" />
      <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>

    <!-- Task data -->
    <Label Grid.Column="0" Grid.Row="0">DataItem1:</Label>
    <TextBox Grid.Column="1" Grid.Row="0" Name="dataItem1TextBox"></TextBox>

    <!-- Accept/Cancel buttons -->
    <StackPanel Grid.Column="1" Grid.Row="1" Margin="0,10,0,0"  HorizontalAlignment="Right" Orientation="Horizontal">
      <Button Name="okButton" Click="okButton_Click" IsDefault="True" Width="50" Height="25">OK</Button>
      <Button Name="cancelButton" Click="cancelButton_Click" IsCancel="True" Width="50" Height="25">Cancel</Button>
    </StackPanel>
  </Grid>
</Page>
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Navigation;
public partial class TaskPage : Page
{
    public TaskPage()
    {
        InitializeComponent();
    }
}

The calling page will use the task page to collect a single piece of data of type String:

Passing Data from a Calling Page to a Task

The first step in a structured navigation is to navigate from the calling page to the first (and possibly only) page of the task. If the calling page doesn't need to pass data to the first task page, the hyperlink can be built declaratively:

<Page x:Class="CSharp.CallingPage" ... >
    <!-- Task page hyperlink -->
    <Hyperlink NavigateUri="TaskPage.xaml">Start Task</Hyperlink>
</Page>

If initial data needs to be passed to the task page, a constructor overload that accepts the data can be added to the task page:

<Page x:Class="CSharp.CallingPage" ... >
    <!-- Task page hyperlink -->
    <Hyperlink Name="startTaskHyperlink" Click="startTaskHyperlink_Click">Start Task</Hyperlink>
</Page>
public partial class CallingPage : Page
{
    ...
    void startTaskHyperlink_Click(object sender, RoutedEventArgs e)
    {
        // Instantiate and navigate to task page
        TaskPage taskPage = new TaskPage("Initial Data Item Value");
        this.NavigationService.Navigate(taskPage);
    }
}
public partial class TaskPage : Page
{
    public TaskPage(string initialDataItem1Value)
    {
        InitializeComponent();
        // Set initial value
        this.dataItem1TextBox.Text = initialDataItem1Value;
    }
}

Once the calling page navigates to the task page, optionally passing data, the task page becomes responsible for capturing data from the user, determining whether a user accepts or cancels the task, and returning the result along with the captured data back to the calling page.

Returning a Task Result

The result of a task is determined by how a task is completed by the user by either:

  • Entering data and accepting the task, typically by pressing either an OK or Finish button.

  • Canceling the task, typically by pressing a Cancel button.

The calling page needs to know how a task completed in order to determine whether it should actually process the returned task data. Consequently, the task page needs to return this information. The trick here is that, no matter how the calling page is navigated back to, there is no mechanism in place that lets it know it is being navigated to from the task page that it navigated to earlier. Furthermore, it may not even be the same instance of the calling page that navigated to the task page; by default, a Page is instantiated each time it is navigated to, whether by calling Navigate or by using navigation history. The simplest way to return the task result is to use application-scope properties, like so:

public partial class TaskPage : Page
{
    ...
    void okButton_Click(object sender, RoutedEventArgs e)
    {
        // Accept task when Ok button is clicked
        TaskPageReturn(true);
    }

    void cancelButton_Click(object sender, RoutedEventArgs e)
    {
        // Cancel task
        TaskPageReturn(false);
    }

    void TaskPageReturn(bool taskResult)
    {
        Application.Current.Properties["TaskResult"] = taskResult;

        // Return to calling page
        this.NavigationService.GoBack();
    }
}

Here, the code sets the application-scope TaskResult property and navigates back to the calling page, which retrieves the task result from the application-scope property:

<Page ... Loaded="callingPage_Loaded">
  ...
</Page>
public partial class CallingPage : Page
{
    ...
    void callingPage_Loaded(object sender, RoutedEventArgs e)
    {
        // If a task happened, get task result
        if (Application.Current.Properties["TaskResult"] == null) return;
        bool taskResult = (bool)Application.Current.Properties["TaskResult"];

        // Remove result and data
        Application.Current.Properties["TaskResult"] = null;
    }
    ...
}

Since the calling page is instantiated anew, the calling page's constructor does the work of detecting if the navigation was initiated by the task page. If so, it retrieves the task result and removes it from the application-scope variable to signify the task has completed and the results have been processed.

Returning Data from a Task to a Calling Page

If the user accepts a task, the task needs to return the data that it collected to the calling page as well as the task result. Just like the task result, this can be done using an application-scope property:

public partial class TaskPage : Page
{
    ...
    void TaskPageReturn(bool taskResult)
    {
        Application.Current.Properties["TaskResult"] = taskResult;
        Application.Current.Properties["TaskData"] = this.dataItem1TextBox.Text;

        // Return to calling page
        this.NavigationService.Navigate(new Uri("CallingPage.xaml", UriKind.Relative));
    }
}
public partial class CallingPage : Page
{
    ...
    void callingPage_Loaded(object sender, RoutedEventArgs e)
    {
        // If a task happened, get task result
        if (Application.Current.Properties["TaskResult"] == null) return;
        bool taskResult = (bool)Application.Current.Properties["TaskResult"];

        if (!taskResult) return;

        // If a task happened, display task data
        string taskData = (string)Application.Current.Properties["TaskData"];
        if (taskData == null) return;

        // "End" the task be removing state associated with 
        // its existence
        Application.Current.Properties["TaskResult"] = null;
        Application.Current.Properties["TaskData"] = null;
    }
}

As with the task result, the task data is removed from application-state once processed, to signify the end of the task.

Removing Task Pages when a Task Completes

As you've seen, the end of the task occurs when the calling page sets the application-scope task result and task data properties to null. However, the task page is still in navigation history, as shown in the following figure:

Consequently, the task page needs to be removed, to keep this instance of the task isolated from other instances. However, this is not easily achieved when using Page.

Issues

It is possible to create implemented structured navigation using Page and Hyperlink, with the help of application-scope variables (Properties) and NavigationService. The technique shown here is the simplest possible, but does not support removal of task pages from navigation history. And, while there are several more ways to develop a structured navigation framework, any of these solutions will not be easily reusable; not only do you need to create the specific UI and data for your tasks, you need to recreate the structured navigation infrastructure each time. You could create a reusable navigation infrastructure by leveraging NavigationService. However, WPF exposes the PageFunction class that, in conjunction with NavigationService, provides the desired infrastructure.

Structured Navigation with PageFunction

PageFunction is the cornerstone of a structured navigation support in WPF. Essentially, page functions enable a style of navigation that is analogous to functions in procedural languages, which typically involves the following:

  • Calling a function.

  • Passing parameters to the function.

  • Performing processing from the function.

  • Calling other functions from the function.

  • Return results from the function to the calling code.

  • Cleaning up.

This topic looks at how to replicate this model by using PageFunction to build a task using structured navigation.

Creating a Page Function

Consider the following page, which will call a task that contains one page function:

<Page 
    x:Class="CSharp.CallingPage"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    WindowTitle="Calling Page" WindowWidth="250" WindowHeight="150">
</Page>
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Navigation;
namespace CSharp
{
    public partial class CallingPage : Page
    {
        public CallingPage()
        {
            InitializeComponent();
        }
    }
}

The task page function is shown here:

<PageFunction
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:sys="clr-namespace:System;assembly=mscorlib" 
    x:Class="CSharp.TaskPageFunction"
    x:TypeArguments="sys:String"
    Title="Task Page Function" WindowWidth="250" WindowHeight="150">
  <Grid Margin="10">
    
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="Auto" />
      <ColumnDefinition />
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
      <RowDefinition Height="Auto" />
      <RowDefinition />
    </Grid.RowDefinitions>

    <!-- Task data -->
    <Label Grid.Column="0" Grid.Row="0">DataItem1:</Label>
    <TextBox Grid.Column="1" Grid.Row="0" Name="dataItem1TextBox"></TextBox>

    <!-- Accept/Cancel buttons -->
    <TextBlock Grid.Column="1" Grid.Row="1" HorizontalAlignment="Right">
      <Button Name="okButton" Click="okButton_Click" IsDefault="True" Width="50" Height="25">OK</Button>
      <Button Name="cancelButton" Click="cancelButton_Click" IsCancel="True" Width="50" Height="25">Cancel</Button>
    </TextBlock>
    
  </Grid>
</PageFunction>
public partial class TaskPageFunction : PageFunction<String>
{
    public TaskPageFunction()
    {
        InitializeComponent();
    }
}

The declaration of a PageFunction is similar to the declaration of a page. However, because PageFunction is a generic class, it requires the type identifier as well, which defines the type of the value that the PageFunction will return. In this case, the PageFunction is defined to return the String type by using the x:TypeArguments attribute in XAML. This is shown below:

<PageFunction ...
    xmlns:sys="clr-namespace:System;assembly=mscorlib"
    x:TypeArguments="sys:String">
    ...
</PageFunction>
public partial class TaskPageFunction : PageFunction<String>
{ ... }

Note that you could also use a custom type from either the local assembly or a referenced assembly:

<PageFunction ... 
    xmlns:local="clr-namespace:LocalNamespace"
    x:TypeArguments="local:CustomType">
    ...
</PageFunction>

As you'll see, the type that is identified with the declaration of a PageFunction plays an important role in returning data from a page function to the calling page.

Passing Data from a Calling Page to a Task

Passing data from a calling page to a task page function is the same as it is when using pages:

<Page x:Class="CSharp.CallingPage" ... >
    <!-- Task page hyperlink -->
    <Hyperlink NavigateUri="TaskPageFunction.xaml">Start Task</Hyperlink>
</Page>

If initial data needs to be passed to the task page, a constructor overload that accepts the data can be added to the task page:

<Page x:Class="CSharp.CallingPage" ... >
    <!-- Task page hyperlink -->
    <Hyperlink Name="startTaskHyperlink" Click="startTaskHyperlink_Click">Start Task</Hyperlink>
</Page>
public partial class CallingPage : Page
{
    ...
    void startTaskHyperlink_Click(object sender, RoutedEventArgs e)
    {
        // Instantiate task page function
        TaskPageFunction taskPageFunction = new TaskPageFunction("Initial Data Item Value");
        this.NavigationService.Navigate(taskPageFunction);
    }
}
public partial class TaskPageFunction : PageFunction<String>
{
    public TaskPageFunction(string initialDataItem1Value)
    {
        InitializeComponent();
        // Set initial value
        this.dataItem1TextBox.Text = initialDataItem1Value;
    }
}

Calling a Task

Calling a task that is composed from page functions is also similar to calling a task that is composed from pages; you navigate to it using the NavigationService:

public partial class CallingPage : Page
{
    ...
    void startTaskHyperlink_Click(object sender, RoutedEventArgs e)
    {
        // Instantiate and navigate to task page function
        TaskPageFunction taskPageFunction = new TaskPageFunction("Initial Data Item Value");
        this.NavigationService.Navigate(taskPageFunction);
    }
}

Returning Task Result and Task Data from a Task to a Calling Page

With structured navigation using Page, task result and data were returned from the task to the calling page using application-scope properties. However, data of any kind is returned from a page function by calling the OnReturn method, like so:

public partial class TaskPageFunction : PageFunction<String>
{
    ...
    void okButton_Click(object sender, RoutedEventArgs e)
    {
        // Accept task when Ok button is clicked
        OnReturn(new ReturnEventArgs<string>(this.dataItem1TextBox.Text));
    }

    void cancelButton_Click(object sender, RoutedEventArgs e)
    {
        // Cancel task
        OnReturn(null);
    }
}

OnReturn is a protected virtual method that you call to return your data to the calling page. Your data needs to be packaged in an instance of the generic ReturnEventArgs type, whose type identifier specifies the type of value that Result returns. In this way, when you declare a PageFunction to operate over a particular type, you are stating that a PageFunction will return an object of that type.

Since it is only a single object, and since both task result and data are required by the calling page, the PageFunction is configured to return the String type, which, as you saw earlier, stores both task result and task data values.

When OnReturn is called, the calling page needs some way of receiving the return value of the PageFunction. For this reason, PageFunction implements the Return event for calling pages to handle; When OnReturn is called, Return is raised, so the calling page can register with Return to receive the notification:

public partial class CallingPage : Page
{
    ...
    void startTaskHyperlink_Click(object sender, RoutedEventArgs e)
    {
        // Instantiate and navigate to task page function
        TaskPageFunction taskPageFunction = new TaskPageFunction("Initial Data Item Value");
        taskPageFunction.Return += taskPageFunction_Return;
        this.NavigationService.Navigate(taskPageFunction);
    }

    void taskPageFunction_Return(object sender, ReturnEventArgs<string> e)
    {
        // Display task result and data
        this.taskResultsTextBlock.Visibility = Visibility.Visible;

        // Display task result
        this.taskResultsTextBlock.Text = (e != null ? "Accepted" : "Canceled");
        if (e == null) return;

        // If a task happened, display task data
        this.taskResultsTextBlock.Text += "\n" + e.Result;
    }
}

Removing Task Pages when a Task Completes

Until a page function returns, users can navigate between the calling page and a task page function without the page function being removed from navigation history. When a page function returns, it’s the value of the RemoveFromJournal property that dictates whether the page function is removed from navigation history. By default, a page function is automatically removed when OnReturn is called because RemoveFromJournal is set to true. To keep a page function in navigation history after OnReturn is called, set RemoveFromJournal to false.

See Also

Reference

PageFunction
NavigationService

Concepts

Navigation Topologies Overview

Other Resources

Structured Navigation Sample