Viewstate compression – the right way

I’ve just made a very nice looking asp.net page that lists some orders in a gridview. What the client wanted was to see a summary of all orders in his online shop, and then to quickly click on an expand button to see all the details, and even modify them, all without a full postback. No Problem. I made a component that uses a ajax toolkit collapsible panel to switch between the summary and order details, put an edit button that turned all labels into textboxes/dropdowns/etc, put the control in a gridview and binded it to the DB. Nice and easy. It works beautifully except that it’s slow. Dead slow.

After a quick investigation I was shocked to discover the viewstate has over 100KB in size. I guess that’s what happens when you load so much data in a gridview. Should have known better. After obvious optimizations, such as not binding order details that haven’t been expanded yet I managed to reduce the viewstate to 60KB. Still very bad. So I decided to compress it.

The first thing I did was google for “viewstate compression”. I found this article: http://www.codeproject.com/KB/viewstate/ViewStateCompression.aspx

At first I though I was saved. It looked like a great article. So I gave it try.

This is his method:

protected override object LoadPageStateFromPersistenceMedium() {
    string viewState = Request.Form["__VSTATE"];
    byte[] bytes = Convert.FromBase64String(viewState);
    bytes = Compressor.Decompress(bytes);
    LosFormatter formatter = new LosFormatter();
    return formatter.Deserialize(Convert.ToBase64String(bytes));
  }

  protected override void SavePageStateToPersistenceMedium(object viewState) {
    LosFormatter formatter = new LosFormatter();
    StringWriter writer = new StringWriter();
    formatter.Serialize(writer, viewState);
    string viewStateString = writer.ToString();
    byte[] bytes = Convert.FromBase64String(viewStateString);
    bytes = Compressor.Compress(bytes);
    ClientScript.RegisterHiddenField("__VSTATE", Convert.ToBase64String(bytes));
  }

I made a new BasePage inheriting from Page and overwrited LoadPageStateFromPersistenceMedium() and SavePageStateToPersistenceMedium(). I copy-pasted the compress and decompress methods, made my page inherit the BasePage and hit F5. Suprize! Ajax is broken. The author didn’t take into consideration that postbacks are of two types: syncronous and asyncronous.
ClientScript.RegisterHiddenField() only works for normal postbacks. No biggie. We can fix by replacing his call to RegisterHiddenField with:

System.Web.UI.ScriptManager sm = System.Web.UI.ScriptManager.GetCurrent(this);
if (sm != null && sm.IsInAsyncPostBack)
	System.Web.UI.ScriptManager.RegisterHiddenField(this, "__VSTATE", vState);
else
	Page.ClientScript.RegisterHiddenField("__VSTATE", vState);

When it finaly seems to be working with AJAX too (and getting good results with the compression – just 7 KB) I hit a snag: the viewstate wasn’t working propperly inside my usercontroll. I have some dropdownlists that no longer get restored :| After one hour of painfull debugging and playing with Fiddler I gave up and decided to write my own viewstate compression.

After a bit of reading in msdn I found out that Microsoft had very different ideas on how this should be achieved. Apparently the class Page has a property PageStatePersister, that is being used to define the ViewState behavior. This is my new Basepage, that replaces the standard PageStatePersister, with my own.

public class BasePage : Page
{
    private ViewStateCompressor _viewStateCompressor;

    public BasePage(): base()
    {
        _viewStateCompressor = new ViewStateCompressor(this);
    }

    protected override PageStatePersister PageStatePersister
    {
        get
        {
            return _viewStateCompressor;
        }
    }
}

All that is left is to write a new PageStatePersister:

public class ViewStateCompressor : PageStatePersister
{
    public ViewStateCompressor(Page page)
        : base(page)
    {
    }

    private LosFormatter _stateFormatter;

    protected new LosFormatter StateFormatter
    {
        get
        {
            if (this._stateFormatter == null)
            {
                this._stateFormatter = new LosFormatter();
            }
            return this._stateFormatter;
        }
    }

    public override void Save()
    {
        using (StringWriter writer = new StringWriter(System.Globalization.CultureInfo.InvariantCulture))
        {
            StateFormatter.Serialize(writer, new Pair(base.ViewState, base.ControlState));
            byte[] bytes = Convert.FromBase64String(writer.ToString());
            using (MemoryStream output = new MemoryStream())
            {
                using (GZipStream gzip = new GZipStream(output, CompressionMode.Compress, true))
                {
                    gzip.Write(bytes, 0, bytes.Length);
                }

                //base.Page.ClientScript.RegisterHiddenField("__PIT", Convert.ToBase64String(output.ToArray()));
                ScriptManager.RegisterHiddenField(Page,"__PIT", Convert.ToBase64String(output.ToArray()));
            }
        }
    }

    public override void Load()
    {

        byte[] bytes = Convert.FromBase64String(base.Page.Request.Form["__PIT"]);
        using (MemoryStream input = new MemoryStream())
        {
            input.Write(bytes, 0, bytes.Length);
            input.Position = 0;
            using (GZipStream gzip = new GZipStream(input, CompressionMode.Decompress, true))
            {
                using (MemoryStream output = new MemoryStream())
                {
                    byte[] buff = new byte[64];
                    int read = -1;
                    read = gzip.Read(buff, 0, buff.Length);
                    while (read > 0)
                    {
                        output.Write(buff, 0, read);
                        read = gzip.Read(buff, 0, buff.Length);
                    }
                    Pair p = ((Pair)(StateFormatter.Deserialize(Convert.ToBase64String(output.ToArray()))));
                    base.ViewState = p.First;
                    base.ControlState = p.Second;
                }
            }
        }
    }
}

That’s it folks. This one works even under my complex AJAX scenario and with user controls. I believe this is how Microsoft intended things to be.

About these ads

2 Responses to “Viewstate compression – the right way”

  1. Tudor Says:

    Welcome to the blogging world! Wish you more and more posts. Well done.

  2. Radu Carean Says:

    Very good article! Thanks for taking the time to share it to the world


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: