Cutting the cancer called View State

by sven 22 December 2008 20:01

Without a doubt the View State has the questionable honor of sparking some of the most heated debates on asp.net "features". Some call it "pure evil", others call it, well, usable. These opinions aside, I will try to give some insight on what impact the View State has on page-size and roundtrip data-size. I will also give some alternatives to using the View State, plugged into the MVP architecture presented a while ago, but also usable on plain asp.net. As background, to get a good primer on the View State, check out the excellent article on MSDN by Scott Mitchell.

Why would I use View State ?

The answer to that is actually quite simple : efficiency. Suppose you're building a page, and the data to display is "hard to get" (e.g. coming from a webservice). The View State sort of caches this data, so that on the next postback, the data is already present to use, no need to refetch that data. That's great right ? Well, yeah, but of course there's a cost associated with this cache, and the cost is in that pesky "__VIEWSTATE" hidden field. This field actually holds the View State data, and is sent from the client to the server on every postback. So when the data was not "hard to get" in the first place, it's kind of silly to send it back to the server on a request, the server should just refetch it.

So it's easy right ? On a case-by-case basis, we decide to enable or disable the View State, and fetch the data on the server on each request when it's disabled. The problem is that when disabling the View State, suddenly some controls don't behave anymore as expected because they rely on it. Also, passing data between postbacks using the View State is very convenient, how can we tackle that ?

Creating a scenario

Let's create a simple page for selecting an item. Items are data-objects consisting of a Value and Name. The name is displayed in a dropdownlist, and when we selected an item, the value of the selected item is shown underneath it. To add some state to the page, we have a button to toggle the visibility of this selected value. And the text on the button should be "Show selected value" or "Hide selected value" depending on the current state. By default (on first request), the selected value should be visible.

So this simple page should look like this :

image

In fact, since it's so simple, we will cheat and already create the ASPX markup beforehand. All cases will use this same markup :

image

Note that we set the "AutoPostBack" property  of the dropdown to initiate a postback on every selection change. Also, in the markup, we don't set the Text-property on the toggle button, nor the Visible-property on the label. Indeed it should be set from the presenter, so that when the business requirements change (e.g, by default this label should be hidden instead of visible), we don't need to change the markup.

Case 1 : Using View State

As usual, we first create the interface for our view. Given the "business requirements", it should look like :

   1: public interface IWithViewstateView : IView
   2: {
   3:     ICollection<MyDataObject> TheData { set; }
   4:     string SelectedValue { get; }
   5:     string LabelSelectedValueText { set; }
   6:     bool IsLabelVisible { get; set; }
   7:     string ToggleButtonText { set; }
   8:     event EventHandler ToggleLabelVisibleClicked;
   9: }

Pretty straightforward, right ? We can set our collection of objects, get which one was selected, set the text of our label, get or set whether the label is visible, set the text on the toggle button and  handle the event for when the toggle button was clicked.

So, on to the presenter. The whole presenter looks like :

   1: public class WithViewstatePresenter : BasePresenter<IWithViewstateView>
   2: {
   3:     public WithViewstatePresenter(IWithViewstateView view) : base(view)
   4:     {
   5:     }
   6:     protected override void SubscribeViewToEvents()
   7:     {
   8:         View.ToggleLabelVisibleClicked += new EventHandler(View_ToggleLabelVisibleClicked);
   9:     }
  10:     void View_ToggleLabelVisibleClicked(object sender, EventArgs e)
  11:     {
  12:         View.IsLabelVisible = !View.IsLabelVisible;
  13:         View.ToggleButtonText = View.IsLabelVisible ? "Hide selected value" : "Show selected value";
  14:         UpdateView();
  15:     }
  16:     protected override void OnViewLoad(object sender, EventArgs e)
  17:     {
  18:         if (!IsPostBack)
  19:         {
  20:             View.TheData = ReferenceDataComponent.GetAllData();
  21:             View.IsLabelVisible = true;
  22:             View.ToggleButtonText = "Hide selected value";
  23:         }
  24:         UpdateView();
  25:     }
  26:     protected override void UpdateView()
  27:     {
  28:         if (View.IsLabelVisible)
  29:             View.LabelSelectedValueText = "The selected value is :" + View.SelectedValue;
  30:     }
  31: }

