ASP.NET Viewstate And Controlstate Performance Enhancements - Saving Viewstate And Controlstate On Server

Posted by Ahmed Tarek Hasan on 12/27/2013 03:33:00 AM with No comments
ASP.NET Viewstate & Controlstate

Viewstate and Controlstate are used in ASP.NET pages to keep states of pages and all controls on them between postbacks. ASP.NET framework saves these states in a container to be used throughout postbacks to maintain these states.

Also, you can explicitly add any info you need to keep between postbacks inside the page viewstate. This may seem urging but you must know that this doesn't come free of charge. Yes viewstate and controlstate are very useful and powerful but they have to be used wisely otherwise you will greatly affect your system performance badly and in an unpleasant way.

The viewstate and controlstate are both saved by the server and then retrieved to keep your page and controls state. The default behavior is that these states are saved into a hidden field on the page so that at postbacks the server will be able to read these states back from the hidden field and retrieve the sates prior the postbacks. Is this good?

This article is not meant to be a full guide about viewstate and controlstate, if you need a full guide you can have a look on the references at the end of this article. So, what is this article about?

This article will focus on how to overcome the drawbacks of heavy viewstate and controlstate and enhance the system performance by saving these states on server rather than sending them back and forth between client and server throughout postbacks.

Analysis
To know how viewstate and controlstate work, you need to know some points in brief:
  1. HTTP is stateless which means that it doesn't support by itself saving the states of requests and responses. That is why each web development platform should handle the states by its own way and system when needed
  2. For ASP.NET, when a request is initiated, the server process the request and builds the whole page and sends it back to the client. At this moment, the server forgets about the whole page object and all info related to the request. This is what is meant by stateless
  3. ASP.NET has its own way of saving the page states. It exposes some methods/events by which you can control how the page state will be saved and then retrieved, but if you didn't override these methods and provide your own implementation, there is always a default behavior which ASP.NET will use to save the states
  4. The default behavior for ASP.NET to save page states is saving/loading them into/from a hidden field on the page
  5. The states the server tends to save are the states of all the page controls before being sent back to the client. At the successive requests, the server can now retrieve these sates to know how the page looked like before the system user applied some changes on it at the client side 
But why the hassle?
I asked myself before why at every request the server needs to know the states on which the page was before the last response, does it really matter? As far as I know when a request is performed the form will be submitted to the server and the server will have all the info required to re-create and re-populate the form fields in the response, so for God's sake why????

Misunderstood about viewstate
Some developers think that viewstate is used to keep values and states of page controls so that the server is able to populate these values and states after postbacks. This is wrong. Believe me even if you disabled the viewstate on a page and its controls the values you entered in the controls will still exist after postbacks. You don't believe me, try it yourself.

Create a web application, add a page and disable viewstate on it, add a server textbox control and make it run at server, add a server button control and make it run at server. Now start the application and enter some text inside the textbox and click on the button. A postback will be performed and the textbox will be populated with the text you enterd yourself before the postback. How? this happened because when you clicked the button the whole form is submitted including the text you entered inside the textbox. So, the server didn't need anything to know the value you entered as it was already sent to it with the request. That's why I told you before viewstate is not responsible for keeping controls values and states.

Believe it or not, one of the main purposes of the viewstate is to track changes made on a page controls. Why keep track of changes? to be able to properly fire events like "ontextchanged" which are based on tracking changes made on a control to properly apply your custom code for handling such situations. Still not convinced? If yes, try the following example.

