Click here to Skip to main content
11,646,838 members (76,016 online)
Click here to Skip to main content

Persisting the state of a web page

, 7 Mar 2006 322.9K 3.1K 223
Rate this:
Please Sign up or sign in to vote.
The complete story on how to persist the state of an ASP.NET web page, including ViewState, Form and QueryString data, by emulating a PostBack.

Overview

One very common requirement of any application is to display a page, allow the user to open a sub-page, and go back to the original ("parent") page when the sub-page is closed. A typical example is to choose an item from a list, display or edit it, and then return to the same position in the list where we started from. This is a no-brainer in a traditional client-side application, but the ASP.NET Framework makes it quite difficult to do in a .NET web application.

What we want is for the page to behave normally in a normal postback situation, but if the page is displayed without a postback, we want to fool it into accepting our stored postback information as a real postback. This article explains how to achieve this in the most unobtrusive way possible.

Background

Because this is such a common requirement of any application, I was surprised that it is not supported by the ASP.NET Framework, and even more surprised that I could not find a single reference to someone with a satisfactory method of achieving it. I found many vague hints about overriding the Page.LoadPageStateFromPersistenceMedium() and Page.SavePageStateToPersistenceMedium(viewState) methods, and although this forms part of the solution, there is a lot more work that needs to be done in order to get the desired results.

After much searching, I found an article by Paul Wilson called "ViewState: Restore after Redirect". His article provides the basis of much of the solution presented here, but had one major flaw which I will discuss below. After devising my own improvements to his solution, I decided to write this article to provide a single, concise, easy to find reference on the subject.

Skip down to the solution if you just want to use the functionality, otherwise read on for an explanation of how it works.

Solution walkthrough

The immediate assumption by most people, including myself, when confronted with this problem is that the state of a page can be reconstructed entirely using only the page's ViewState. This, however, is not the case.

The problem with ViewState

There is still a lot of confusion about what the ViewState contains, and how to store and access the information. This is probably due to the poor documentation provided on the subject and the many obscure data structures used to represent it. The Page.ViewState property represents it using a class called System.Web.UI.StateBag, the Page.LoadPageStateFromPersistenceMedium() method leaves us guessing by typing it as a generic object, although it is actually a tree with a root of type System.Web.UI.Triplet, and the Control.SaveViewState() method uses a different representation again, which as far as I could determine is actually a generic object that only has to be compatible with the corresponding Control.LoadViewState(object) method. The __VIEWSTATE string is a representation of the Page.LoadPageStateFromPersistenceMedium() representation of the object that has been serialized using the LosFormatter class. I was pretty close to giving up myself after sifting through this poorly cross-referenced documentation, thinking that ViewState is obviously something Microsoft doesn't want us to mess with too much for whatever reason.

There are other good articles explaining ViewState data structures, but the good news is that you don't really have to understand them. The one thing that is important to know however is that the ViewState does not contain all of the information required to reconstruct the state of a page, contrary to what some people might tell you. It contains all of the information except the data which is available in the Request.Form collection, which I refer to in this article and in the code as PostData. Nor does it contain any data which was provided in the request's query string, which is the list of parameters provided at the end of the request URL and accessed using the Request.QueryString property. The PostData is used to repopulate the current values of the form fields and other controls, and is essential for the reconstruction of a page's state. One cannot assume that a page does not use QueryString parameters to reconstruct its state during a postback.

This means that the ViewState, the PostData, and the QueryString must all be stored to be able to reconstruct the state completely. Some astute readers may wonder why the ViewState needs to be stored separately when the PostData already contains the ViewState in the hidden __VIEWSTATE field. The reason is that the __VIEWSTATE field contains the ViewState of the page when it was last displayed, and does not reflect any changes that may have been made to it during the processing of the current request.

Although the fact that the PostData is not saved in the ViewState is mentioned in other articles on this subject, none actually attempt to persist it along with the ViewState, let alone the QueryString data. The Paul Wilson article mentioned above attempted to solve this problem by adding a Change event handler to every control on the page, which forces the relevant PostData to be included in the ViewState. This is far from ideal as it requires constant intervention to ensure that all the controls are updated by the persistent state mechanism.

The "PageState"

Here, I will introduce the term PageState, which is a combination of the ViewState, PostData, and the request URL of the page. This combination contains all of the information required to reconstruct the page's state, since the QueryString forms part of the URL. We can represent it using the following class:

public class PageState {
    public object ViewStateObject;
    public NameValueCollection PostData;
    public Uri Url;

