A while ago, several colleges of mine were having a terrible time with the loading times of some pages of their web application. Come to find out, they were suffering from ViewState bloat. ViewState was something I always tried to stay away from in the past because of this factor. Sure, it was very helpful but I found ways around it. However, I have come to a change of heart to realize ViewState is a wonderful thing, except that it can still cause pages to suffer from the ViewState bloat symptom. You could be a frugal programmer and control the ViewState more by only allowing certain pieces into but there are times when you want to store large amounts of data in the ViewState. For example, you may have search results that were very costly to find and you need to persist the results for paging. So, I started looking at different devices to store the ViewState in. I thought of putting it into our session state which is stored in a SQL database, but the timeout settings for session and page view state were very different in some cases and this method would not allow for the flexibility that is required. Or perhaps someone avoids session state like the plague or it isn't stored in a central repository in case of a web farm approach. The decision was easy, I would create a small table that stores ViewState in it much like the session state. I actually put the table and procedures in the same database as our session state just for consistency. It was also easy to script the job that cleans up timed out ViewState since we already had one for the session state. Here is the install script you will need to run to setup the table, stored procedures, and job to clean up the table every so often. USE ASPState GO DECLARE @jobID BINARY(16) SELECT @jobID = job_id FROM msdb.dbo.sysjobs WHERE name = N'ASPState_Job_ExpireViewState' IF (@JobID IS NOT NULL) BEGIN IF EXISTS(SELECT * FROM msdb.dbo.sysjobservers WHERE job_id = @JobID AND server_id <> 0) BEGIN RAISERROR (N'Unable to import job ''ASPNET_Job_ExpireViewState'' since there is already a multi-server job with this name.', 16, 1) END ELSE EXECUTE msdb.dbo.sp_delete_job @job_name = N'ASPState_Job_ExpireViewState' END GO IF OBJECTPROPERTY(OBJECT_ID(N'dbo.ExpireViewState'), N'IsProcedure') = 1 DROP PROCEDURE dbo.ExpireViewState IF OBJECTPROPERTY(OBJECT_ID(N'dbo.SetViewState'), N'IsProcedure') = 1 DROP PROCEDURE dbo.SetViewState IF OBJECTPROPERTY(OBJECT_ID(N'dbo.GetViewState'), N'IsProcedure') = 1 DROP PROCEDURE dbo.GetViewState IF OBJECTPROPERTY(OBJECT_ID(N'dbo.ViewState'), N'IsUserTable') = 1 DROP TABLE dbo.ViewState GO CREATE TABLE dbo.ViewState ( ViewStateId UNIQUEIDENTIFIER NOT NULL, Value IMAGE NOT NULL, LastAccessed DATETIME NOT NULL, Timeout INT NOT NULL CONSTRAINT CK_ViewState_Timeout CHECK(Timeout > 0), CONSTRAINT PK_ViewState PRIMARY KEY CLUSTERED (ViewStateId), ) GO CREATE PROCEDURE dbo.GetViewState (@viewStateId UNIQUEIDENTIFIER) AS SET NOCOUNT ON DECLARE @textPtr VARBINARY(16) DECLARE @length INT UPDATE dbo.ViewState SET LastAccessed = GETUTCDATE(), @textPtr = TEXTPTR(Value), @length = DATALENGTH(Value) WHERE ViewStateId = @viewStateId IF @length IS NOT NULL BEGIN SELECT @length AS Length READTEXT ViewState.Value @textPtr 0 @length END RETURN 0 GO CREATE PROCEDURE dbo.SetViewState (@viewStateId UNIQUEIDENTIFIER, @value IMAGE, @timeout INT = 20) AS SET NOCOUNT ON IF @viewStateId IS NULL BEGIN RETURN -1 END ELSE IF @timeout < 1 BEGIN RETURN -2 END ELSE IF @value IS NULL BEGIN RETURN -3 END IF EXISTS(SELECT * FROM ViewState WHERE ViewStateId = @viewStateID) BEGIN UPDATE dbo.ViewState SET LastAccessed = GETUTCDATE() ,Value = @value WHERE ViewStateID = @viewStateId END ELSE BEGIN INSERT INTO dbo.ViewState (ViewStateId, Value, DateLastAccessed, Timeout) VALUES (@viewStateId, @value, GETUTCDATE(), @timeout) END RETURN 0 GO CREATE PROCEDURE dbo.ExpireViewState AS SET NOCOUNT ON DELETE FROM dbo.ViewState WHERE GETUTCDATE() > DATEADD(minute, Timeout, LastAccessed) GO GRANT EXECUTE ON dbo.GetViewState TO [ASPNET] GRANT EXECUTE ON dbo.SetViewState TO [ASPNET] GO BEGIN TRANSACTION DECLARE @jobID BINARY(16) DECLARE @returnCode INT SET @returnCode = 0 IF NOT EXISTS(SELECT * FROM msdb.dbo.syscategories WHERE name = N'[Uncategorized (Local)]') EXEC msdb.dbo.sp_add_category @name=N'[Uncategorized (Local)]' EXECUTE @returnCode = msdb.dbo.sp_add_job @job_id = @jobID OUTPUT, @job_name = N'ASPState_Job_ExpireViewState', @owner_login_name = NULL, @description = N'Deletes expired view state information.', @category_name = N'[Uncategorized (Local)]', @enabled = 1, @notify_level_email = 0, @notify_level_page = 0, @notify_level_netsend = 0, @notify_level_eventlog = 0, @delete_level = 0 IF @@ERROR <> 0 OR @returnCode <> 0 GOTO QuitWithRollback EXECUTE @returnCode = msdb.dbo.sp_add_jobstep @job_id = @jobID, @step_id = 1, @step_name = N'ASPState_JobStep_ExpireViewState', @command = N'EXECUTE ExpireViewState', @database_name = N'ASPState', @server = N'', @database_user_name = N'', @subsystem = N'TSQL', @cmdexec_success_code = 0, @flags = 0, @retry_attempts = 0, @retry_interval = 1, @output_file_name = N'', @on_success_step_id = 0, @on_success_action = 1, @on_fail_step_id = 0, @on_fail_action = 2 IF @@ERROR <> 0 OR @ReturnCode <> 0 GOTO QuitWithRollback EXECUTE @returnCode = msdb.dbo.sp_update_job @job_id = @jobID, @start_step_id = 1 IF @@ERROR <> 0 OR @ReturnCode <> 0 GOTO QuitWithRollback EXECUTE @returnCode = msdb.dbo.sp_add_jobschedule @job_id = @jobID, @name = N'ASPState_JobSchedule_ExpireViewState', @enabled = 1, @freq_type = 4, @active_start_date = 20001016, @active_start_time = 0, @freq_interval = 1, @freq_subday_type = 4, @freq_subday_interval = 1, @freq_relative_interval = 0, @freq_recurrence_factor = 0, @active_end_date = 99991231, @active_end_time = 235959 IF @@ERROR <> 0 OR @ReturnCode <> 0 GOTO QuitWithRollback EXECUTE @returnCode = msdb.dbo.sp_add_jobserver @job_id = @jobID, @server_name = N'(local)' IF @@ERROR <> 0 OR @ReturnCode <> 0 GOTO QuitWithRollback COMMIT TRANSACTION GOTO EndSave QuitWithRollback: IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION EndSave: GO To keep it simple I simply look for one value, the connection string to use in an appSetting key called “ViewStateConnectionString“. Optionally, you can define a “ViewStateTimeout“ with a value being the number of minutes to wait before clearing the ViewState data. You can also set this value per page if you have certain pages that will live longer on the client before a post back. If the connection string is not provided, it will not affect the ViewState processing as it will resort to the old method. The following is an example of the appSettings section. <appSettings> <add key="ViewStateConnectionString" value="Server=(local);Database=ASPState;Trusted_Connection=yes;"/> <add key="ViewStateTimeout" value="20"/> </appSettings> Here is the class that you will need to inherit from if you want to take advantage of this ViewState change, provided for you in the wonderful language of C#. I apologize for the lack of comments, hopefully it is self explanatory. using System; using System.Configuration; using System.Data; using System.Data.SqlClient; using System.Globalization; using System.IO; using System.Web; using System.Web.UI; using System.Web.UI.HtmlControls; public class SqlViewStatePage : Page { private const int DefaultViewStateTimeout = 20; private string _viewStateConnectionString; private TimeSpan _viewStateTimeout; public SqlViewStatePage() : base() { if (this.IsDesignMode) return; this._viewStateConnectionString = ConfigurationSettings.AppSettings["ViewStateConnectionString"]; try { this._viewStateTimeout = TimeSpan.FromMinutes(Convert.ToDouble(ConfigurationSettings.AppSettings["ViewStateTimeout"])); } catch { this._viewStateTimeout = TimeSpan.FromMinutes(SqlViewStatePage.DefaultViewStateTimeout); } } protected bool IsDesignMode { get { return (this.Context == null); } } protected bool IsSqlViewStateEnabled { get { return (this._viewStateConnectionString != null && this._viewStateConnectionString.Length > 0); } } public TimeSpan ViewStateTimeout { get { return this._viewStateTimeout; } set { this._viewStateTimeout = value; } } private string GetMacKeyModifier() { int value = this.TemplateSourceDirectory.GetHashCode() + this.GetType().Name.GetHashCode(); if (this.ViewStateUserKey != null) return string.Concat(value.ToString(NumberFormatInfo.InvariantInfo), this.ViewStateUserKey); return value.ToString(NumberFormatInfo.InvariantInfo); } private LosFormatter GetLosFormatter() { if (this.EnableViewStateMac) return new LosFormatter(true, this.GetMacKeyModifier()); return new LosFormatter(); } private Guid GetViewStateGuid() { string viewStateKey; viewStateKey = this.Request.Form["__VIEWSTATEGUID"]; if (viewStateKey == null || viewStateKey.Length < 1) { viewStateKey = this.Request.QueryString["__VIEWSTATEGUID"]; if (viewStateKey == null || viewStateKey.Length < 1) return Guid.NewGuid(); } try { return new Guid(viewStateKey); } catch (FormatException) { return Guid.NewGuid(); } } protected override object LoadPageStateFromPersistenceMedium() { Guid viewStateGuid; byte[] rawData; if (this.IsDesignMode) return null; if (!this.IsSqlViewStateEnabled) return base.LoadPageStateFromPersistenceMedium(); viewStateGuid = this.GetViewStateGuid(); rawData = null; using (SqlConnection connection = new SqlConnection(this._viewStateConnectionString)) using (SqlCommand command = new SqlCommand("GetViewState", connection)) { command.CommandType = CommandType.StoredProcedure; command.Parameters.Add("@returnValue", SqlDbType.Int).Direction = ParameterDirection.ReturnValue; command.Parameters.Add("@viewStateId", SqlDbType.UniqueIdentifier).Value = viewStateGuid; connection.Open(); using(SqlDataReader reader = command.ExecuteReader()) { if (reader.Read()) rawData = (byte[])Array.CreateInstance(typeof(byte), reader.GetInt32(0)); if (reader.NextResult() && reader.Read()) reader.GetBytes(0, 0, rawData, 0, rawData.Length); } } using (MemoryStream stream = new MemoryStream(rawData)) return this.GetLosFormatter().Deserialize(stream); } protected override void SavePageStateToPersistenceMedium(object viewState) { Guid viewStateGuid; HtmlInputHidden control; if (this.IsDesignMode) return; if (!this.IsSqlViewStateEnabled) { base.SavePageStateToPersistenceMedium(viewState); return; } viewStateGuid = this.GetViewStateGuid(); using (MemoryStream stream = new MemoryStream()) { this.GetLosFormatter().Serialize(stream, viewState); using (SqlConnection connection = new SqlConnection(this._viewStateConnectionString)) using (SqlCommand command = new SqlCommand("SetViewState", connection)) { command.CommandType = CommandType.StoredProcedure; command.Parameters.Add("@returnValue", SqlDbType.Int).Direction = ParameterDirection.ReturnValue; command.Parameters.Add("@viewStateId", SqlDbType.UniqueIdentifier).Value = viewStateGuid; command.Parameters.Add("@value", SqlDbType.Image).Value = stream.ToArray(); command.Parameters.Add("@timeout", SqlDbType.Int).Value = this._viewStateTimeout.Ticks / TimeSpan.TicksPerMinute; connection.Open(); command.ExecuteNonQuery(); } } control = this.FindControl("__VIEWSTATEGUID") as HtmlInputHidden; if (control == null) this.RegisterHiddenField("__VIEWSTATEGUID", viewStateGuid.ToString()); else control.Value = viewStateGuid.ToString(); } } |