10

Just spent a lot of time exorcising asp.net's large (but understandably useful) viewstate from an app, and i think it's worth sharing how it's done.

Basically, i want this question to be open to all solutions for shrinking/compressing/removing viewstate.

Chris
  • 39,719
  • 45
  • 189
  • 235

8 Answers8

7

First easy option, use the built-in SessionPageStatePersister class. What this does is keep the viewstate on the session on the server, rather than sending it to the client. However, it still sends a smaller viewstate down so isn't all roses:

using System.Web.UI;
... the following goes in your Page class (eg your .aspx.cs) ...
PageStatePersister pageStatePersister;
protected override PageStatePersister PageStatePersister
{
  get
  {
    // Unlike as exemplified in the MSDN docs, we cannot simply return a new PageStatePersister
    // every call to this property, as it causes problems
    return pageStatePersister ?? (pageStatePersister = new SessionPageStatePersister(this));
  }
}

This method shrunk a particularly large postback from 100k to 80k. Not great, but a good start.

Chris
  • 39,719
  • 45
  • 189
  • 235
  • 100kB ViewState? Are you sure that everything persisted in the viewstate has to be there? – Ladislav Mrnka Jan 17 '11 at 23:19
  • @ladislav I think that would be the reason for this – jcolebrand Jan 17 '11 at 23:33
  • 1
    @Ladislav - i'm sure that most of that viewstate doesn't need to be there. But its a legacy app and it's not 'commercially viable' to go through it and fix it up. – Chris Jan 18 '11 at 22:21
  • 1
    +1 for the comment that saved my life at the very end of a friday!!! // Unlike as exemplified in the MSDN docs, we cannot simply return a new PageStatePersister every call to this property, as it causes problems – Vinicius Jun 07 '13 at 18:25
6

Switch to ASP.NET MVC! No ViewState!

Simon_Weaver
  • 140,023
  • 84
  • 646
  • 689
  • 1
    That's a horrid suggestion. (EDIT: ok, there's a valid reason for the suggestion, but it's not that large a window, and he _did_ say he's excising it from an existing site) – jcolebrand Jan 17 '11 at 22:59
  • 4
    It probably isn't useful for people with existing sites that use viewstate, but: I can't help but agree with you. – Marc Gravell Jan 17 '11 at 23:00
  • 1
    @drachenstein - yes i'd say my answer is a little tongue in cheek. but useful knowledge for newcomers that may not even have heard of MVC - or small projects that may benefit in other ways from switching to MVC. in retrospect i found it is undoubtably the BEST way to remove ViewState – Simon_Weaver Jan 17 '11 at 23:00
  • @drachenstern that is probably a bit strong; sure, it isn't a trivial transition for large code-bases, but the suggestion is valid. – Marc Gravell Jan 17 '11 at 23:01
  • @drachenstern - and he also said 'i want this question to be open to all solutions for shrinking/compressing/removing viewstate' :-) it looks like he's already pretty much completed the task for his site. @chris - appreciate you sharing your findings :-) – Simon_Weaver Jan 17 '11 at 23:03
  • +1 I think this is a valid answer since the question is a general how to achieve a result question, I don't think the OP particularly restricted the answers to being usable on existing code. – csharptest.net Jan 17 '11 at 23:09
5

Another better option, roll your own PageStatePersister. Here's mine, heavily inspired by http://aspalliance.com/72:

using System.Web.UI;

... in your page class:

PageStatePersister pageStatePersister;
protected override PageStatePersister PageStatePersister
{
  get
  {
    // Unlike as exemplified in the MSDN docs, we cannot simply return a new PageStatePersister
    // every call to this property, as it causes problems
    return pageStatePersister ?? (pageStatePersister = new BetterSessionPageStatePersister(this));
  }
}

... in your BetterSessionPageStatePersister.cs:

/// <summary>
/// This class allows the viewstate to be kept server-side, so that postbacks are as small as possible.
/// It is similar to the built-in 'SessionPageStatePersister', but it yields smaller postbacks,
/// because the SessionPageStatePersister still leaves some viewstate (possibly it leaves the controlstate)
/// in the postback.
/// </summary>
class BetterSessionPageStatePersister : PageStatePersister
{
  public BetterSessionPageStatePersister(Page page)
    : base(page)
  { }