    public PageState(object viewStateObject, 
        NameValueCollection postData, Uri url) {
        ViewStateObject=viewStateObject;
        PostData=postData;
        Url=url;
    }
}

Note that the Page.LoadPageStateFromPersistenceMedium() and Page.SavePageStateToPersistenceMedium(viewState) methods also refer to something called a "PageState" in their names. This is just an unfortunate inconsistency, as they really only deal with the ViewState.

Now that we know what we need to persist, all we have to do is override the Page.SavePageStateToPersistenceMedium(viewState) method to create a PageState object and store it somewhere like in the Session, and override the Page.LoadPageStateFromPersistenceMedium() method to load it back again when the page is next displayed. Right?

A first attempt

Let's try the following code:

// pageState is non-null if a postback is being emulated
// from the persisted PageState:
private PageState pageState=null;

protected bool flagToIndicateThatPageIsBeingRedirected=false;

protected override object LoadPageStateFromPersistenceMedium() {
    pageState=LoadPageState(Request.Url);
    if (IsPostBack || pageState==null) {
        // this is a normal postback, so don't use persisted 
        // page state
        pageState=null;
        // clear the page state from the persistence medium so
        // it is not used again:
        RemoveSavedPageState(Request.Url);
        return base.LoadPageStateFromPersistenceMedium();
    }
    // If we get to this point, we want to 
    // restore the persisted page state.
    // Check whether the current request 
    // URL matches the persisted URL:
    if (pageState.Url.AbsoluteUri!=Request.Url.AbsoluteUri) {
        // The url, and hence the query string, 
        // doesn't match the one in the
        // page state, so reload this page 
        // immediately with the persisted URL:
        Response.Redirect(pageState.Url.AbsoluteUri,true);
    }
    // clear the page state from the persistence medium so
    // it is not used again:
    RemoveSavedPageState(Request.Url);
    Request.Form=pageState.PostData;
    return pageState.ViewStateObject;
}

protected override void SavePageStateToPersistenceMedium(
                                          object viewState)
{
    if (flagToIndicateThatPageIsBeingRedirected) {
        // persist the current state
        SavePageState(Request.Url,new PageState(viewState,
                                 Request.Form,Request.Url));
    } else {
        // default to normal behaviour
        base.SavePageStateToPersistenceMedium(viewState);
    }
}

protected static PageState LoadPageState(Uri pageURL) {
  return (PageState)HttpContext.Current.Session[
                              GetPageStateKey(pageURL)];
}

protected static void SavePageState(Uri pageURL, 
                                 PageState pageState) {
    HttpContext.Current.Session[GetPageStateKey(pageURL)]=
                                                  pageState;
}

protected static void RemoveSavedPageState(Uri pageURL) {
    SavePageState(pageURL,null);
}

private static string GetPageStateKey(Uri pageURL) {
  // Returns a key which will uniquely 
  // identify this page's PageState
  // in a global namespace based on its URL path.
  return "_PAGE_STATE_"+pageURL.AbsolutePath;
}

The persisted PageState, if it exists, is stored for possible later use in a class variable called pageState.

The flagToIndicateThatPageIsBeingRedirected is intended to be a boolean flag which needs to be set whenever a Response.Redirect() method is called so that the SavePageStateToPersistenceMedium(viewState) method knows that the state needs to be persisted.

The LoadPageState(Uri), SavePageState(Uri,PageState), RemoveSavedPageState(Uri) and GetPageStateKey(Uri) methods at the bottom encapsulate the process of loading, saving, and removing the page state from an arbitrary persistence medium (in this case the Session object) using a key based on the page's URL.

The code in LoadPageStateFromPersistenceMedium() that redirects the page if the current request URL doesn't match the persisted URL acknowledges that we can't simply set the request's URL (and hence QueryString) by changing its properties. Redirecting to the persisted URL seems the cleanest way to ensure that the Request.Url and Request.QueryString properties are the same as when the state was saved.

If you try to compile this code, you would find that the compilation fails because the Request.Form property is read-only. To make matters worse, the internal System.Web.HttpValueCollection object used to store the data in this property is also read only, so we can't even populate it with our own data. Let's just ignore this problem for the time being by commenting this line out.

Once we have compiled the code, some simple testing will reveal that some other problems are preventing things from working as intended anyway. To cut a long story short, we also have the following issues:

  1. The LoadPageStateFromPersistenceMedium() method is never called by the ASP.NET Framework if IsPostBack is false.
  2. Because IsPostBack still returns false when we are "emulating" a PostBack, any other code in the page that checks it will not function correctly.
  3. The SavePageStateToPersistenceMedium() method is never called by the ASP.NET Framework if the Response.Redirect() has previously been called.

