Article: Inductive UI’s with WPF Navigation Applications
Navigation applications are an attractive feature of Windows Presentation Foundation that allow you to create task-focused user interfaces in a manner that is very difficult to achieve using Windows Forms. In this article, I’ll discuss some of the benefits the navigation application model provides, then describe the features in WPF that make building them possible. I hope to dispel a few myths and concerns about them in the process.
Inductive User Interfaces
In 2001, Microsoft published a paper called Inductive User Interface Guidelines. If you have not read this paper, I would suggest doing so. It is largely technology agnostic, and I know that many of my colleagues at Readify consider it to be a classic. I’ll attempt to summarize the paper for the purposes of this article, but there is no substitute for reading the original.
Deductive UI’s
Applications like Visual Studio or Microsoft Word are designed for people who have a pretty good idea what they are doing, and how they can do it. Although every application has a learning curve, users use these applications many hours a week, sometimes a lot more, and quickly get used to the UI.
- They know that typing into the big white thing in the middle of the screen is how they compose a document.
- They know that if they type the first letter of the variable, Intellisense will give them the options.
- They know that to debug, they can set breakpoints by hitting F9 with the line they want selected.
These UI’s are described as deductive, that is, the user needs to deduce how to use the application to accomplish their goals. If you didn’t know what a breakpoint was or how to set one, there’s nothing obvious about the UI that says "click here and press F9, I’ll help you debug this broken thing". Instead, users learn to be efficient with these applications via reading manuals, taking training courses, being taught by friends or coworkers, or good, old fashioned practice.
Deductive UI’s can be tricky and time consuming to learn, but this is a worthwhile investment since you will probably use them every day of your life. Like any investment, return on investment is critical: how long does it take to become productive with your UI, and how long will I reap those benefits?
Inductive UI’s
Some applications are used far less frequently than Word or Visual Studio. If you were building a desktop application that is going to help people design custom Birthday Cards using photos from their cameras, it is likely to only be used once every month or two. If Auntie Jean had to spend a day learning to use your application, by the time she was productive the birthday would probably have passed. To top it off, it might be another month until she uses it again, in which time she’s probably forgotten most of what she learned.
In contrast to deductive UI’s, which are geared towards productivity but come with a learning curve, inductive user interfaces are geared for ease of use and accessibility. Your Birthday Card Designer can assume the user is probably trying to make a birthday card, so instead of giving them a blank slate with every power option at their fingertips, why not lead them?
The Microsoft paper offers Microsoft Money 2000 as an example of an inductive user interface. Money is an application most people wouldn’t use every day, and so the UI can afford to sacrifice power-user productivity in order to lower the initial learning curve.
Inductive UI Styles
Most good inductive UI’s are task oriented. A Birthday Card Designer, for example, has only one task: design a birthday card.
Microsoft Money has many more features, each of which is generally presented as some form of task. Instead of an intimidating grid with a list of every payment in and out of the system that you can edit inline, Money simply offers a few common choices as links on the home page: Pay a Bill, Deposit Salary, Setup an Account, and so on.
Designing task-centric applications can be quite easy, especially if your methodology involves documenting use cases or scenarios, since there is often a one-to-one mapping. If you spend time asking "what general things do they use this application for?", you’ll probably arrive at a good list.
Most inductive UI’s materialize in two forms:
- Standalone Wizards, which guide users through a process.
- Navigational applications.
Visual Studio may be described as a very deductive UI, but the various Wizards, such as the Project Upgrade Wizard or Import/Export Settings Wizard are very task-centric, and could be described as inductive (once you figure out how to get to them). Money takes navigation to the extreme, by building the entire application around navigation.
From this point onwards I will concentrate on the second form, but many of the objects I will demonstrate can be used for building standalone Wizards in non-navigation applications too.
Navigation Applications
Navigation applications are applications that are composed of many "pages", and often feel similar to web applications, although not always. A navigation application usually provides some kind of shell in which the pages are hosted, with custom chrome or widgets around it.
The pages of a navigation application can usually be broken down into two categories:
- Standalone, free pages.
- Pages that make up a step in a process.
For example, an online banking application may have a number of standalone pages, such as:
- The Welcome page
- The Account Balance page
- The Help page
These pages are often read-only, or can be navigated to at any time, and don’t really form part of a single "task" that the user performs.
Then there are many tasks the user can perform:
- Applying for a credit card
- Paying a bill
- Requesting to increase their transaction limits
Note that these tasks may comprise subtasks, which can make our navigation model rather complex. Let’s dissect each of these sections and discuss how WPF handles them.
The Shell
The Shell is usually the main window of your application, and it’s where your pages are hosted. In WPF, you have two options:
- If you are building a XAML Browser Application, or XBAP, the shell will normally be the web browser that is hosting your application. It is possible to create your own shell within the browser window, though it may confuse users.
- If you are building a standalone application, you will need to build your own shell.
Building your own shell is as simple as creating a standard WPF Window in a normal WPF application, and using a WPF Frame element. The Frame's job is to declare a section of the window that your pages will be hosted in, and to provide all of the services around navigation. Here is an example:
<Window
x:Class="NavigationSample.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300"
>
<DockPanel>
<Frame x:Name="_mainFrame" />
</DockPanel>
</Window>
From code, you can tell the frame to navigate, like so:
_mainFrame.Navigate(new Page1());
Which just so happens to be a helpful shortcut to:
_mainFrame.NavigationService.Navigate(new Page1());
NavigationService
Navigation comprises many functions: going back, going forward, going to a new page, refreshing a page, and so on. Objects in the WPF Navigation ecosystem, like the Frame class, as well as the Page class which I’ll describe shortly, can access this functionality by a NavigationService property, which is, surprisingly of type NavigationService. For example:
_mainFrame.NavigationService.GoBack(); _mainFrame.NavigationService.GoForward(); _mainFrame.NavigationService.Refresh();
This same object is made available to every page hosted within the frame, so pages can signal that they wish to navigate backwards or forwards. When navigating, the Navigate method will accept just about anything you want:
// Show www.google.com in a web browser control
_mainFrame.NavigationService.Navigate(new Uri("http://www.google.com/"));
// Show Page1.xaml
_mainFrame.NavigationService.Navigate(new Uri("Page1.xaml", UriKind.Relative));
// Show a specific customized instance of Page1.xaml
_mainFrame.NavigationService.Navigate(new Page1());
// Show any old visual element
_mainFrame.NavigationService.Navigate(new Button());
// Show a TextBlock around a string
_mainFrame.NavigationService.Navigate("Hello world");
Frames make their content public through the Content dependency property, which makes it easy to bind to. When navigating to a page, for instance, the Content property will give you a Page object. Showing the title of the current page in your shell becomes very trivial:
<Window
x:Class="NavigationSample.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title=”{Binding Path=Content.Title, ElementName=_mainFrame}”
Height=”300″ Width=”300″
>
<DockPanel>
<Frame x:Name=”_mainFrame” />
</DockPanel>
</Window>
The NavigationService also provides a number of events you can subscribe to, if you want to control the navigation process:
Navigating, when the frame is about to navigate. Set Cancel to true to stop.Navigated, when navigation has finished but before it is renderedNavigationFailed, when something goes wrongNavigationProgress, when chunks of a remote navigation call are being downloaded. Nice for progress bars.NavigationStopped, when theStopLoadingmethod is called (like clicking "Stop" in IE) or a new Navigate request is made during downloadingLoadCompleted, when the page has been rendered
Customizing the Chrome
The Frame control ships with a standard UI that provides back and forward buttons once you have navigated to a second page. Out of the box, it looks very similar to IE 7.0, although it doesn’t take into account your current Windows theme:
Fortunately, just like every other WPF control, how the frame looks is completely up to you. Just apply a ControlTemplate and use a ContentPresenter to show the content of the page:
<ControlTemplate TargetType="Frame">
<DockPanel Margin="7">
<StackPanel
Margin="7"
Orientation="Horizontal"
DockPanel.Dock="Top"
>
<Button
Content="Avast! Go back!"
Command="{x:Static NavigationCommands.BrowseBack}"
IsEnabled="{TemplateBinding CanGoBack}"
/>
<Button
Content="Forward you dogs!"
Command="{x:Static NavigationCommands.BrowseForward}"
IsEnabled="{TemplateBinding CanGoForward}"
/>
</StackPanel>
<Border
BorderBrush="Green"
Margin="7"
BorderThickness="7"
Padding="7"
CornerRadius="7"
Background="White"
>
<ContentPresenter />
</Border>
</DockPanel>
</ControlTemplate>
In that example, TemplateBindings are used because the Frame (being templated) exposes CanGoBack and CanGoForward properties. The NavigationCommands are a set of static routed UI commands that the frame will automatically intercept - there’s no need for C# event handlers to call NavigationService.GoBack().
The NavigationService and Frame object expose many other properties and events; these are just the tip of the iceberg, and the ones you’ll customize most often when building inductive UI’s with WPF.
Note: WPF includes a class called NavigationWindow, which is essentially a Window which also doubles as a Frame, by implementing most of the same interfaces. Alternatively, you can set the StartupUri in your Application XAML file to point to a page, and WPF will create a NavigationWindow. It sounds useful at first, but most of the time you need more control over the Window, so I’ve never had any need to use this class. I am just pointing it out for the sake of completeness, though your mileage may vary.
Pages
Pages are the cornerstone of navigation applications, and are the primary surface users interact with. Similar to Windows or UserControls, a Page is a XAML markup file with code behind. If you are working with XBAP’s, the Page is the root of the application.
Just like a Window or UserControl, a page can only have one child, which you’ll need to set to a layout panel to do anything fancy. You can do whatever you like in a WPF Page - 3D, animation, rotation; it’s just like any other control.
Using the Navigation Service from a Page
Each Page has a reference to the NavigationService object provided by its container - usually a Frame. Note that the Page and its containing Frame will share the same reference to the NavigationService, so the Page won’t be able to reference it during it’s constructor.
The Page can make use of the NavigationService’s Navigating event to stop users navigating away from the page, by setting the Cancel property on the event arguments. This is useful if the user has unsaved changes on the page, and you want to give them a chance to save them. Don’t worry about unsubscribing from the event either - WPF will detect when you navigate away from the page, and automatically removes all event handlers, to avoid any memory leaks (since the NavigationService object lives a lot longer than any particular page would).
Since the page is hosted within the Frame in the Visual Tree, you can also make use of some of the NavigationCommands routed commands, since the parent Frame will automatically handle them:
<Page
x:Class="NavigationSample.Page2"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Page2"
>
<Grid>
<Button Command="{x:Static NavigationCommands.BrowseBack}">Cancel</Button>
</Grid>
</Page>
In addition, you can of course use the NavigationService to perform any other kind of navigation, such as navigating to a new page or going backwards.
Hyperlinks
One additional feature you can use when building pages is the Hyperlink control, and the automatic navigation it provides. Hyperlink is an Inline, so it has to be used within a control like a TextBlock or FlowDocument paragraph. You can use it in other WPF applications, but automatic navigation only works within navigation applications. Here is an example:
<Page
x:Class="NavigationSample.Page1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Page1"
>
<Grid>
<TextBlock>
<Hyperlink NavigateUri="Page2.xaml">Go to page 2</Hyperlink>
</TextBlock>
</Grid>
</Page>
Passing data between pages
There are two ways you can pass information between pages using WPF. First, I’ll show you the way not to do it, then I’ll show the preferred way to do it
Although it’s not obvious, you can pass query string data to a page, and extract it from the path. For example, your hyperlink could pass a value in the URI:
<TextBlock>
<Hyperlink NavigateUri="Page2.xaml?Message=Hello">Go to page 2</Hyperlink>
</TextBlock>
When the page is loaded, it can extract the parameters via NavigationService.CurrentSource, which returns a Uri object. It can then examine the Uri to pull apart the values. However, I strongly recommend against this approach except in the most dire of circumstances.
A much better approach involves using the overload for NavigationService.Navigate that takes an object for the parameter. You can initialize the object yourself, for example:
Customer selectedCustomer = (Customer)listBox.SelectedItem; this.NavigationService.Navigate(new CustomerDetailsPage(selectedCustomer));
This assumes the page constructor receives a Customer object as a parameter. This allows you to pass much richer information between pages, and without having to parse strings.
Page Lifecycles
Many people have been confused about how long pages live for and how Back/Forward navigation works. The simplest way to demonstrate this is to create a couple of simple pages, Page1 and Page2, which contain hyperlinks to each other. I am going to write the same tracing code in each page:
public Page1()
{
InitializeComponent();
Trace.WriteLine("Page 1 constructed");
}
private void Button_Click(object sender, RoutedEventArgs e)
{
Trace.WriteLine("Page 1 navigating to page 2");
this.NavigationService.Navigate(new Uri("Page2.xaml", UriKind.Relative));
}
private void Page_Loaded(object sender, RoutedEventArgs e)
{
Trace.WriteLine("Page 1 loaded");
}
private void Page_Unloaded(object sender, RoutedEventArgs e)
{
Trace.WriteLine("Page 1 unloaded");
}
~Page1()
{
Trace.WriteLine("Page 1 destroyed");
}
I’ve also handled the Back and Forward buttons myself, so I can trace when they are clicked, and I’ve added a button to force the garbage collector to run. Here’s what happens:
Page 1 navigating to page 2 Page 2 constructed Page 1 unloaded Page 2 loaded Garbage collector runs Page 1 destroyed Clicked Back Page 1 constructed Page 2 unloaded Page 1 loaded Garbage collector runs Page 2 destroyed
So, the navigation system works as you expect - the page is unloaded, and available for the garbage collector. However, it does become more complicated. Suppose your page required some kind of parameter data to be passed to it:
public Page1(DateTime date)
{
InitializeComponent();
Trace.WriteLine("Page 1 constructed " + date.ToString());
}
private void Button_Click(object sender, RoutedEventArgs e)
{
Trace.WriteLine("Page 1 navigating to page 2");
this.NavigationService.Navigate(new Page2(DateTime.Now));
}
When navigating, if you click "Back", WPF can’t possibly know what values to pass to the constructor; therefore it must keep the page alive. Here’s the trace output:
Page 1 navigating to page 2 Page 2 constructed 5/06/2008 12:23:19 PM Page 1 unloaded Page 2 loaded Garbage collector runs Clicked Back Page 2 unloaded Page 1 loaded
Notice that although the pages are loaded and unloaded many times, it is the same page instance. This means that:
- If you navigate using a URI, WPF will create the page by invoking the constructor each time. The navigation history keeps the URI, not the object.
- If you navigate passing an object directly, WPF will keep the object alive.
This gives you a few things to consider:
- Over time, that can amount to some serious memory usage.
- The
LoadedandUnloadedevents are called each time the page is shown or disappears, so use them as chances to clear all the data you can in order to minimize memory. - The URI navigation mode explained above can be useful for these reasons, but don’t abuse it
Lastly, each page provides a KeepAlive boolean property, which is false by default. However, it only applies when using URI navigation, which explains why many people have tried changing it only to see no difference. Back/forward simply couldn’t work if objects were not kept alive, since the navigation service has no way to construct them. An option I’d like to see is being able to pass a lambda or delegate which is used to reload the page.
PageFunctions
WPF Pages serve to allow us to build standalone pages within our application - pages like Welcome, Help, or View Account Balances. However, when it comes to completing tasks, they fall short in a number of ways. Consider the scenario for paying a bill:
Once the task is complete, you need to return to the Home page. Should the Confirm page be written to Navigate directly to the Home page when a button is clicked? What if the Pay Bill task was triggered from another page? And if the user clicks "Back" upon arriving back at the Home page, should they get to cancel the payment after they already confirmed it? And what if we don’t want to allow them to go back?
Or consider a more sophisticated example. Maybe the biller they want to pay doesn’t appear in their list of existing billers. You might have another task to define a biller - something like this:
This causes us to think carefully about our navigation model:
- Will the Verify and Confirm page be hard-coded to transition to Transaction details?
- When they get to the Transaction Details page, what will clicking "Back" in the shell do?
- If it went to Verify and Confirm, we’d be in trouble, since the biller has already been saved
What we are really doing here is interrupting one task to start another - like taking a detour - and then returning to our previous task. After we return, we can then complete the original task. Once a task returns, its navigation journal should be cleared, and it should be as if the detour never happened. This creates a couple of rules:
- Within a task, you can click back or forward as much as you like, until it returns.
- When a task is completed, the browser history "stack" is unwound to where it was when the task began
It’s helpful to picture this as code:
PaidBill PayBill()
{
var biller = SelectBiller();
EnterTransactionDetails(biller);
return Confirm();
}
Biller SelectBiller()
{
if (BillerAlreadyExists)
return SelectExisting();
else
return CreateNew();
}
Where each function represents a task that can branch out, and then return to the original task.
Introducing PageFunctions
Now you can begin to sense where Page Functions get their name. WPF Page Functions are a special type of Page that inherit from a PageFunction<T> class. They are standard Page objects, but with one difference: they provide a Return event which makes it possible to unwind the navigation journal.
Returning
Just like our pseudo code above, and like any other kind of function, when a PageFunction returns it needs to declare a type of object that is being returned, so that the page which triggered the PageFunction can get data back from it. Here’s what a the code behind might look like:
public partial class VerifyAndConfirmNewBillerPage : PageFunction<Biller>
{
public VerifyAndConfirmNewBillerPage(Biller newBiller)
{
InitializeComponent();
this.DataContext = newBiller;
}
private void ConfirmButton_Click(object sender, RoutedEventArgs e)
{
this.OnReturn(new ReturnEventArgs<Biller>((Biller)this.DataContext));
}
}
Notice that the class inherits from a generic type. When the user clicks "confirm" on this page, it will raise a Return event to whatever page instantiated it, and returns the Biller that was created.
The page that came before this one would need to subscribe to the event in order to expect it. Here is how that would look:
public partial class DefineNewBillerPage : PageFunction<Biller>
{
public DefineNewBillerPage()
{
InitializeComponent();
this.DataContext = new Biller();
}
private void SubmitButton_Click(object sender, RoutedEventArgs e)
{
var nextPage = new VerifyAndConfirmNewBillerPage((Biller)this.DataContext);
nextPage.Return += new ReturnEventHandler<Biller>(BillerVerified);
this.NavigationService.Navigate(nextPage);
}
private void BillerVerified(object sender, ReturnEventArgs<Biller> e)
{
this.OnReturn(e);
}
}
When the user enters information about their new biller and click "Submit", we navigate to the Verify and Confirm page, and we subscribe to its Return event. On that page, they’ll hit Confirm, which will Return control back to our page. Our page will then also return - this is similar to when you make a function call at the last line of an existing function, in that the innermost function’s stack is cleared, it returns to the calling function, and then that function returns.
Back at the Select Existing Biller page, here’s how that code behind would appear:
public partial class SelectExistingBillerPage : PageFunction<PaidBill>
{
public SelectExisingBillerPage()
{
InitializeComponent();
this.DataContext = GetAllExisingBillers();
}
private void AddNewButton_Click(object sender, RoutedEventArgs e)
{
var nextPage = new DefineNewBillerPage();
nextPage.Return += new ReturnEventHandler<Biller>(NewBillerAdded);
this.NavigationService.Navigate(nextPage);
}
private void NewBillerAdded(object sender, ReturnEventArgs<Biller> e)
{
// A biller has been added - we can pluck it from the event args
var nextPage = new EnterTransactionDetailsPage(e.Result);
this.NavigationService.Navigate(nextPage);
}
private void SelectExisingButton_Click(object sender, RoutedEventArgs e)
{
var nextPage = new EnterTransactionDetailsPage((Biller)listBox1.SelectedItem);
this.NavigationService.Navigate(nextPage);
}
}
Note how in the return event handler our page navigates to the Transaction Details page? Thus, our navigation is actually:
This model allows pages to complete a task, return to whoever called them, and have the previous task continue without any additional knowledge. To users, it would appear as a detour, where in reality the pages operate just like function calls.
Since one page returns to the caller, it might be worrying that the previous page will quickly flash up before navigating. Fortunately, this is not the case, as WPF waits for the Return event handler to execute before loading or rendering the previous page. If the Return event handler from the calling page asks to navigate somewhere else, WPF would perform the navigation without showing the page.
Markup
PageFunctions do inherit from the Page base class, and apart from the concept of returning, they operate just like any other Page. Since the base class is generic, the XAML accompanying the PageFunction does look slight different:
<PageFunction
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:domain=”clr-namespace:DomainModel”
x:Class=”NavigationSample.DefineNewBillerPage”
x:TypeArguments=”domain:Biller”
Title=”Define a new Biller”
>
<Grid>
<… />
</Grid>
</PageFunction>
PageFunction Lifecycles
When using PageFunctions, you need a reference to the object in order to subscribe to the event and for the handler to be invoked, and so navigation with PageFunctions always involves the PageFunction object being left alive in memory; similar to normal pages.
Fortunately, once a PageFunction has returned, it is removed from the navigation journal and can be garbage collected. This means that although the pages that make up a task will consume memory, once the task is finished, the pages are destroyed. In this way, a largely task-focused UI can use the rich navigation model without degrading performance.
Update: As discussed in the comments below, the above paragraph only applies when KeepAlive on each PageFunction is set to true. By default, WPF does some complicated things with PageFunctions and return event handlers, and my recommendation is to avoid this behavior. I’d love to hear real cases where the KeepAlive=false behavior was beneficial.
Summary
Navigation applications are made up of many pages, which can be categorized as either standalone pages, or pages that contribute towards a step in a user’s task. Standalone pages are usually represented as Page objects, and steps within a task are represented as PageFunctions. Pages of any kind can be hosted with a WPF Window using the Frame control, or within an XBAP application hosted by the browser.
The NavigationService object shared between a Page and it’s host provides many hooks into the navigation system. Page lifecycles can complicate things, but are logical when you think about them. Pay special attention to the lifetime of your pages and find ways to reduce their memory footprint when not in view (remove all data during the Unloaded event, and restore it during the Loaded event, for example). As always, use memory profilers to detect major leaks.
Navigation applications make it easy to build user interfaces that are task-focused and geared for ease-of-use over productivity. This style of UI is very difficult to achieve using Windows Forms or other UI systems. Even on the web, which shares similar navigation styles, concepts such as journal unwinding, returning to calling pages and remembering state between pages can be notoriously difficult to implement. Entire frameworks in like Struts in Java exist to enable just these scenarios.
Thanks
Thanks to Omar, Matt, Mitch and Darren for proof-reading this article and pointing out a few things I missed.
Further reading
- The original Inductive UI Guidelines paper
- IUI’s and Web-Style Navigation in Windows Forms
- WPF Navigation Overview
- XBAP’s
Filed under: WPF


