Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Persisting the state of a web page

0.00/5 (No votes)
7 Mar 2006 1  
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:

/// <summary>

/// This method performs depth-first recursion on 

/// all controls contained in the specified control,

/// calling the framework's LoadPostData on each and 

/// adding those modified to the modifiedControls list.

/// </summary>

private void LoadPostData(Control control, 
                        ArrayList modifiedControls) {
    // Perform recursion of child controls:

    foreach (Control childControl in control.Controls)
        LoadPostData(childControl,modifiedControls);
    // Load the post data for this control:

    if (control is IPostBackDataHandler) {
        // Get the value of the control's name attribute, 

        // which is the GroupName of radio buttons,

        // or the same as the UniqueID 

        // attribute for all other controls:

        string nameAttribute=(control is RadioButton) ? 
            ((RadioButton)control).GroupName : control.UniqueID;
        if (control is CheckBoxList) {
            // CheckBoxLists also require special handling:

            int i=0;
            foreach (ListItem listItem in ((ListControl)control).Items)
                if (PostData[nameAttribute+':'+(i++)]!=null) {
                    listItem.Selected=true;
                    modifiedControls.Add(control);
                }
        } else {
            // Don't process this control if its key

            // isn't in the PostData, as the

            // LoadPostData implementation of some controls

            // throws an exception in this case.

            if (PostData[nameAttribute]==null) return;
            // Call the framework's LoadPostData on this control

            // using the name attribute as the post data key:

            if (((IPostBackDataHandler)control).LoadPostData(
                                               nameAttribute,PostData))
                modifiedControls.Add(control);
        }
    }
}

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:

/// <summary>

/// The persisted PageState.

/// This is non-null if a postback 

/// is being emulated from the

/// persisted PageState.

/// </summary>

private PageState pageState=null;

/// <summary>

/// Contains the URL to redirect to

/// </summary>

private string redirectSavingPageStateURL=null;


/// 

/// Constructor

/// 

public PersistentStatePage() {
    // The following statement must be uncommented 

    // if running under .NET 2.0 to avoid an

    // "Invalid postback or callback argument" exception

    // when restoring the page state.

    //EnableEventValidation=false;

}

/// <summary>

/// Indicates whether the current 

/// page is being or has been

/// restored from a persisted state

/// </summary>

public bool IsRestoredPageState {
    get {
        return pageState!=null;
    }
}

/// <summary>

/// Returns the post data from the 

/// persisted PageState if it exists,

/// otherwise the actual post 

/// data from Request.Form

/// </summary>

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

/// <summary>

/// Returns the data passed back 

/// from the sub-page which 

/// can be used to make changes 

/// to the saved page state.

/// This data is passed back via the query string.

/// </summary>

public NameValueCollection PassBackData {
  get {
    return IsRestoredPageState ? 
            pageState.PassBackData : null;
  }
}

/// <summary>

/// Call this method instead of Response.Redirect(url) 

/// to cause this page to restore its current state when 

/// it is next displayed.

/// </summary>

public void RedirectSavingPageState(string url) {
    redirectSavingPageStateURL=url;
}

/// <summary>

/// Call this method to redirect 

/// to the specified relative URL,

/// specifying whether to restore the page's saved state

/// (assuming its state was saved when it was last shown).

/// The specified URL is relative 

/// to that of the current request.

/// This method is usually called from a "sub-page", 

/// which doesn't extend this class. 

/// </summary>

public static void RedirectToSavedPage(string url, 
                                bool restorePageState) {
    if (!restorePageState) RemoveSavedPageState(url);
    HttpContext.Current.Response.Redirect(url);
}

/// <summary>

/// Call this method to clear the saved 

/// PageState of the page with the

/// specified relative URL. This ensures 

/// that the next redirect to the

/// specified page will not revert to the saved state.

/// The specified URL is relative 

/// to that of the current request.

/// </summary>

public static void RemoveSavedPageState(string url) {
  RemoveSavedPageState(new Uri(HttpContext.Current.Request.Url,
                                                          url));
}

/// <summary>

/// This method is called by the 

/// framework after the Init event to

/// determine whether a postback is 

/// being performed. The Page.IsPostBack

/// property returns true iff this 

/// method returns a non-null.

/// </summary>

protected override NameValueCollection DeterminePostBackMode() {
    pageState=LoadPageState(Request.Url);
    NameValueCollection normalReturnObject=
                   base.DeterminePostBackMode();
    // default to normal behaviour if 

    // there is no persisted pagestate:

    if (!IsRestoredPageState) 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.

    // Save PassBackData if we have not already done so:

    if (pageState.PassBackData==null) {
        pageState.PassBackData=Request.QueryString;
        // call SavePageState again in 

        // case the change we just made

        // is not persisted purely in memory:

        SavePageState(pageState.Url,pageState);
    }
    // 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;
}

/// <summary>

/// This method is called by the framework 

/// after DeterminePostBackMode(),

/// but before custom event handling. 