It turns out that the first two of these problems can be easily solved by overriding the page's DeterminePostBackMode() method, which is used by the framework to determine the value of IsPostBack and whether the LoadPageStateFromPersistenceMedium() method is executed. A post back is signified by a non-null return value from DeterminePostBackMode(). The documentation of this method is not very explicit about what exactly the return value is used for, but my experiments suggest it is used to feed the event processing methods, but (strangely) not to populate the control values. We can therefore override the method to perform its default behavior if a real PostBack is occurring, or to return a non-null object if we want to emulate a PostBack with our persisted data. Note that returning our saved PageState.PostData object will cause the same event to fire that caused the original redirection, which is not what we want. The empty Request.Form object does the trick and is of the correct type:

protected override NameValueCollection DeterminePostBackMode() {
    pageState=LoadPageState(Request.Url);
    NameValueCollection normalReturnObject=
                       base.DeterminePostBackMode();
    // default to normal behaviour if there 
    // is no persisted pagestate:
    if (pageState==null) return normalReturnObject;
    if (normalReturnObject!=null) {
        // this is a normal postback, so 
        // don't use persisted page state
        pageState=null;
        // clear the page state from the persistence medium so
        // it is not used again:
        RemoveSavedPageState(Request.Url);
        return normalReturnObject;
    }
    // If we get to this point, we want to 
    // restore the persisted page state.
    // Check whether the current request 
    // URL matches the persisted URL:
    if (pageState.Url.AbsoluteUri!=Request.Url.AbsoluteUri) {
        // The url, and hence the query string, 
        // doesn't match the one in the
        // page state, so reload this page 
        // immediately with the persisted URL:
        Response.Redirect(pageState.Url.AbsoluteUri,true);
    }
    // clear the page state from the persistence medium so
    // it is not used again:
    RemoveSavedPageState(Request.Url);
    // return a non-null value to indicate 
    // a PostBack to the framework:
    return Request.Form;
}

protected override object LoadPageStateFromPersistenceMedium() {
    // default to normal behaviour if we don't want to
    // restore the persisted page state:
    if (pageState==null) 
        return base.LoadPageStateFromPersistenceMedium();
    // otherwise, return the ViewStateObject 
    // contained in the persisted pageState:
    // (The following line is commented out 
    // as we will deal with the problem 
    // of the read-only Request.Form property later:)
    // Request.Form=pageState.PostData;
    return pageState.ViewStateObject;
}

Notice that the code checking the request URL has been moved from the LoadPageStateFromPersistenceMedium() method up into the DeterminePostBackMode(), which happens earlier in the page lifecycle.

Solving the third problem is also relatively easy. Instead of calling the Response.Redirect() method, other parts of the code should simply set a class variable containing the URL to redirect to. The SavePageStateToPersistenceMedium() method can then call Response.Redirect() after saving the state if the variable is set:

// redirectSavingPageStateURL contains the URL to redirect to:
private string redirectSavingPageStateURL=null;

public void RedirectSavingPageState(string url) {
    // Call this method instead of 
    // Response.Redirect(url) to cause this
    // page to restore its current state 
    // when it is next displayed
    redirectSavingPageStateURL=url;
}

protected override void SavePageStateToPersistenceMedium(
                                          object viewState)
{
    if (redirectSavingPageStateURL==null) {
        // default to normal behaviour
        base.SavePageStateToPersistenceMedium(viewState);
    } else {
        // persist the current state and redirect to the new page:
        SavePageState(Request.Url,
            new PageState(viewState,Request.Form,Request.Url));
        Response.Redirect(redirectSavingPageStateURL);
    }
}

The problem with Request.Form

Now, back to the problem with the read-only Request.Form property.

This is one problem for which the workaround is still fairly clunky. Hopefully, Microsoft will see fit to make the Request.Form property read/write in a coming version, but in the meantime, we have to be content with substituting our own PostData property where we have been directly accessing Request.Form everywhere else in the page.

The PostData property will return our persisted PostData if it has been loaded, otherwise it returns the Request.Form object:

public NameValueCollection PostData {
    get {
        return (pageState!=null) ? 
            pageState.PostData : Request.Form;
    }
}

