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:
private PageState pageState=null;
protected bool flagToIndicateThatPageIsBeingRedirected=false;
protected override object LoadPageStateFromPersistenceMedium() {
pageState=LoadPageState(Request.Url);
if (IsPostBack || pageState==null) {
pageState=null;
RemoveSavedPageState(Request.Url);
return base.LoadPageStateFromPersistenceMedium();
}
if (pageState.Url.AbsoluteUri!=Request.Url.AbsoluteUri) {
Response.Redirect(pageState.Url.AbsoluteUri,true);
}
RemoveSavedPageState(Request.Url);
Request.Form=pageState.PostData;
return pageState.ViewStateObject;
}
protected override void SavePageStateToPersistenceMedium(
object viewState)
{
if (flagToIndicateThatPageIsBeingRedirected) {
SavePageState(Request.Url,new PageState(viewState,
Request.Form,Request.Url));
} else {
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) {
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:
- The
LoadPageStateFromPersistenceMedium()
method is never called by the ASP.NET Framework if IsPostBack
is false
.
- Because
IsPostBack
still returns false
when we are "emulating" a PostBack, any other code in the page that checks it will not function correctly.
- 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();
if (pageState==null) return normalReturnObject;
if (normalReturnObject!=null) {
pageState=null;
RemoveSavedPageState(Request.Url);
return normalReturnObject;
}
if (pageState.Url.AbsoluteUri!=Request.Url.AbsoluteUri) {
Response.Redirect(pageState.Url.AbsoluteUri,true);
}
RemoveSavedPageState(Request.Url);
return Request.Form;
}
protected override object LoadPageStateFromPersistenceMedium() {
if (pageState==null)
return base.LoadPageStateFromPersistenceMedium();
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:
private string redirectSavingPageStateURL=null;
public void RedirectSavingPageState(string url) {
redirectSavingPageStateURL=url;
}
protected override void SavePageStateToPersistenceMedium(
object viewState)
{
if (redirectSavingPageStateURL==null) {
base.SavePageStateToPersistenceMedium(viewState);
} else {
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:
ArrayList modifiedControls=new ArrayList();
LoadPostData(this,modifiedControls);
foreach (IPostBackDataHandler control in modifiedControls)
control.RaisePostDataChangedEvent();
which uses the following private
method:
private void LoadPostData(Control control,
ArrayList modifiedControls) {
foreach (Control childControl in control.Controls)
LoadPostData(childControl,modifiedControls);
if (control is IPostBackDataHandler) {
string nameAttribute=(control is RadioButton) ?
((RadioButton)control).GroupName : control.UniqueID;
if (control is CheckBoxList) {
int i=0;
foreach (ListItem listItem in ((ListControl)control).Items)
if (PostData[nameAttribute+':'+(i++)]!=null) {
listItem.Selected=true;
modifiedControls.Add(control);
}
} else {
if (PostData[nameAttribute]==null) return;
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:
private PageState pageState=null;
private string redirectSavingPageStateURL=null;
public PersistentStatePage() {
}
public bool IsRestoredPageState {
get {
return pageState!=null;
}
}
public NameValueCollection PostData {
get {
return (IsRestoredPageState) ?
pageState.PostData : Request.Form;
}
}
public NameValueCollection PassBackData {
get {
return IsRestoredPageState ?
pageState.PassBackData : null;
}
}
public void RedirectSavingPageState(string url) {
redirectSavingPageStateURL=url;
}
public static void RedirectToSavedPage(string url,
bool restorePageState) {
if (!restorePageState) RemoveSavedPageState(url);
HttpContext.Current.Response.Redirect(url);
}
public static void RemoveSavedPageState(string url) {
RemoveSavedPageState(new Uri(HttpContext.Current.Request.Url,
url));
}
protected override NameValueCollection DeterminePostBackMode() {
pageState=LoadPageState(Request.Url);
NameValueCollection normalReturnObject=
base.DeterminePostBackMode();
if (!IsRestoredPageState) return normalReturnObject;
if (normalReturnObject!=null) {
pageState=null;
RemoveSavedPageState(Request.Url);
return normalReturnObject;
}
if (pageState.PassBackData==null) {
pageState.PassBackData=Request.QueryString;
SavePageState(pageState.Url,pageState);
}
if (pageState.Url.AbsoluteUri!=Request.Url.AbsoluteUri) {
Response.Redirect(pageState.Url.AbsoluteUri,true);
}
RemoveSavedPageState(Request.Url);
return Request.Form;
}
protected override object LoadPageStateFromPersistenceMedium() {
if (!IsRestoredPageState)
return base.LoadPageStateFromPersistenceMedium();
return pageState.ViewStateObject;
}
override protected void OnLoad(EventArgs e) {
if (IsRestoredPageState) {
ArrayList modifiedControls=new ArrayList();
LoadPostData(this,modifiedControls);
foreach (IPostBackDataHandler control in modifiedControls)
control.RaisePostDataChangedEvent();
}
base.OnLoad(e);
}
private void LoadPostData(Control control,
ArrayList modifiedControls) {
foreach (Control childControl in control.Controls)
LoadPostData(childControl,modifiedControls);
if (control is IPostBackDataHandler) {
string nameAttribute=(control is RadioButton) ?
((RadioButton)control).GroupName : control.UniqueID;
if (control is CheckBoxList) {
int i=0;
foreach (ListItem listItem in ((ListControl)control).Items)
if (PostData[nameAttribute+':'+(i++)]!=null) {
listItem.Selected=true;
modifiedControls.Add(control);
}
} else {
if (PostData[nameAttribute]==null) return;
if (((IPostBackDataHandler)control).LoadPostData(
nameAttribute,PostData))
modifiedControls.Add(control);
}
}
}
protected override void SavePageStateToPersistenceMedium(
object viewState)
{
if (redirectSavingPageStateURL==null) {
base.SavePageStateToPersistenceMedium(viewState);
} else {
SavePageState(Request.Url,
new PageState(viewState,Request.Form,Request.Url));
Response.Redirect(redirectSavingPageStateURL);
}
}
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) {
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:
[Serializable]
public class PageState {
private SerializableViewState serializableViewState;
public NameValueCollection PostData;
public Uri Url;
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;
}
}
}
[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());
}
}
To use the solution as provided in the source download in your existing page, the following changes are required to your code:
- First of all, if you are using .NET 2.0, uncomment the
EnableEventValidation=false
line in the constructor of the PersistentStatePage
class.
- Your page should extend
X.Web.UI.PersistentStatePage
instead of System.Web.UI.Page
, or alternatively copy the code into your own page.
- Use
RedirectSavingPageState(url)
instead of Response.Redirect(url)
whenever you want the current page to restore its current state when it is next displayed.
- 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
- 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.