The proof
Try this:
  1. Create a web application
  2. Add a page and enable viewstate on it
  3. Add the following markup inside the form tag
    <asp:TextBox ID="box" runat="server" Text="" EnableViewState="true" ontextchanged="box_TextChanged"></asp:TextBox>
    <asp:Button ID="btn" runat="server" Text="Do Postback" onclick="btn_Click" />
    
  4. Write this code on the code behind inside the page class
    protected void btn_Click(object sender, EventArgs e)
    {
    }
    
    protected void box_TextChanged(object sender, EventArgs e)
    {
    }
    
  5. Put a breakpoint on the "box_TextChanged" event
  6. Run the application in debug mode
  7. Write "Test Test" in the textbox
  8. Click "Do Postback" button
  9. You will reach the breakpoint, hit F5 to return to client-side
  10. Delete "Test Test" from the textbox and leave it empty
  11. Click "Do Postback" button
  12. You will reach the breakpoint, hit F5 to return to client-side
  13. Stop debugging and disable the viewstate on the page
  14. Disable the viewstate on the textbox so that the markup will be as follows
    <asp:TextBox ID="box" runat="server" Text="" EnableViewState="false" ontextchanged="box_TextChanged"></asp:TextBox>
    <asp:Button ID="btn" runat="server" Text="Do Postback" onclick="btn_Click" />
    
  15. Put a breakpoint on the "box_TextChanged" event
  16. Run the application in debug mode
  17. Write "Test Test" in the textbox
  18. Click "Do Postback" button
  19. You will reach the breakpoint, hit F5 to return to client-side
  20. Notice that the textbox text is "Test Test", even without viewstate!!!
  21. Delete "Test Test" from the textbox and leave it empty
  22. Click "Do Postback" button
  23. You will not reach the breakpoint, even when the text is changed from "Test Test" to ""!!!
Confused? You have the right to. Here is what happened:
  1. When you created the textbox using the markup, the default value of the textbox is empty or ""
  2. At the first load of the page, the server loaded the textbox with its default value which was set into the markup which was "" in our case
  3. Since this was the first page load, the server already knew that whatever viewstate was enabled or disabled it would not matter as the page was in its default state
  4. At client-side, when you entered "Test Test" inside the textbox then followed by postback the server created the whole page and its controls from scratch
  5. So, the first step was to create the textbox and pre-populate it with its default value which is "" in our case
  6. At this point the viewstate may have played a role, so:
    1. When viewstate was enabled:
      1. The server checked if any viewstate was saved from before
      2. In this case, no viewstate was saved because the previous load was the first page load as we stated above in step #3
      3. So, the textbox text was not changed and it stayed ""
    2. When viewstate was disabled:
      1. The textbox text was not changed and it stayed ""
  7. Server loaded the new textbox text from the submitted form, so in our case it was found to be "Test Test"
  8. Server set the textbox text to the value retrieved in the previous step which is "Test Test"
  9. Server compared the textbox value from step #6 and #8 and figured out that the value has changed from "" to "Test Test", so the server fired the "box_TextChanged" event
  10. Before rendering the page, the server had something to do:
    1. When viewstate was enabled:
      1. The server saved the viewsate of the page controls, so the textbox state was saved and the saved value of the textbox was "Test Test"
    2. When viewstate was disabled:
      1. No state was saved
  11. Back again at client side, when you cleared the textbox text and performed a postback, the server created the whole page and its controls from scratch
  12. So, the first step was to create the textbox and pre-populate it with its default value (from the markup) which is "" in our case
  13. At this point the viewstate may have played a role, so:
    1. When viewstate was enabled:
      1. The server checked if any viewstate was saved from before
      2. In this case, viewstate was found and the saved texbox value was "Test Test"
      3. So, the server re-populated the textbox with its previous value which was saved in the viewstate, in our case, "Test Test""
    2. When viewstate was disabled:
      1. The textbox text was not changed and it stayed ""
  14. Server loaded the new textbox text from the submitted form, so in our case it was found to be ""
  15. Server set the textbox text to the value retrieved in the previous step which is ""
  16. Server compared the textbox value from step #13 and #15 to check if any changes had been applied on the textbox, so:
    1. When viewstate was enabled:
      1. A change had been applied from "Test Test" to ""
      2. The server fired the "box_TextChanged" event
    2. When viewstate was disabled:
      1. No change had been applied as both values are ""
      2. The server didn't fire the "box_TextChanged" event
That's it, I think you now got it right, right?

The last thing to mention here is

ASP.NET Page Lifecycle

Why to save viewstate and controlstate on server?
Now after we have understood what viewstate and controlstate are about, let's discuss something. We said before that ASP.NET has a default way or approach to save and load viewstate and controlstate if not other approach is set by the system developer. This default approach is saving and loading states into and from a hidden field on the page. Is this good? may be it is good for some cases but if your page controls are complex or many or you are stuffing too many objects into the viewstate, this will make the viewstate and controlstate large in size and in this case the hidden field will take too much size on the page. This will eventually cause the response size to be large. I think this is enough for a reason on why to try to find another approach for saving and loading states.