There is still one problem however. The ASP.NET Framework automatically populates control values with the data from Request.Form, and does not provide any way of specifying an alternative source for this data (see the MSDN documentation on processing postback data, for details). We therefore need to carry out this operation manually when the persisted state is loaded. This turns out to be somewhat more convoluted than one might first expect, and the current "best guess" as to how to achieve it is represented by the following code:

// Populate controls with PostData, saving 
// a list of those that were modified:
ArrayList modifiedControls=new ArrayList();
LoadPostData(this,modifiedControls);
// Raise PostDataChanged event on all modified controls:
foreach (IPostBackDataHandler control in modifiedControls)
    control.RaisePostDataChangedEvent();

which uses the following private method:

/// <span class="code-SummaryComment"><summary>
</span>

All we need to do is have this code called after the framework has loaded the ViewState, and we have our final solution! An appropriate place to insert it is in the form's OnLoad method.

One last improvement

Another very common requirement of a sub-page is the ability to pass data back to the parent page. The solution discussed so far does not easily support this, as the whole idea has been to reconstruct exactly the state of the page before the sub-page was called. The most obvious way the data can be passed back is for the sub-page to include it in the query string when redirecting back to the parent page, but the current solution considers the query string to be part of the page state which must be restored to its original state.

To make the query string data passed back from the sub-page available to the parent page, we need to save it before it is overwritten with the original query string. We can store this saved data along with the original state data in the PageState object, and access it via a new property called PassBackData.

The DeterminePostBackMode() method contains the code that saves the query string to the PageState object, and the rest of the code to support this is fairly self-explanatory.

The final solution

Our code now looks like this:

/// <span class="code-SummaryComment"><summary>
</span>

The constructor has been added to include setting the EnableEventValidation property to false if running under .NET 2.0. It must remain commented out if running under .NET 1.1 as the property does not exist in versions prior to 2.0.

The public IsRestoredPageState property allows the page developer to determine whether the page has been restored from a persisted state. In most cases, it will not be necessary to distinguish between a normal postback and an emulated postback from a persisted state, but giving the developer access to this information will almost certainly be useful in some circumstances. It is also used internally for better readability instead of checking for a non-null value of pageState.

The static RedirectToSavedPage(string url, bool restorePageState) and RemoveSavedPageState(string url) methods are intended for use from other pages to control whether this page's state is restored when it is next displayed.

The source download at the top of this article contains an extended System.Web.UI.Page class called X.Web.UI.PersistentStatePage that contains all of this functionality. You can either extend the PersistentStatePage class instead of the standard Page class whenever you need a page requiring a persistent state, or you can simply copy the code into your own pages.

The PageState class has also been modified to make it serializable, which is necessary when it is to be stored in a database or other persistent medium. This involves creating a serializable wrapper class around the view state object, since the System.Web.UI.Triplet class used by the framework to represent it hasn't had the [Serializable] attribute applied to it and so can't be serialized automatically.

The modified PageState class and the SerializableViewState wrapper class are defined as follows:

/// <span class="code-SummaryComment"><summary>
</span>

Using the solution

To use the solution as provided in the source download in your existing page, the following changes are required to your code:

  1. First of all, if you are using .NET 2.0, uncomment the EnableEventValidation=false line in the constructor of the PersistentStatePage class.
  2. Your page should extend X.Web.UI.PersistentStatePage instead of System.Web.UI.Page, or alternatively copy the code into your own page.
  3. Use RedirectSavingPageState(url) instead of Response.Redirect(url) whenever you want the current page to restore its current state when it is next displayed.
  4. Replace every occurrence of Request.Form (if any) with PostData in your state-saving pages.

To redirect back to a page that has saved its state from a sub-page, or any other page:

  • Call the PersistentStatePage.RedirectToSavedPage(url, restorePageState) method, specifying whether its state should be restored.

or:

  • Call the Response.Redirect(url) method as normal, which is equivalent to calling PersistentStatePage.RedirectToSavedPage(url, true).

Either way, you can pass data back to the parent page by including it in the query string of the specified url, which can then be accessed via the PassBackData property in the parent page.

Good luck with your ASP.NET projects, and make sure you ask yourself the question: why aren't I writing a ClickOnce application instead?