  const string ViewStateFieldName = "__VIEWSTATEKEY";
  const string ViewStateKeyPrefix = "ViewState_";
  const string RecentViewStateQueue = "ViewStateQueue";
  const int RecentViewStateQueueMaxLength = 5;

  public override void Load()
  {
    // The cache key for this viewstate is stored in a hidden field, so grab it
    string viewStateKey = Page.Request.Form[ViewStateFieldName] as string;

    // Grab the viewstate data using the key to look it up
    if (viewStateKey != null)
    {
      Pair p = (Pair)Page.Session[viewStateKey];
      ViewState = p.First;
      ControlState = p.Second;
    }
  }

  public override void Save()
  {
    // Give this viewstate a random key
    string viewStateKey = ViewStateKeyPrefix + Guid.NewGuid().ToString();

    // Store the view and control state
    Page.Session[viewStateKey] = new Pair(ViewState, ControlState);

    // Store the viewstate's key in a hidden field, so on postback we can grab it from the cache
    Page.ClientScript.RegisterHiddenField(ViewStateFieldName, viewStateKey);

    // Some tidying up: keep track of the X most recent viewstates for this user, and remove old ones
    var recent = Page.Session[RecentViewStateQueue] as Queue<string>;
    if (recent == null) Page.Session[RecentViewStateQueue] = recent = new Queue<string>();
    recent.Enqueue(viewStateKey); // Add this new one so it'll get removed later
    while (recent.Count > RecentViewStateQueueMaxLength) // If we've got lots in the queue, remove the old ones
      Page.Session.Remove(recent.Dequeue());
  }
}
Chris
  • 39,719
  • 45
  • 189
  • 235
  • This method shrunk my postbacks from 101k to 49k, so i'm happy with it :) – Chris Jan 17 '11 at 23:03
  • In Save(), could you reuse the same key on PostBack? – yanta Jul 05 '11 at 10:03
  • 1
    This code only allows X tabs to be open at the same time, in this example, opening 6 tabs will remove the viewstate for the first tab. – Andreas Apr 17 '12 at 15:49
  • Very good answer. One possible optimization / alternative : in Load() method, just after `ViewState` and `ControlState` have been set, remove viewstate from `Session` and recent queue, since it no more likely to be used. By doing this, discarding viewstates that are still in use somewhere else (eg : in other open tabs) will happen less often. Note : to make this work, `Queue` should be changed to a `LinkedList` to be able to remove a viewstate at any position. One drawback : if user goes back to previous page and resubmit the form (eg: by pressing F5 and confirm) it will not work anymore. – tigrou May 02 '13 at 14:16
2

It's important at first to understand what the heck viewstate is and why you want it in the first place. After that, it's just a matter of being mindful of what the app is doing for you and remembering to attach the UseViewState="false" to all the elements that would normally use viewstate.

Now to remember why it's useful, you'll have a definite need to retrieve things more often manually.

A time and a place for all tools, yes?

jcolebrand
  • 15,889
  • 12
  • 75
  • 121
2

Completely get rid of it:

    protected override object LoadPageStateFromPersistenceMedium()
    {
        return null;
    }

    protected override void SavePageStateToPersistenceMedium(object viewState)
    {
    }
anon
  • 4,578
  • 3
  • 35
  • 54
  • Where do you declare that at? – jcolebrand Jan 17 '11 at 23:02
  • In your codebehind or between a – anon Jan 17 '11 at 23:03
  • If you actually need to use the ViewState and want to reduce page /postback size (for mobile apps and the like) you can use the code snippet anon provided to save the ViewState in a Session variable. – Jason Towne Jan 17 '11 at 23:06
  • @drachenstern: Yes sir! (There's probably a weird way to do it across-the-board by subclassing the Page class, but that seems a little over-the-top.) – anon Jan 17 '11 at 23:07
  • 2
    @Jason Towne - Store it in Session? That seems a little scary and non-scalable unless your session state is in the database. – anon Jan 17 '11 at 23:08
  • @anon, Very true. It's definitely not *ideal*, but I've used it in the past on a low traffic in-house app in the past. If the app had a heavier user base I definitely wouldn't recommend it. – Jason Towne Jan 17 '11 at 23:11
1

You can try it or it!

Lucas S.
  • 537
  • 2
  • 7
  • 18
  • I was about to write an answer right on your first "it" :) without having read the article, ever!! – usr-local-ΕΨΗΕΛΩΝ Jan 18 '11 at 00:12
  • That is a good solution. However, I believe the best handling for viewstates consists in avoid them as much as possible. The most of developers aren't used to disabling a viewstate when it is unnecessary. – Lucas S. Jan 18 '11 at 01:20
