How Controls Retain Their State when You Navigate Away

Over on Bea’s excellent blog, Sam asked a great question about how controls retain their state when you navigate away from a page:

This got nothing to do with threads, sorry for intruding, but I don’t know where to ask this question (managed newsgroups provide no answer)

When you navigate back in a browser-like WPF application, the pages are re-created (at least the constructor is called).

And somehow, magically, the content for Textbox(es) in the page will be filled with whatever had been in there when the page had been left.

Since the page is created anew on navigating ‘Back’, where does this content come from? Is it bound to some kind of session storage as default?

And how do I get my own values back (as content for Listboxes for example are lost on navigating)?

thanks, Sam!

First of all, Sam, I am sorry that no one answered your newsgroup question. I just returned from a long vacation and I hope that this delayed response is still helpful.

Understanding how control state is retained requires a bit of knowledge of how “journaling” works. Journaling is the mechanism for keeping track of the user’s navigation history – the “Journal” is an internal data structure which consists, among other things, of a ForwardStack and BackStack (these stacks are exposed on Frame and NavigationWindow). As a user navigates through a set of pages (say by clicking on Hyperlinks that point to other pages), JournalEntries are added to the BackStack. Specifically, a BackStack entry for a page is added upon navigating away from the page. If the user clicks on the back button, a few things happen: a JournalEntry is popped off the BackStack, the page it represents is rehydrated, the framework navigates to the rehydrated page, and a JournalEntry for the current page is created and added to the ForwardStack. If a user initiates a new navigation, the forward stack is cleared.

This system is modeled after how navigation works in all popular web browsers today, and you will notice that WPF navigation works the same way that navigation works in Internet Explorer, for example.

There are two main ways that JournalEntries are created, depending on what is being journaled (and also on the value of the KeepAlive property, which I will discuss in a moment):

  1. By URI: If you navigate to a XAML page via its source URI (e.g. page1.xaml), and then navigate away from it, the page is stored simply by creating a new JournalEntry that stores the URI for that page (the JournalEntry.Source property is used for this purpose). Navigating back to the page simply requires the framework to get the Source and navigate to it.
  2. By object: If you navigate to an object rather than a XAML page (e.g. by creating an object tree programmatically and calling Navigate(object content)), and then navigate away from it, we cannot journal by URI because there is no URI (!). In this case, the object tree is kept alive and a reference to the tree is stored in its JournalEntry. Navigating back to the page involves re-attaching the saved tree as the Content of the Frame or NavigationWindow.

The default behavior for journaling pages that have URIs is by URI, but this can be overridden by setting JournalEntry.KeepAlive (which is an attached DP) if desired. Keep in mind that there are working set implications when you use KeepAlive, because the whole page really is kept alive in memory. URI JournalEntries are, of course, smaller.

At this point, it is probably obvious to you that when we journal “by object,” storing the user-modified state of controls (like TextBoxes and RadioButtons) is trivial: because the whole tree is kept alive, there is no additional work to do. When we journal by URI, we traverse the tree and look for controls that have properties which should be journaled. We store these property values in the JournalEntry along with the source URI for the page. We know which properties should be journaled because they have set the FrameworkPropertyMetadataOptions.Journal flag. Also note that we will ignore controls which do not have the PersistId property set. When a page is rehydrated from a JournalEntry, we navigate to the JournalEntry.Source URI and we apply all of the deltas to control state that we have stored in the Journal.

In general, controls whose state may be changed via user interaction maintain their state across navigations (there are notable exceptions, like PasswordBox). When authoring custom controls, you should consider whether or not your controls have state that may be modified via user interaction, and if they can, then you should usually mark those properties with FrameworkPropertyMetadataOptions.Journal and set the PersistId. If your control stores sensitive data like a password or credit card number, you will likely not want that state to be journaled.

Sam, I did not understand the last part of your question, about ListBox. Could you elaborate or send me an example of some source code that is not behaving as expected?

6 thoughts on “How Controls Retain Their State when You Navigate Away”

  1. It had been quite a surprise to read my own name in your blog – had to look twice :)

    First, thanks for the explanation, this is really helpful for me since I’m new to this browser-like stuff.

    I had stumbled upon KeepAlive=”True” to get my pages to work, but to understand the ‘why’ and ‘how’ of this is very nice.

    For my question about the Listbox, I had a Page containing a Listbox and a few buttons/Textboxes to enter stuff into the Listbox.

    On navigating to another page and back to the Listbox page the content of the Listbox was gone.
    Should a Listbox get persisted just like a Textbox, and I was using it wrong?
    (do you need some source to clarify? I could squeeze an example in a comment, I guess.)

    oh, and thanks a lot!

  2. Lauren,

    I sent you an answer by mail a few days ago, did you get that mail?

    Somehow I got the feeling the mail got lost :(

  3. For those following along at home, this is the response I sent to Sam in email:

    Sam,
    We tried this out, and we saw the same behavior you are seeing. According to Sam Bent, one of our databinding experts, one option would be to actually update the SQL database before navigating. If this is not the right behavior for your application, you could:

    • Use KeepAlive=true (which I believe you have already experimented with. This should work, but of course will keep the whole page alive in memory)
    • Keep a reference to the ListBox data source and repopulate it yourself
    • Maintain static state for the page that the new instance of the page would reference and manipulate (assuming you are journaling by URI and not using KeepAlive=true)

    (Thanks to Dave Relyea for help with the suggestions above).

    Hope this helps,
    Lauren

  4. In Navigate “By object” you say:

    “In this case, the object tree is kept alive and a reference to the tree is stored in its JournalEntry.”

    I didn’t test this to see if it were true but it is not true if you navigate a 3rd way which you didn’t mention but is very similar to By Object:

    pfAccountStatus pf = new pfAccountStatus();
    mainFrame.Content = pf;
    // pf… is a PageFunction, mainFrame is a Frame

    I could have done this By Object the other way and maybe I should have accept now the object tree is not kept alive accept for the properties I turn the Journal bit on for.

    On another note, any idea how you can programatically keep a navigated page from going into the journal at all (i.e. you can’t navigate back to it)? I was googling that question when I stumbled on this page.

  5. how read data from JouranelEntry ,i.e i want stire in text block in Hoizontal way , is it possible ?

Comments are closed.