Version history

  • 7th Jul, 2004
    • First published.
  • 3rd Mar, 2005
    • Bug fixes: Code populating controls with PostData moved from the LoadPageStateFromPersistenceMedium method to the OnLoad method, and calls to RaisePostDataChangedEvent added.
  • 15th Apr, 2005
    • Bug fix: PageState made serializable.
  • 30th May, 2005
    • Feature: IsRestoredPageState property added.
  • 15th Jul, 2005
    • Feature: RemoveSavedPageState and RedirectToSavedPageState methods added.
  • 15th Dec, 2005
    • Bug Fix: Grouped radio buttons fixed.
  • 22nd Dec, 2005
    • Bug Fix: PostData[nameAttribute] is checked for null value before calling LoadPostData on controls, which was causing exceptions for some controls.
  • 12th Jan, 2006
    • Bug Fix: control.ID replaced with control.UniqueID to fix the problem with controls contained within a data binding server control that repeats.
  • 5th Mar, 2006
    • Bug Fix: CheckBoxList controls fixed.
    • Bug Fix: Support for .NET 2.0 added by documenting required change to constructor.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

Share

About the Author


You may also be interested in...

Comments and Discussions

 
QuestionPersistentStatePage with Event Validation Pin
Lluthus9-Feb-15 21:37
memberLluthus9-Feb-15 21:37 
GeneralMy vote of 5 Pin
BdB7869025-Jan-11 0:32
memberBdB7869025-Jan-11 0:32 
GeneralGreat Piece Pin
Dagdason23-Dec-10 12:10
memberDagdason23-Dec-10 12:10 
GeneralNeed help abt the persistance Pin
navengates26-Aug-10 22:13
membernavengates26-Aug-10 22:13 
GeneralMy vote of 5 Pin
ogoidias2-Jul-10 11:26
memberogoidias2-Jul-10 11:26 
QuestionCan it work without disabling EnableEventValidation Pin
Shrikant Kale21-Mar-10 21:11
memberShrikant Kale21-Mar-10 21:11 
Generalworks right out of the box Pin
Synjyn1-Dec-09 1:43
memberSynjyn1-Dec-09 1:43 
GeneralMultiView Control Pin
Jason Tepe14-Aug-09 11:30
memberJason Tepe14-Aug-09 11:30 
Generalusing your code Pin
qadirv18-Jun-09 1:23
memberqadirv18-Jun-09 1:23 
GeneralRe: using your code Pin
Synjyn1-Dec-09 1:43
memberSynjyn1-Dec-09 1:43 
GeneralUsing AJAX Pin
alhambra-eidos26-May-09 2:44
memberalhambra-eidos26-May-09 2:44 
GeneralDeterminePostBackMode is the key to vitality! Pin
Alexey D. Luffy14-May-09 23:36
memberAlexey D. Luffy14-May-09 23:36 
GeneralRe: DeterminePostBackMode is the key to vitality! Pin
Martin Jericho15-May-09 3:28
memberMartin Jericho15-May-09 3:28 
GeneralRe: DeterminePostBackMode is the key to vitality! Pin
GezwiZ28-Mar-12 3:17
memberGezwiZ28-Mar-12 3:17 
GeneralRe: DeterminePostBackMode is the key to vitality! Pin
Lluthus10-Feb-15 6:50
memberLluthus10-Feb-15 6:50 
Generalgood job Pin
Mico_Perez_II9-Dec-08 20:01
memberMico_Perez_II9-Dec-08 20:01 
QuestionJavascript is not working while returning back to the parent page Pin
ramanansekar6-Sep-08 3:19
memberramanansekar6-Sep-08 3:19 
GeneralGood solution Pin
Sunil@CP17-Jun-08 0:52
memberSunil@CP17-Jun-08 0:52 
GeneralProblem in Callback Script Pin
fsm_cute19-May-08 20:11
memberfsm_cute19-May-08 20:11 
GeneralGreat Article, however I think I found bug Pin
karpach9628-Jan-08 12:38
memberkarpach9628-Jan-08 12:38 
GeneralPersistentStatePage with Event Validation Pin
wzychla19-Nov-07 22:14
memberwzychla19-Nov-07 22:14 
GeneralRe: PersistentStatePage with Event Validation Pin
Martin Jericho20-Nov-07 1:06
memberMartin Jericho20-Nov-07 1:06 
GeneralRe: PersistentStatePage with Event Validation [modified] Pin
wzychla20-Nov-07 1:26
memberwzychla20-Nov-07 1:26 
GeneralYou saved my night ! Pin
Ralph.Net17-Nov-07 14:53
memberRalph.Net17-Nov-07 14:53 
GeneralRe: You saved my night ! Pin
Martin Jericho17-Nov-07 22:55
memberMartin Jericho17-Nov-07 22:55 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.150731.1 | Last Updated 8 Mar 2006
Article Copyright 2004 by Martin Jericho
Everything else Copyright © CodeProject, 1999-2015
Layout: fixed | fluid