Nothing special here. On the first request, we fill the view with data coming from our referencedata component and specify that the selected value label should be visible. For simplicity, the "data" is just a generated list of 1000 items. On next requests, we don't need to fill the view with this data anymore, the View State will have it "cached". The only thing we need to do on evey round trip, is set the label of the selected value (if visible). And of course handle the toggle button click event when it occurs.

That was simple again. Now for the implementation of our view :

   1: public partial class Withviewstate : BasePage,IWithViewstateView
   2: {
   3:     protected override IPresenter CreatePresenter()
   4:     {
   5:         return new WithViewstatePresenter(this);
   6:     }
   7:     protected override void CreateEventhandlers()
   8:     {
   9:         ButtonToggleValuevisible.Click += (s, e) => ToggleLabelVisibleClicked(s, e);
  10:     }
  11:     public ICollection<MyDataObject> TheData
  12:     {
  13:         set
  14:         {
  15:             foreach (var o in value)
  16:                 DropDownListData.Items.Add(new ListItem(o.Name,o.Value) );
  17:         }
  18:     }
  19:     public string SelectedValue
  20:     {
  21:         get { return DropDownListData.SelectedValue; }
  22:     }
  23:     public string LabelSelectedValueText
  24:     {
  25:         set { LabelSelectedValue.Text = value; }
  26:     }
  27:     public bool IsLabelVisible
  28:     {
  29:         get { return GetPageParamAsBool("LabelVisible"); }
  30:         set
  31:         {
  32:             SetPageParam("LabelVisible", value);
  33:             LabelSelectedValue.Visible = value;
  34:         }
  35:     }
  36:     public string ToggleButtonText
  37:     {
  38:         set { ButtonToggleValuevisible.Text = value; }
  39:     }
  40:     public event EventHandler ToggleLabelVisibleClicked;
  41: }

Taking into account that Resharper generated most of this code, again only a few lines of manual coding. Purists would argue that I could have used "return LabelSelectedValue.Visible;" on line 29, and not have used the GetPageParam/SetPageParam at all. But since we're trying to work directly with the View State, we call these methods. As explained at the end of this post here, the  Set/GetPageParamxxx methods are wrappers around the View State, implemented in the BasePage as :

image

So, this wraps up our case. The page is working flawlessly, but what is happening under the covers ? The asp.net framework encodes (Base64 encoding) all View State data, and this is passed to the client in a hidden field. So on postback, this is also sent to the server.

Let's do a "View Source" of our page :

image

Ouch ... now that's a huge viewstate. In the right hand corner at the bottom, we see that the page is 173kb in size. And the excellent Web Development Helper reveals the following :

image

The request is a whopping 91kb ! So whenever the user changes the selection on the page, 91kb of data is sent from the client to the server, and 173kb from the server to the client ! That's over a quarter of a megabyte per round-trip. Now that is just a waste of bandwidth. Let's see if we can reduce this by getting rid of the View State.

Case 2 : Without the View State, flavor 1

We start out case 2 with some "Enhanced IDE Refactoring", aka "copy paste", copying our markup, view and presenter to a new bunch of files called WithoutViewstate1.aspx, IWithoutViewstate1 and WithoutViewstate1Presenter. The only thing we change is that in the markup Page element, we put "EnableViewState='false'". Then press F5 to run the page.

image

The page comes up nice, and when we do view source, we see that the viewstate field is shrunk enormously, only leaving some minimal data for the Control State, yielding a page size of only 93kb ! That 80kb less data to send on each request . Talk about return-on-investment :)

But behold ...when we select an item in the dropdownlist, we get :

image

All our items are gone ... the dropdown data, the button, the label ... let's start with the drop down data.  In our presenter we had an "if (!IsPostBack)" condition before filling the view with the data. So we move the filling of the data to the UpdateView() method, which gets called on every request, and run again. Now, after every postback we get :

image

Let's ignore the missing button and label for a moment. Everytime we select another item, the selection gets reverted to the start situation. The source of our problem is that the filling of our dropdownlist happens too late in the life cycle of the page. Indeed in the MSDN article is explained that the LoadPostBackData happens right before the Load event. So let's move our filling of the data before that Load-event. The Init event looks sweet (and when you think of it, "Init" is a nice name for a method where we will initialize controls with reference data)