You’ve been cranking out quality posts latley!
Thanks ALOT for point this out - I’m really getting into UI design and architecture, and this was a new whitepaper to me.
Cheers,
Jonas
Paul - great intro post into WPF navigation… here’s a few thoughts…
Would you recommend the WPF navigation - in particular PageFunctions for building mid/large scale apps? To me the NavigationService and PageFunctions seemed like a pretty nice idea but in practice it was easier to write a custom framework to do the same thing. [For example Blend doesn’t support PageFunctions. This was my first clue that prehaps what initially appeared the easiest path, wasn’t.]
Would be interesting to see a follow up on how the out-of-box navigation components supplied in WPF can be incorporated into application framework practicing MVP/MVPoo. Or whether it should even be attempted?
I’d also suggest that the Back/Forward button browser paradigm is of limited use in most applications. For relatively linear workflows such as described above they work OK (as does a Next/Previous in a wizard) but for general form navigation I’ve found them to be too confusing. There are so many scenarios where they don’t work or are ambiguous. For example going to a “detail” form and deleting the record - its not possible go back/foward to any form involving that record (since it no longer exists). I’ve attempted building frameworks that take into account all these considerations and at the end of the day have had to explain them to the customer. If it requires explaining, its not intuitive and doesn’t “fit” with the web paradigm its pretending to be. In that particular example the agreed solution was to remove the buttons altogether and instead have a “Back” button that actually represented “drill down” depth (something quite different).
Nigel
Hi Paul,
Great article - But I’ve just been playing with PageFunctions and noticed that KeepAlive does play an important part on them.
The navigation framework does get rid of PageFunction instances unless you set KeepAlive to true (try writing a PageFunction without a default constructor, and ‘OnReturn(…)’ to it. The runtime complains that it cannot create the type unless it has a default constructor.
Digging around I found an old MSDN forum -
http://forums.msdn.microsoft.com/en-US/wpf/thread/4da0da42-eec7-4e57-be87-426356e5f0f4/
and did a quick test - put a textbox, and a private variable on the Page. As the forum said, when the new instance was created the textbox value was set, but the private variable wasn’t.
Cheers,
Graeme
[…] Inductive UI’s with WPF Navigation Applications (Paul Stovell) […]
“Since one page returns to the caller, it might be worrying that the previous page will quickly flash up before navigating. Fortunately, this is not the case, as WPF waits for the Return event handler to execute before loading or rendering the previous page. If the Return event handler from the calling page asks to navigate somewhere else, WPF would perform the navigation without showing the page.”
If I’m understanding you correctly, this is flat out wrong. Unless the KeepAlive property is set to true, when the calling page navigates to the page function, it will indeed “flash up” (whether or not it gets collected before returning can not be determined, due to the nature of GC). Obviously this sounds disconcerting. We’ve got an event handler on that page registered! Well, actually, you don’t anymore.
The MSDN documentation has this curious thing to say about the PageFunction.Return event:
“Occurs when a called PageFunction)>) returns, and can only be handled by the calling page.”
Note the part about “can only be handled by the calling page”. The documentation is serious about that. Try to set it to a handler on some other object, such as the ViewModel when using M-V-VM pattern, and you’ll get a runtime exception. Why is that? And what did I mean when I said you don’t have an event handler on the calling page registered anymore?
I’m not sure precisely how the implementation is done (I’ve been meaning to spelunk here, but haven’t had the time), but basically what happens is the event registration is torn down by the navigation framework, and then reconstructed when you return. This means the actual method that gets called on a return is on the new instance of the calling page, not on the old instance! This is the reason for the restriction from the MSDN documentation. If the handler were on some other object, the navigation system couldn’t perform the same trick and we’d have lifetime issues to deal with.
Unfortunately, this “hack” in the navigation system has some obvious drawbacks. The page function concept doesn’t play nicely with a “pure” MVPoo architecture.
Oh, btw, this is wrong as well.
“When using PageFunctions, you need a reference to the object in order to subscribe to the event and for the handler to be invoked, and so navigation with PageFunctions always involves the PageFunction object being left alive in memory; similar to normal pages. Thus, KeepAlive generally has no effect on PageFunctions. ”
The NavigationService is obviously doing all of the hackery required to make this stuff work, but a PageFunction behaves just like a Page with regards to KeepAlive.
To prove all of this to yourself, create a sample application with a Page1 that calls a PageFunction1 and registers a return handler. Then add a Guid to both and a Debug.WriteLine that prints out the Guid in both constructors, as well as in the return handler.
Proving the lifetime behavior of the Page is simple. Call the PageFunction and return, and note the output indicates a new Page1 was created and it’s that page on which the handler was called.
Proving the lifetime behavior of the PageFunction is nearly as simple. Call the PageFunction, and then instead of returning, use the navigation buttons to navigate back and then forward. Note that a new PageFunction1 is created each time.
Now play with the KeepAlive property for both Page1 and PageFunction1 and note the change in behavior.
This behavior is what one would expect at a first blush. However, experienced .NET coders at some point will conclude what you did, and then be surprised that this is not the actual behavior. MSDN doesn’t explain this… it only hints at it with the cryptic comment about the handler having to be on the calling page. Fun, aint it? And by fun, I actually mean it’s a PITA to deal with in all but the most general cases, and then it’s a surprise to experienced developers.
Hi wekempf,
Your comments are very valid and appreciated. When I had tested returning, I had accidentally left KeepAlive in my tests. Because of that, the rest of my observations appeared to match with those of regular page functions, and things generally “just worked”. It’s actually quite concerning given that KeepAlive doesn’t seem to affect regular pages depending on the navigation model, but it does affect page functions. If you navigate from the Return event handler, as I wrote, you do indeed avoid the page “flashing”; however there’s a trick as described here which I neglected:
http://forums.msdn.microsoft.com/en-US/wpf/thread/5dbb6ce1-0f81-431b-a9c6-0ce59e780c2d
However, I don’t them as being as bad as you present; so long as KeepAlive is always True, the rest of the article applies. Personally I can’t see why KeepAlive shouldn’t be true, given the pagefunctions are collected as soon as the “task” is finished (and they have returned).
I’ll update the article in light of these revelations and make some other parts a little clearer. Thanks for pointing out the omission!
Paul
This has GOT to be one of the best “How To” articles around regarding designing and building a WPF application that performs basic navigation! Thanks so much!
Graeme,
You are correct in the same point as wekempf; KeepAlive needs to be true for PageFunctions to perform the way you expect (no requirement for a public default constructor, instances kept alive, return event raised normally). Setting it to false (or leaving the default) really screws things up. I assumed that just as KeepAlive in regular pages didn’t take effect if the page was built manually, the same would logically apply to PageFunctions, which was a mistake. I’ll update the article.
Paul
I’m not sure what you mean by “It’s actually quite concerning given that KeepAlive doesn’t seem to affect regular pages depending on the navigation model, but it does affect page functions.” In what specific way do you think KeepAlive differs between the two?
The link you provide doesn’t seem to be very related? However, I now believe I misunderstood what you meant by “flashing”. I assume that meant the page would “go up in smoke” as in it would be collected. However, now I believe your just referring to a flash in the UI as a page is navigated to and then away from quickly, correct? My misunderstanding… it but it lead to an important discussion. To me, the lifetime aspects of pages are much more concerning than anything else here.
BTW, I’ve discovered at least some of the magic involved here. There’s an internal ReturnEventSaverInfo structure that does the magic of attaching/detaching registered event handlers.
>> I’m not sure what you mean by “It’s actually quite concerning given that KeepAlive doesn’t seem to affect regular pages depending on the navigation model, but it does affect page functions.” In what specific way do you think KeepAlive differs between the two?
As I wrote in the pages section, when you navigate by passing a page object directly, which can have constructor parameters etc., navigation behaves exactly as if KeepAlive were true, even if it is set to false. KeepAlive for normal pages only seems to take effect when navigating via a URI.
>> However, now I believe your just referring to a flash in the UI as a page is navigated to and then away from quickly, correct?
Yep
>> To me, the lifetime aspects of pages are much more concerning than anything else here.
For a single flow of pages in a task, the lifetimes don’t concern me as the GC will collect the pages as soon as the pagefunctions return (even with keepalive set to true). I may have 7 pages strung together in a task, but so long as the memory is cleared once the task is complete, I don’t mind all 7 pages occupying memory during the task. I do recognise that this will vary between applications and the amount of data you are dealing with.
Paul
Sorry, the link I meant was:
http://forums.msdn.microsoft.com/en-US/wpf/thread/b37dee34-2301-411a-9be0-054954b84ded
I normally do this by storing a reference to the NavigationService as a private variable, but the approach in that post works too.
Hi Nigel,
>> For example going to a “detail” form and deleting the record - its not possible go back/foward to any form involving that record (since it no longer exists).
This is actually where PageFunctions would help. When you return from a PageFunction, it is removed from the navigation journal (unless RemainsInJournal is true), so the user can’t go “back”. I believe you can also manage the navigation journal yourself. If I really needed a custom navigation scheme, I’d probably use WPF’s navigation display and Navigate() mechanisms and just deal with the journal myself.
As far as using navigation in MVP scenarios, it’s not something I’ve been able to do and I have spent some time thinking about it. I think it’s possible but would involve creating a few wrapper interfaces that can be injected into each presenter. I might try to piece together an example of doing it.
All of that said, I have no problem throwing out the built in frameworks and rolling your own if it doesn’t suit your needs. My goal was really to explain the WPF navigation model and some of the issues/limitations for those who want to use it. I do think it handles 80% of cases quite well, but I’m sure you’ve thought of the issues around these systems a lot more than I have.
Paul
Hi Paul,
Awesome post!
I have a question which I can’t find an answer to, even in Chris Sells’ book:
If it’s not a good idea to pass data in the URI, and I don’t want to pass data in by Navigating to an object instead of a URI (because it would force WPF to retain it until the Frame is gone), then how should I pass data?
The extraData parameter in Navigate doesn’t seem to be accessible from the navigated page. Any ideas?
By the way, I love your Bindable LINQ project. That’s bound to come in handy soon.
Thanks,
Rei
One thing I’ve noticed when using a custom host window with an embedded frame is that this code:
… doesn’t work when the frame navigates to HTML content rather than XAML. Obviously this is because HTML isn’t rendered using a ContentProvider and thus has no “Content.Title” property to bind to.
For now I’ve managed to work around the problem by changing my binding to this:
External Content
So if it can’t find a “Content.Title” property, it falls back to the text “External Content”.
I think with a custom converter and a bit of tinkering this could be made to tunnel into the HTML content and discover the title of the page you’ve navigated to, but for now this is doing the job.
D’oh! All my XAML got cleansed from that last comment!