Click here to Skip to main content
15,893,923 members
Articles / Web Development / ASP.NET

Persisting the state of a web page

Rate me:
Please Sign up or sign in to vote.
4.74/5 (89 votes)
7 Mar 200613 min read 446.6K   3.4K   225  
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.
using System;
using System.Collections;
using System.Collections.Specialized;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Runtime.Serialization;
using System.IO;

namespace X.Web.UI {
	/// <summary>
	/// To use this class, simply call the RedirectSavingPageState(string url) method instead of
	/// Response.Redirect to call a page and have this page keep its state.
	/// You must also replace Request.Form with PostData throughout the existing code in your page.
	/// 
	/// The substitution of Request.Form in existing code is necessary because the .NET framework doesn't
	/// let us replace or modify the Request.Form collection.
	/// </summary>
	public class PersistentStatePage : System.Web.UI.Page {
		/// <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;

		/// <summary>
		/// Constructor
		/// </summary>
    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();
			if (!IsRestoredPageState) return normalReturnObject; // default to normal behaviour if there is no persisted pagestate.
			if (normalReturnObject!=null) {
				// this is a normal postback, so don't use persisted page state
				pageState=null;
				RemoveSavedPageState(Request.Url); // clear the page state from the persistence medium so it is not used again
				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);
			}
			RemoveSavedPageState(Request.Url); // clear the page state from the persistence medium so it is not used again
			// This method must return something other than null, otherwise the framework does not call
			// the LoadPageStateFromPersistenceMedium() method and IsPostBack will return false.
			// Request.Form is an empty object of the correct type to achieve this.
			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() {
			if (!IsRestoredPageState) return base.LoadPageStateFromPersistenceMedium(); // default to normal behaviour if we don't want to restore the persisted page state
			return pageState.ViewStateObject; // otherwise return the ViewStateObject contained in the persisted pageState
		}

		/// <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.
			// http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpguide/html/cpconreceivingpostbackdatachangednotifications.asp
			// 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;
		}
	}

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

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

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


Written By
Australia Australia
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions