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.

Advertisements