Magellan and WPF Page management woes

Progress on Magellan has been a little slow this week as I have been experimenting with some ways to optimize memory usage when dealing with WPF pages. A lot of it comes down to issues with WPF's navigation system and some of the limitations it has.

The major problem we encounter when using WPF Page objects is that if you were to do something like this:

NavigationService.Navigate(new MyPage());

You just got yourself an object that will never be garbage collected.

This happens because the WPF navigation journal has to have a way of returning 'back' to that page if the user clicks a back button. Since you created it, the navigation service has no way of knowing how to re-create it, so it just keeps a reference to it. The only way of clearing these out is to call NavigationService.RemoveBackEntry a to remove the page from the navigation journal completely.

Using URI's

So the solution is to use URI's for navigation:

NavigationService.Navigate(new Uri("/MyPage.xaml", UriKind.Relative));

Now when the WPF navigation service adds the page to the journal, it just stores the URI rather than keeping a reference to the instance of the page. Problem solved, right?

If you think so, you're about to learn about the stupidest feature that ever made it into WPF :)

Metadata Journaling

When most people think ViewState, they think ASP.NET web forms, and Notepad.exe screens full of encoded data taking up the top half of an ASPX page. They think of a confusing page lifecycle and a leaky abstraction that takes weeks to comprehend. Well, it turns out WPF has it's own version of ViewState - although it's only used when clicking "Back".

When URI's are used for Page navigation, the WPF navigation framework creates a journal entry with the URI. Then it walks the visual tree, and any dependency property that it encounters with the FrameworkPropertyMetadata.Journal flag will be serialized. Most properties don't have this flag however, so only small parts of the page are saved.

What this means is that if we use a Model-View-ViewModel pattern and set the DataContext on our page, when go 'Back', we'll no longer have a data context, since the DataContext property isn't serialized. All data will be missing from the page.

Using CustomContentState

A solution to this is to use WPF navigation's IProvideCustomContentState feature. This allows you to preserve information about the page before it is added to the journal, and to restore it when you return to the page. Here is an example:

public class PageView : Page, IProvideCustomContentState
    public PageView()

    public CustomContentState GetContentState()
        return new RestoreModelContentState(DataContext);

internal class RestoreModelContentState : CustomContentState
    private readonly object _model;

    public RestoreModelContentState(object model)
        _model = model;

    public override void Replay(NavigationService navigationService, NavigationMode mode)
        var element = navigationService.Content as FrameworkElement;
        if (element == null) return;
        element.DataContext = _model;

Now, when you leave a page, the page will be garbage collected but not before the DataContext is moved into the journal. If you click 'Back' a new page will be created, and the DataContext will be restored.

The problem now, though, is that while our pages are being garbage collected, the DataContext's aren't. If we have a big model, that model will sit around forever unless we clear the navigation journal. This might be OK if your models aren't too big.


Right now I'm at a point where I don't think there's a good clean solution for this that doesn't involve memory being wasted.

The cop-out solution seems to be to avoid the problem entirely by rolling back the journal at major points during the application, so that the journal never gets too big. A good example is to clear the journal after a major task completes, or when the user is taken back to the home page. If this were the case, the combination of CustomContentState to restore the model plus URI's could work.

I do have a very tricky solution in mind, which involves only using a single page with a query string in the URI, and having it re-invoke the controller when you click back. This way, models wouldn't need to be kept alive at all as the page would just be re-fetched. The other benefit to this is when you go back, the page would be up-to-date rather than an old copy of the page, which seems to suit the rich client model better. It would involve a breaking change (your views would have to become ContentControls or UserControls instead of Pages) but might have the best result overall.

Finally, I'm also considering throwing out WPF navigation completely and rolling my own. It wouldn't be tied to Magellan's MVC model specifically, but it would support scenarios that Magellan is trying to accommodate as well as getting around other WPF navigation limitations (like the stupid PageFunction Return event handler must be a Page limitation).

I'd love to get your thoughts on what you would expect from something like Magellan.

A picture of me

Welcome, my name is Paul Stovell. I live in Brisbane and work on Octopus Deploy, an automated deployment tool for .NET applications.

Prior to founding Octopus Deploy, I worked for an investment bank in London building WPF applications, and before that I worked for Readify, an Australian .NET consulting firm. I also worked on a number of open source projects and was an active user group presenter. I was a Microsoft MVP for WPF from 2006 to 2013.

29 Nov 2009

My opinion is to "roll your own", but why not use an existing alternative? Have you looked at Orktane's nRoute? That might give you what you need without having to write something from scratch.

Carl Scarlett
Carl Scarlett
22 Dec 2009

Is the problem even worth solving? How many applications have you seen that use pages? How many of those have large models? Given the 80-20 rule, it sounds like a lot of effort to solve an issue for what I suspect is a minority.

It'd be good to track down some stats on the breakdown of types of WPF apps. You probably need to implement a system of metric collection into Magellan to answer this sort of question once you get some take-up.