How to save viewstate and controlstate on server?
To control the way ASP.NET will save and load your page states, you need to override two events on the page class but before going deep into code let's highlight some points first.

The whole idea here is to save the states on a text file on the server. This way the server will not have to send the states back and forth between server and client which makes the request and response sizes smaller and the whole application performance better.

So, for this approach to work as it should, a state file should be created for every user so that users will not share states. This could be handled using session ids as the file names or something like that. This will work because we know that session ids are unique for all users and it is impossible for two users to have the same session id.

Problem
This is good but there is a problem with this approach. We said that session ids are unique for all users and that every user will have his own unique session id, but, for the same user, if he opens more than one page of the application on more than one tab, all these pages and tabs will share the same session id. So, now we have to differentiate between the states of pages even for the same user because we don't want to load the states of page A to page B.

Solution
We have to define an id for each page a user opens, so that the combination of this id with the user session id form a unique page id. This combined id will be used as the state file id. To do this, we will generate a unique page id at the page first load and save this id on a hidden field on the page.

Building the whole solution
As we said before, there are two events to override on the page class:
  1. The "SavePageStateToPersistenceMedium" event which is fired when the server saves the page states before the page is rendered
  2. The "LoadPageStateFromPersistenceMedium" event which is fired when the server loads the saved states from the previous response
So, we will create our own class derived from the "Page" class and customize it to look as in the code below.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.UI;
using DevelopmentSimplyPut;
using System.IO;
using System.Web.UI.WebControls;

namespace DevelopmentSimplyPut.CustomStatePreservePages
{
    public class InFileStatePreservePage : Page
    {
        private string pageId;
        public string PageId
        {
            get
            {
                string result = "";

                if (!string.IsNullOrEmpty(pageId))
                {
                    result = pageId;
                }
                else
                {
                    result = Request.Form["hdnPageId"];
                }

                return result;
            }
        }

        public string StatePreserveFilesFolderPath
        {
            get
            {
                return Path.Combine(Request.PhysicalApplicationPath, Constants.StatePreserveFilesFolderName);
            }
        }

        protected void Page_Load(object sender, EventArgs e)
        {
            if(!IsPostBack)
            {
                pageId = Session.SessionID.ToString() + Guid.NewGuid().ToString();
                Page.ClientScript.RegisterHiddenField("hdnPageId", pageId);
            }
        }

        protected override object LoadPageStateFromPersistenceMedium()
        {
            if (Page.Session != null)
            {
                if (Page.IsPostBack)
                {
                    string filePath = Session[PageId].ToString();

                    if (!string.IsNullOrEmpty(filePath))
                    {
                        if (!File.Exists(filePath))
                        {
                            return null;
                        }
                        else
                        {
                            StreamReader sr = File.OpenText(filePath);
                            string viewStateString = sr.ReadToEnd();
                            sr.Close();

                            try
                            {
                                File.Delete(filePath);
                            }
                            catch
                            {

                            }

                            LosFormatter los = new LosFormatter();
                            return los.Deserialize(viewStateString);
                        }
                    }
                    else
                    {
                        return null;
                    }
                }
                else
                {
                    return null;
                }
            }
            else
            {
                return null;
            }
        }

        protected override void SavePageStateToPersistenceMedium(object state)
        {
            if (state != null)
            {
                if (Page.Session != null)
                {
                    if (!Directory.Exists(StatePreserveFilesFolderPath))
                    {
                        Directory.CreateDirectory(StatePreserveFilesFolderPath);
                    }

                    string fileName = Session.SessionID.ToString() + "-" + DateTime.Now.Ticks.ToString() + ".vs";
                    string filePath = Path.Combine(StatePreserveFilesFolderPath, fileName);

                    Session[PageId] = filePath;

                    LosFormatter los = new LosFormatter();
                    StringWriter sw = new StringWriter();
                    los.Serialize(sw, state);

                    StreamWriter w = File.CreateText(filePath);
                    w.Write(sw.ToString());
                    w.Close();
                    sw.Close();
                }
            }
        }
    }
}