So we add the Init event to the IWithoutViewstateView1 interface (and leave the rest unchanged) :

   1: public interface IWithoutViewstateView1 : IView
   2: {
   3:     event EventHandler Init;
   4:     ICollection<MyDataObject> TheData { set; }
   5:     string SelectedValue { get; }
   6:     string LabelSelectedValueText { set; }
   7:     bool IsLabelVisible { get; set; }
   8:     event EventHandler ToggleLabelVisibleClicked;
   9: }
The Init event is, analog to the Load event, automatically hooked up to the asp.net Init event, so we don't need to change anything to the view implementation (aka the codebehind). In the presenter, we attach a handler to the event and move the filling of the data to this handler :
   1: protected override void SubscribeViewToEvents()
   2: {
   3:     View.ToggleLabelVisibleClicked += new EventHandler(View_ToggleLabelVisibleClicked);
   4:     View.Init += new EventHandler(View_Init);
   5: }
   6: void View_Init(object sender, EventArgs e)
   7: {
   8:     View.TheData = ReferenceDataComponent.GetAllData();
   9: }
The rest of the presenter remains unchanged, and we run again :
image
Sweet, the dropdownlist works. Now let's work on the missing button and label. When we click the empty button, the buttontext and label are shown again, but when we click "Hide" it's still shows up. And when we select another value in the dropdownlist, the buttontext and label always disappear. How can this be ?
The solution for the button text is quite simple. Analog to the filling of the dropdown, we now have to set the text on every request. So we move the following line from the ToggleLabelvisibleClicked event handler to the UpdateView method that in our case gets executed on every request:
   1: View.ToggleButtonText = View.IsLabelVisible ? "Hide selected value" : "Show selected value";
Now when we run, the buttontext is always visible, but every time we select an item from the dropdown, it is reverted to "show selected label" and the selected value label is gone. Indeed, the "IsLabelVisible" property is always false after a postback, since it's stored and retrieved with the  Set/GetPageParam methods that use the View State, which is disabled. Only on first request, "IsLabelvisible" is true, because we set it to true in the OnViewLoad() method. So we tracked our problem down to these Set/GetPageParam methods in the BasePage class. Let's change 'em !
When trying to solve this problem, we quickly come to the conclusion that using a hidden field to persist data beyond a postback is actually a nice way to do it. Indeed, a hidden field is automatically posted and html-encoded. On the other hand, a NameValueCollection is a more convenient storage structure then an encoded string. Since these changes must happen for every page, we'll make a new abstract BasePageWithoutViewState, that derives from the BasePage. Also we'll make the Set/GetPageParam methods (that have direct access to the View State) virtual, so we can override them in our new class.
We start by adding a NameValueCollection and a HiddenField :
   1: private NameValueCollection _pageParameters = new NameValueCollection();
   2: private HiddenField _pageStateField = new HiddenField() { ID = "__pageState" };    
Then we override OnInit to add a call to CreatePageParameters(), which will take the persisted values from the hidden field, and put them in the NameValueCollection. (The ParamHelper class is a helper class that provides methods to convert a NameValuecollection to/from an  URL-encoded string.)
   1: protected override void OnInit(EventArgs e)
   2: {
   3:     CreatePageParameters();
   4:     base.OnInit(e);
   5: }
   6:  
   7: private void CreatePageParameters()
   8: {
   9:     _pageParameters = ParamHelper.StringToParams(Request.Form["__pageState"]);
  10:     Page.Form.Controls.Add(_pageStateField);
  11: }
Ofcourse, we'll need to also override the Render() method, to make sure our parameters are converted back into an url-encoded string (the AsBool/AsInt getter and setter methods don't need to be overridden as they will use the virtual functions) :
   1: protected override void Render(HtmlTextWriter writer)
   2: {
   3:     //persist the pageparameter right before rendering
   4:     _pageStateField.Value = ParamHelper.ParamsToString(_pageParameters);
   5:     base.Render(writer);
   6: }
And then finally, we override the GetPageParam and SetPageParam to use our new NameValueCollection instead of the ViewState:
   1: internal override void SetPageParam(string key, string value)
   2: {
   3:     _pageParameters[key] = value;
   4: }
   5:  
   6: internal override string GetPageParam(string key)
   7: {
   8:     return _pageParameters[key];
   9: }