1

You can, with a little trickery, hijack just the serialization of the page state by deriving from System.Web.Page and override the PageStatePersister property:

    private PageStatePersister _pageStatePersister = null;
    protected override PageStatePersister PageStatePersister
    {
        get { return _pageStatePersister ?? (_pageStatePersister = new PersistState(this)); }
    }

Once you've done this you can derive a new instance from HiddenFieldPageStatePersister and from there use reflection to change the persistence implementation:

    class PersistState : HiddenFieldPageStatePersister, IStateFormatter
    {
        public PersistState(Page p) : base(p)
        {
            FieldInfo f = typeof(PageStatePersister).GetField("_stateFormatter", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.SetField);
            f.SetValue(this, this);
        }

        object IStateFormatter.Deserialize(string serializedState)
        {
            BinaryFormatter f = new BinaryFormatter();
            using (GZipStream gz = new GZipStream(new MemoryStream(Convert.FromBase64String(serializedState)), CompressionMode.Decompress, false))
                return f.Deserialize(gz);                    
        }

        string IStateFormatter.Serialize(object state)
        {
            BinaryFormatter f = new BinaryFormatter();
            using (MemoryStream ms = new MemoryStream())
            {
                using (GZipStream gz = new GZipStream(ms, CompressionMode.Compress, true))
                    f.Serialize(gz, state);
                return Convert.ToBase64String(ms.ToArray());
            }
        }
    }

BEWARE

This is an example only for exploratory purposes. The above code IS A SECURITY RISK as it does not sign and encrypt the payload and therefore can easily be hacked by anyone attempting to do your site harm.

Again DO NOT USE THIS CODE without a full and complete understanding of security, cryptography, and .Net serialization.

</warning>

The real problem, as other have said, is the use of page state to begin with. The easiest solution to a poorly written ASP.NET app that makes heavy use of page state is to stand up a state server and use the SessionPageStatePersister.

csharptest.net
  • 62,602
  • 11
  • 71
  • 89
  • LMAO, I wasn't actually expecting an up-vote it was just to demonstrate how, not to really suggest it as an answer. I wonder if I can down-vote my own answer? – csharptest.net Jan 18 '11 at 00:02
  • Nope. bummer. Apparently you can't down-vote yourself either ;) – csharptest.net Jan 18 '11 at 00:03
  • I voted for it - it's a worthwhile solution! If you somehow made it store and check for a random nonce (eg a random guid) in the user's session it should be secure. – Chris Jan 18 '11 at 00:14
  • No this solution is again, just a "cool I didn't know I could do that" thing, not a "worthwhile solution". Not only does it need a lot more than a 'nonce' to be secure, but it also will generally produce more page data not less. MS went to great lengths to create the page state as small and secure as possible. Far better off leaving the default implementation until you can teach yourself not to store state in a stateless protocol :) – csharptest.net Jan 18 '11 at 00:21
0

One method that we have used in my company is to remove most of it by removing the runat="server" calls. Then we use javascript, or a good javascript library like jQuery or Prototype, to populate the HTML elements using ajax calls to the server.

My boss did a lot of work with a website that had several megs of viewstate data. He used the above method and it "works great with no viewstate".

Mike Webb
  • 8,855
  • 18
  • 78
  • 111
  • So basically you just made the site a purely AJAX site? Admirable goal, wish I could do the same on mine, and it's really only the conversion/setup/training time to get it working properly, after that it's just a matter of upkeep. Trying to move my coworkers in that direction now. – jcolebrand Jan 17 '11 at 23:32
  • Yah, in the case of websites and aspx pages we always try to have complete separation. Back-end, engine type code and calculations should happen discretely on the server with C#, visuals and things like that on the client side with JS. Just feels cleaner that way for some reason. – Mike Webb Jan 18 '11 at 16:39