And now any page you create in the system should inherit from the "InFileStatePreservePage" class and in the "Page_Load" event call the base first as in the code below.
public partial class MyPage : InFileStatePreservePage
{
    protected void Page_Load(object sender, EventArgs e)
    {
        base.Page_Load(sender, e);
    }
}

Deleting the abandoned state files
To delete the remaining state files, you need to make sure that the files you are going to delete are the outdated files only. To do that you should delete only the files that are not modified for a period greater than the session timeout period. So, to do that you can add the code below to your Global.asax file.
void Application_Start(object sender, EventArgs e) 
{
 string stateFilesDirectory = System.IO.Path.Combine(Server.MapPath("~"), DevelopmentSimplyPut.Constants.StatePreserveFilesFolderName);
 Application["stateFilesDirectory"] = stateFilesDirectory;
 
 if (!string.IsNullOrEmpty(stateFilesDirectory) && System.IO.Directory.Exists(stateFilesDirectory))
 {
  string[] files = System.IO.Directory.GetFiles(stateFilesDirectory);
  foreach (string file in files)
  {
   System.IO.FileInfo fi = new System.IO.FileInfo(file);
   fi.Delete();
  }
 }
}

void Application_End(object sender, EventArgs e) 
{
 string stateFilesDirectory = Application["stateFilesDirectory"].ToString();

 if (!string.IsNullOrEmpty(stateFilesDirectory) && System.IO.Directory.Exists(stateFilesDirectory))
 {
  string[] files = System.IO.Directory.GetFiles(stateFilesDirectory);
  foreach (string file in files)
  {
   System.IO.FileInfo fi = new System.IO.FileInfo(file);
   fi.Delete();
  }
 }
}

void Session_Start(object sender, EventArgs e) 
{
 string stateFilesDirectory = Application["stateFilesDirectory"].ToString();

 if (!string.IsNullOrEmpty(stateFilesDirectory) && System.IO.Directory.Exists(stateFilesDirectory))
 {
  string[] files = System.IO.Directory.GetFiles(stateFilesDirectory);
  int timeoutInMinutes = Session.Timeout;
  int bufferMinutes = 5;
  foreach (string file in files)
  {
   System.IO.FileInfo fi = new System.IO.FileInfo(file);
   if (fi.LastAccessTime < DateTime.Now.AddMinutes((-1 * (timeoutInMinutes + bufferMinutes))))
   {
    fi.Delete();
   }
  }
 }
}

void Session_End(object sender, EventArgs e)
{
 string stateFilesDirectory = Application["stateFilesDirectory"].ToString();
 
 if (!string.IsNullOrEmpty(stateFilesDirectory) && System.IO.Directory.Exists(stateFilesDirectory))
 {
  string[] files = System.IO.Directory.GetFiles(stateFilesDirectory);
  int timeoutInMinutes = Session.Timeout;
  int bufferMinutes = 5;
  foreach (string file in files)
  {
   System.IO.FileInfo fi = new System.IO.FileInfo(file);
   if (fi.LastAccessTime < DateTime.Now.AddMinutes((-1 * (timeoutInMinutes + bufferMinutes))))
   {
    fi.Delete();
   }
  }
 }
}

Important prerequisite
For this solution to work well you need to set your application session timeout mode to Inproc. Otherwise, the session "Session_Start" and "Session_End" events will not fire and in this case the only time you will be clearing the abandoned state files will be at "Application_Start" and "Application_End" events which is a way too late and may cause the server to have low disk space. So, to do so you have to set your application web.config file as below.
<configuration>
    <system.web>
     <sessionState cookieless="UseCookies" mode="InProc" timeout="60"/>
    </system.web>
</configuration>


That's it, hope you can find this helpful someday. For further reading you can check the resources below.
Good luck.


References
  1. TRULY Understanding ViewState - Infinities Loop 
  2. ViewState in SQL
  3. ViewState Compression - CodeProject
  4. Understanding ASP.NET View State
  5. Flesk.NET Components - Viewstate Optimizer
  6. ViewState: Various ways to reduce performance overhead - CodeProject
  7. Keep ASP.NET ViewState out of ASPX Page for Performance Improvement - CodeProject
  8. Control State vs. View State Example