Pages

Wednesday, August 12, 2009

Custom Persister to store the state in a backend database

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
 
   1:  SET ANSI_NULLS ON
   2:  GO
   3:  SET QUOTED_IDENTIFIER ON
   4:  GO
   5:  CREATE TABLE [dbo].[PageViewState](
   6:      [ID] [uniqueidentifier] NOT NULL,
   7:      [Value] [text] NOT NULL,
   8:      [LastUpdatedOn] [datetime] NOT NULL,
   9:   CONSTRAINT [PK_PageViewState] PRIMARY KEY CLUSTERED 
  10:  (
  11:      [ID] ASC
  12:  )WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
  13:  ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
  14:  GO
  15:   
  16:  SET ANSI_NULLS ON
  17:  GO
  18:  SET QUOTED_IDENTIFIER ON
  19:  GO
  20:  CREATE PROCEDURE [dbo].[GetByID] 
  21:      @id uniqueidentifier 
  22:  AS
  23:  BEGIN
  24:      SET NOCOUNT ON;
  25:      
  26:      select Value
  27:      from PageViewState
  28:      where ID = @id
  29:   
  30:  END
  31:   
  32:  GO
  33:  SET ANSI_NULLS ON
  34:  GO
  35:  SET QUOTED_IDENTIFIER ON
  36:  GO
  37:  CREATE PROCEDURE [dbo].[SaveViewState]
  38:      @id uniqueidentifier, 
  39:      @value text
  40:  AS
  41:  BEGIN
  42:      SET NOCOUNT ON;
  43:      
  44:      if (exists(select ID from PageViewState where ID = @id))
  45:          update PageViewState
  46:          set Value = @value, LastUpdatedOn = getdate()
  47:          where ID = @id
  48:      else
  49:          insert into PageViewState
  50:          (ID, Value, LastUpdatedOn)
  51:          values (@id, @value, getdate())
  52:   
  53:  END
  54:   
  55:   
 
 
CleanupPageViewState.sql
 
   1:  delete from PageViewState
   2:  where LastUpdatedOn < dateadd(minute, -240, current_timestamp)

 


Original post can be found here