That's it ! We now make sure our page derives from BasePageWithoutviewState instead of BasePage an run it again :
image
Works as a charm! When doing "view source" on our page, at the bottom we see our own "Page State" holding the value for the "LabelVisible" parameter :
image 
Also, let's peek into Web Development Helper :
image
We learn that our page-size has shrunk from 173kb to 91kb, and more importantly that the request-size (the data sent from the client to the server when posting back) went from 91kb to 8kb ! Since most people have a much slower upload-speed then download-speed, the decrease in request-size  can be a major boost in responsiveness for the website. And the total traffic for one roundtrip has gone from over 250kb to less that 100kb.

Case 3 : Without the View State, flavor 2

In the previous case, an argument could be made that we are leaking too much implementation details of the view into the presenters. Indeed, we are forced to introduce the asp.net Init-event into our presenter, and make sure that we fill the view with our data on that event, or it's too late. What if we don't want that kind of knowledge into our presenter ?

For this case, we'll copy all code from the previous case to a new bunch of files WithoutViewstate2.aspx, IWithoutViewstate2 and WithoutViewstate2Presenter. The only thing we change is remove the Init event and move the filling of the dropdown to the UpdateView method.
So our presenter looks like :
   1: public class WithoutViewstate2Presenter : BasePresenter<IWithoutViewstateView2>
   2: {
   3:  public WithoutViewstate2Presenter(IWithoutViewstateView2 view)
   4:         : base(view)
   5:     {
   6:     }
   7:     protected override void SubscribeViewToEvents()
   8:     {
   9:         View.ToggleLabelVisibleClicked += new EventHandler(View_ToggleLabelVisibleClicked);
  10:     }
  11:     void View_ToggleLabelVisibleClicked(object sender, EventArgs e)
  12:     {
  13:         View.IsLabelVisible = !View.IsLabelVisible;
  14:         UpdateView();
  15:     }
  16:     protected override void OnViewLoad(object sender, EventArgs e)
  17:     {
  18:         if (!IsPostBack)
  19:         {
  20:             View.IsLabelVisible = true;
  21:         }
  22:         UpdateView();
  23:     }
  24:     protected override void UpdateView()
  25:     {
  26:         View.TheData = ReferenceDataComponent.GetAllData();
  27:         View.ToggleButtonText = View.IsLabelVisible ? "Hide selected value" : "Show selected value";
  28:         if (View.IsLabelVisible)
  29:             View.LabelSelectedValueText = "The selected value is :" + View.SelectedValue;
  30:     }
  31: }
Now, our page works again only half ..the button and label work fine, but the the dropdownlist reverts back to the top element in the list on every postback. This one is actually easy to solve when we think a minute how HTTP-POST's work. The value of the selected item in the dropdown is sent to the server on every postback, so instead of getting the selected value from the dropdownlist-control , we can easily fetch it like this
   1: public string SelectedValue
   2: {
   3:     set { DropDownListData.SelectedValue = value; }
   4:     get { return Request.Form[DropDownListData.UniqueID]; }
   5: }
You'll notice that we also have added a setter to the property, because we now need to set the value of the dropdown on every request. So we add the following line to the UpdateView of the presenter, and all is well :
   1: protected override void UpdateView()
   2: {
   3:     View.TheData = ReferenceDataComponent.GetAllData();
   4:     View.SelectedValue = View.SelectedValue;
   5:     View.ToggleButtonText = View.IsLabelVisible ? "Hide selected value" : "Show selected value";
   6:     if (View.IsLabelVisible)
   7:         View.LabelSelectedValueText = "The selected value is :" + View.SelectedValue;
   8: }
Everything works as expected ! Web Dev Helper shows us again that the request and response size are only 8kb and 91kb.
image

Conclusion : is the View State evil ?

Well, it depends. For small amounts of "hard to fetch" data, it can be an efficient cache, making your life easier and the page snappier. But when used in combination with large amounts of "easy to fetch" data, it doesn't make sense to use the View State. All is does is make your pages (much) bigger, thus increasing the traffic to and from your website ! The real lesson to learn here is to make sure you understand what the View State does and impacts, and, when needed, how you can work around it.

 

Source code for this example :

MVPViewState.zip (34,74 kb)

Currently rated 2.0 by 2 people

  • Currently 2/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Tags:

C# | Design Patterns | MVP

Comments

Comments are closed
(c) 2008 Qsoft.be