locked
Store ViewState in the Database instead of Hidden Form Field RRS feed

  • Question

  • User1567897397 posted

    As requested in this post, following is an example of a way to store viewstate in a database on the server instead of the hidden form field on the page. The form only maintains a guid that is used to retrieve saved viewstate from the DB. This can reduce bandwidth considerably if viewstate is something that you can't or don't want to disable completely.

    The following code snippets are included:

    • SamplePageStatePersister.cs - custom mechanism for loading and saving viewstate for the page.
    • SamplePageAdapter.cs - returns a new instance of SamplePageStatePersister (this is enlisted by the App.Browser file).
    • App.Browser - must be added to the web project and tells the web application to use our custom page adapter.
    • PageViewStateServices.cs - handles the interaction with the database via stored procedures to optimise performance.
    • PageViewState.sql - creates the table to hold the view state and the stored procedures to interact with the database. You will need to apply the appropriate permissions.
    • CleanupPageViewState.sql - deletes old view state to stop the database growing uncontrollably. Configure as a scheduled job and can be modified to extend or reduce the time window for view state being maintained in the database. Defaults to 4 hours.

    NOTE: This current example will not work if you use Server.Transfer and carry across the form data in the transfer. In this case, the view state ID will be carried across too and then subsequent back navigation will result in an error.

     

    SamplePageStatePersister.cs

    using System;
    using System.Globalization;
    using System.IO;
    using System.Data;
    using System.Configuration;
    using System.Web;
    using System.Web.Security;
    using System.Web.UI;
    using System.Web.UI.WebControls;
    using System.Web.UI.WebControls.WebParts;
    using System.Web.UI.HtmlControls;

    /// <summary>
    /// -----------------------------------------------------------------------------------------------------------------------------------------------------------------
    /// This class is the page state persister for the application.
    ///
    /// Created by Jason Hill on 26/6/2007.
    /// -----------------------------------------------------------------------------------------------------------------------------------------------------------------
    /// </summary>

    public class SamplePageStatePersister : System.Web.UI.PageStatePersister
    {

        private Page _page;

        /// -----------------------------------------------------------------------------------------------------------------------------------------------------------------
        /// <summary>
        /// Constructor.
        /// </summary>
        /// <remarks>
        /// <author>jhill</author>
        /// <creation>Wednesday, 30 May 2007</creation>
        /// </remarks>
        /// <param name="page">Page.</param>
        /// -----------------------------------------------------------------------------------------------------------------------------------------------------------------

        public SamplePageStatePersister(Page page)
            : base(page)
        {
            _page = page;
        }

        /// -----------------------------------------------------------------------------------------------------------------------------------------------------------------
        /// <summary>
        /// Get the unique ID for the view state.
        /// </summary>
        /// <remarks>
        /// <author>jhill</author>
        /// <creation>Wednesday, 30 May 2007</creation>
        /// </remarks>
        /// -----------------------------------------------------------------------------------------------------------------------------------------------------------------

        private Guid GetViewStateID()
        {
            string viewStateKey;

            // Get the ID from the request
            viewStateKey = _page.Request["__VIEWSTATEID"];

            // Assign a new ID if we don't have one in the request
            if (string.IsNullOrEmpty(viewStateKey))
            {
                return Guid.NewGuid();
            }

            // Use the ID from the request if it is valid, else assign a new ID
            try
            {
                return new Guid(viewStateKey);
            }
            catch (FormatException)
            {
                return Guid.NewGuid();
            }

        }

        /// -----------------------------------------------------------------------------------------------------------------------------------------------------------------
        /// <summary>
        /// Load the view state from persistent medium.
        /// </summary>
        /// <remarks>
        /// <author>jhill</author>
        /// <creation>Wednesday, 30 May 2007</creation>
        /// </remarks>
        /// -----------------------------------------------------------------------------------------------------------------------------------------------------------------

        public override void Load()
        {

            // Load view state from DB
            string pageViewState = PageViewStateServices.GetByID(GetViewStateID());

            if (pageViewState == null)
            {
                ViewState = null;
                ControlState = null;
            }
            else
            {

                // Deserialize into a Pair of ViewState and ControlState objects
                IStateFormatter formatter = StateFormatter;
                Pair statePair = (Pair)formatter.Deserialize(pageViewState);

                // Update ViewState and ControlState
                ViewState = statePair.First;
                ControlState = statePair.Second;
            }

        }

        /// -----------------------------------------------------------------------------------------------------------------------------------------------------------------
        /// <summary>
        /// Save the view state to persistent medium.
        /// </summary>
        /// <remarks>
        /// <author>jhill</author>
        /// <creation>Wednesday, 30 May 2007</creation>
        /// </remarks>
        /// <param name="viewState">View state to save.</param>
        /// -----------------------------------------------------------------------------------------------------------------------------------------------------------------

        public override void Save()
        {

            // Create a pair for ViewState and ControlState
            Pair statePair = new Pair(ViewState, ControlState);
            IStateFormatter formatter = StateFormatter;

            // Save the view state
            Guid id = GetViewStateID();
            PageViewStateServices.Save(id, formatter.Serialize(statePair));

            // Store the ID of the view state in a hidden form field
            HtmlInputHidden control = _page.FindControl("__VIEWSTATEID") as HtmlInputHidden;
            if (control == null)
            {
                ScriptManager.RegisterHiddenField(_page, "__VIEWSTATEID", id.ToString());
            }
            else
            {
                control.Value = id.ToString();
            }

        }


    }

     
    SamplePageAdapter.cs
    using System;
    using System.Data;
    using System.Configuration;
    using System.Web;
    using System.Web.Security;
    using System.Web.UI;
    using System.Web.UI.WebControls;
    using System.Web.UI.WebControls.WebParts;
    using System.Web.UI.HtmlControls;
    
    /// <summary>
    /// -----------------------------------------------------------------------------------------------------------------------------------------------------------------
    /// This class is the page adapter for the application.
    /// 
    /// Created by Jason Hill on 26/6/2007.
    /// -----------------------------------------------------------------------------------------------------------------------------------------------------------------
    /// </summary>
    public class SamplePageAdapter : System.Web.UI.Adapters.PageAdapter
    {
    
        /// -----------------------------------------------------------------------------------------------------------------------------------------------------------------
        /// <summary>
        /// Gets the state persister for the page.
        /// </summary>
        /// <remarks>
        /// 	<author>jhill</author>
        /// 	<creation>Wednesday, 30 May 2007</creation>
        /// </remarks>
        /// -----------------------------------------------------------------------------------------------------------------------------------------------------------------
        public override PageStatePersister GetStatePersister()
        {
            return new SamplePageStatePersister(Page);
        }
    
    }
    
     
     
     
    App.Browser
     
    <browsers>
      <browser refID="Default">
        <controlAdapters>
          <adapter controlType="System.Web.UI.Page" adapterType="SamplePageAdapter" />
        </controlAdapters>
      </browser>
    </browsers>
    
     
     
     
     
    PageViewStateServices.cs
     
    using System;
    using System.Data;
    using System.Data.SqlClient;
    using System.Collections;
    using System.Text;
    
    /// <summary>
    /// -----------------------------------------------------------------------------------------------------------------------------------------------------------------
    /// This class provides services for handling page view state.
    /// 
    /// Created by Jason Hill on 26/6/2007.
    /// -----------------------------------------------------------------------------------------------------------------------------------------------------------------
    /// </summary>
    public static class PageViewStateServices
    {
    
        /// -----------------------------------------------------------------------------------------------------------------------------------------------------------------
        /// <summary>
        /// Get a page view state by ID.
        /// </summary>
        /// <remarks>
        /// 	<author>jhill</author>
        /// 	<creation>Wednesday, 30 May 2007</creation>
        /// </remarks>
        /// <param name="id">ID.</param>
        /// -----------------------------------------------------------------------------------------------------------------------------------------------------------------
        public static string GetByID(Guid id)
        {
    
            using (SqlConnection connection = new SqlConnection(Common.PageViewStateConnectionString))
            {
                connection.Open();
    
                try
                {
                    using (SqlCommand command = connection.CreateCommand())
                    {
                        command.CommandType = CommandType.StoredProcedure;
                        command.CommandText = "GetByID";
                        command.Parameters.Add(new SqlParameter("@id", id));
                        return (string)command.ExecuteScalar();
                    }
                }
    
                finally
                {
                    connection.Close();
                }
            }
    
        }
    
        /// -----------------------------------------------------------------------------------------------------------------------------------------------------------------
        /// <summary>
        /// Save the view state.
        /// </summary>
        /// <remarks>
        /// 	<author>jhill</author>
        /// 	<creation>Wednesday, 30 May 2007</creation>
        /// </remarks>
        /// <param name="id">Unique ID.</param>
        /// <param name="value">View state value.</param>
        /// -----------------------------------------------------------------------------------------------------------------------------------------------------------------
        public static void Save(Guid id, string value)
        {
    
            using (SqlConnection connection = new SqlConnection(Common.PageViewStateConnectionString))
            {
                connection.Open();
    
                try
                {
    
                    using (SqlCommand command = connection.CreateCommand())
                    {
                        command.CommandType = CommandType.StoredProcedure;
                        command.CommandText = "SaveViewState";
                        command.Parameters.Add(new SqlParameter("@id", id));
                        command.Parameters.Add(new SqlParameter("@value", value));
                        command.ExecuteNonQuery();
                    }
    
                }
    
                finally
                {
                    connection.Close();
                }
            }
    
        }
    
    }
    
     
     
     
    PageViewState.sql
     
    SET ANSI_NULLS ON
    GO
    SET QUOTED_IDENTIFIER ON
    GO
    CREATE TABLE [dbo].[PageViewState](
    	[ID] [uniqueidentifier] NOT NULL,
    	[Value] [text] NOT NULL,
    	[LastUpdatedOn] [datetime] NOT NULL,
     CONSTRAINT [PK_PageViewState] PRIMARY KEY CLUSTERED 
    (
    	[ID] ASC
    )WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
    ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
    GO
    
    SET ANSI_NULLS ON
    GO
    SET QUOTED_IDENTIFIER ON
    GO
    CREATE PROCEDURE [dbo].[GetByID] 
    	@id uniqueidentifier 
    AS
    BEGIN
    	SET NOCOUNT ON;
    	
    	select Value
    	from PageViewState
    	where ID = @id
    
    END
    
    GO
    SET ANSI_NULLS ON
    GO
    SET QUOTED_IDENTIFIER ON
    GO
    CREATE PROCEDURE [dbo].[SaveViewState]
    	@id uniqueidentifier, 
    	@value text
    AS
    BEGIN
    	SET NOCOUNT ON;
    	
    	if (exists(select ID from PageViewState where ID = @id))
    		update PageViewState
    		set Value = @value, LastUpdatedOn = getdate()
    		where ID = @id
    	else
    		insert into PageViewState
    		(ID, Value, LastUpdatedOn)
    		values (@id, @value, getdate())
    
    END
    
     
     
     
    CleanupPageViewState.sql
     
    delete from PageViewState
    where LastUpdatedOn < dateadd(minute, -240, current_timestamp)
    
     
    Monday, July 21, 2008 11:52 PM

All replies

  • User-627724879 posted

    Did you not know you could just change the provider? There is a PageStatePersister class you inherit. The SessionPageStatePersister is the most common one out of the box, but you can create your own. You just set the provider in your web.config file.

    Tuesday, July 22, 2008 2:12 AM
  • User1567897397 posted

    Um...the code I provided is a custom implementation of PageStatePersister.

    Tuesday, July 22, 2008 2:19 AM
  • User-597672045 posted

    Jason, this is wonderful.  Thank you very much.  I am seeing about 80-90% reductions in bytes sent on my pages and about 40% on bytes received.

    A couple of observations.

    GetMacKeyModifier is not used anywhere.

    In one of my applications, I keep getting the same   viewStateKey from _page.Request["__VIEWSTATEID"] no matter what page I was on.   Found out why.  This particular app uses Server.Transfer("to_page.aspx") so it retains the same _VIEWSTATEID.  I am phasing out Server.Transfer though.

    Tuesday, July 22, 2008 3:24 AM
  • User1567897397 posted

    Thanks for the feedback. I have removed the GetMacKeyModifier() and GetLosFormatter() methods as they are redundant. They were used in a previous implementation but have been superceded.

    We don't use Server.Transfer at all so I can't provide much help with that. Did it still work OK or did you encounter problems when using Server.Transfer?

    Tuesday, July 22, 2008 7:51 PM
  • User-597672045 posted

     

    Thanks for the feedback. I have removed the GetMacKeyModifier() and GetLosFormatter() methods as they are redundant. They were used in a previous implementation but have been superceded.

    We don't use Server.Transfer at all so I can't provide much help with that. Did it still work OK or did you encounter problems when using Server.Transfer?

    It works fine.  Because it retains the same id, PageStatePersister keeps updating the same row, so the table for page views does not grow as fast.  I think it's better that way.

     

    Tuesday, July 22, 2008 7:58 PM
  • User-597672045 posted

    Jason, I take back what I said about Server.Transfer retaining the same id being better.

    The problem is, when the user presses the back button, you now have the same viewstate you backed from being applied to your current page, and results in an error.

    Thursday, July 24, 2008 9:17 PM
  • User1567897397 posted

    Yeah...that is what I was expecting to see when you mentioned server.transfer. I haven't used it so don't have much insight. I think you can pass a parameter to indicate whether you want form data carried across. Specifying "false" might fix the problem but would cause problems with your app if you are expecting the form data to be carried over.

    It might be possible to tweak the code so that you generate a new viewstate ID for the page that you have trsansferred to instead of using the one saved in the form data.

    Thursday, July 24, 2008 9:30 PM
  • User-597672045 posted

     The error I get is this

    Validation of viewstate MAC failed. If this application is hosted by a Web Farm or cluster, ensure that <machineKey> configuration specifies the same validationKey and validation algorithm. AutoGenerate cannot be used in a cluster. 

    I am phasing out Response.Redirect.  Server.Transfer does not provide any value to me, and I don't really care much about hiding parameters.

    Also when you go from pageA.aspx to pageB.aspx with Server.Transfer, the url still says PageA.aspx even though you are on pageB.aspx, which is irritating.

    I was told that Server.Transfer was the proper .NET way.

     

    Thursday, July 24, 2008 9:34 PM
  • User-1904880320 posted

    Hello, and thanks for the useful example.  However I have been trying to make it work for a VB.NET project (vs 2008) we are working on.  I had some problems with the constructor of the page persister but it finally compiled.  However when I try to run it, I immediately get an error (no page is loaded first) stating that it cannot load the SamplePageAdapter type and points to the App.Browser file on the line were it is called.

    Can you help???  Have you seen anyone do this in VB so that I can see that example???  I reallly don't know were to begin, since the compilation process goes through without a hitch, so I don't see why it can't load the type.

    Thanks.

    Wednesday, December 23, 2009 5:17 PM
  • User863677091 posted

    I know I'm trying this a few years after you posted this code, but I have a question. Going from page to page seems to work great. But I have a large gridview on one page where all of the rows have to be saved every time the user clicks a save button that is in the footer row of the gridview. For some reason, with this code, my save sub isn't seeing any rows in the gridview on the postback. Do you have any ideas of what I could be doing wrong?

    Thursday, February 18, 2010 1:42 PM