/// It returns the view state that the

/// framework uses to restore the state of the controls.

/// </summary>

protected override object LoadPageStateFromPersistenceMedium() {
    // default to normal behaviour if we don't want to

    // restore the persisted page state:

    if (!IsRestoredPageState) 
        return base.LoadPageStateFromPersistenceMedium();
    // otherwise, return the ViewStateObject

    // contained in the persisted pageState:

    return pageState.ViewStateObject;
}

/// <summary>

/// This method is called by the framework after

/// LoadPageStateFromPersistenceMedium() 

/// to raise the Load event.

/// Controls are populated with data from 

/// PostData here because it has to

/// happen after the framework has loaded 

/// the view state, which happens after

/// the execution of the LoadPageStateFromPersistenceMedium() 

/// method.

/// </summary>

override protected void OnLoad(EventArgs e) {
    // The following code is meant to emulate 

    // what ASP.NET does "automagically"

    // for us when it populates the controls 

    // with post data before processing

    // the events. The difference is that 

    // this one populates them with our

    // persisted post data instead of the 

    // actual post data from Request.Form.

    if (IsRestoredPageState) {
        // 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();
    }
    base.OnLoad(e);
}

/// <summary>

/// This method performs depth-first recursion on all 

/// controls contained in the specified control,

/// calling the framework's LoadPostData on each and 

/// adding those modified to the modifiedControls list.

/// </summary>

private void LoadPostData(Control control, 
                          ArrayList modifiedControls) {
    // Perform recursion of child controls:

    foreach (Control childControl in control.Controls)
        LoadPostData(childControl,modifiedControls);
    // Load the post data for this control:

    if (control is IPostBackDataHandler) {
        // Get the value of the control's name attribute, 

        // which is the GroupName of radio buttons, or the same as

        // the UniqueID attribute for all other controls:

        string nameAttribute=(control is RadioButton) ? 
              ((RadioButton)control).GroupName : control.UniqueID;
        if (control is CheckBoxList) {
            // CheckBoxLists also require special handling:

            int i=0;
            foreach (ListItem listItem in ((ListControl)control).Items)
                if (PostData[nameAttribute+':'+(i++)]!=null) {
                    listItem.Selected=true;
                    modifiedControls.Add(control);
                }
        } else {
            // Don't process this control if its key

            // isn't in the PostData, as the

            // LoadPostData implementation of some controls

            // throws an exception in this case.

            if (PostData[nameAttribute]==null) return;
            // Call the framework's LoadPostData on this control

            // using the name attribute as the post data key:

            if (((IPostBackDataHandler)control).LoadPostData(
                                               nameAttribute,PostData))
                modifiedControls.Add(control);
        }
    }
}

/// <summary>

/// This method is called by the framework 

/// between the PreRender and Render events.

/// It is only called if this page is to be 

/// redisplayed, not if Response.Redirect

/// has been called. To ensure it is called 

/// before we redirect, we must postpone

/// the Response.Redirect call until now.

/// </summary>

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);
    }
}

/// <summary>

/// Override this method to load the 

/// state from a persistence medium

/// other than the Session object.

/// </summary>

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

/// <summary>

/// Override this method to save the state to a persistence medium

/// other than the Session object.

/// </summary>

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

/// <summary>

/// Override this method to remove 

/// the state from a persistence medium

/// other than the Session object.

/// </summary>

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

/// <summary>

/// Returns a key which will uniquely identify a page in a

/// global namespace based on its URL.

/// </summary>

private static string GetPageStateKey(Uri pageURL) {
    return "_PAGE_STATE_"+pageURL.AbsolutePath;
}

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:

/// <summary>

/// This is the object stored in the persistence medium,

/// containing the view state, post data, and URL.

/// </summary>

[Serializable]
public class PageState {
    private SerializableViewState serializableViewState;
    public NameValueCollection PostData;
    public Uri Url;

    // PassBackData is used to store data that is 

    // passed back to the parent page from the sub-page:

    public NameValueCollection PassBackData;

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

    public object ViewStateObject {
        get {
            return serializableViewState.ViewStateObject;
        }
    }
}

/// <summary>

/// This is a simple wrapper around the view state object

/// to make it serializable.

/// </summary>

[Serializable]
public class SerializableViewState : ISerializable {
    public object ViewStateObject;

    private const string ViewStateStringKey="ViewStateString";
    
    public SerializableViewState(object viewStateObject) {
        ViewStateObject=viewStateObject;
    }

    public SerializableViewState(SerializationInfo info, 
                                 StreamingContext context) {
        ViewStateObject=new LosFormatter().Deserialize(
                         info.GetString(ViewStateStringKey));
    }
    
    public void GetObjectData(SerializationInfo info, 
                              StreamingContext context) {
        StringWriter stringWriter=new StringWriter();
        new LosFormatter().Serialize(stringWriter,
                                    ViewStateObject);
        info.AddValue(ViewStateStringKey,
                            stringWriter.ToString());
    }
}

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