CoreCoder

LANGUAGES: C# | VB.NET

ASP.NET VERSIONS: 1.x | 2.0

 

Viewstate of Mind

ASP.NET Viewstate Is Critical to Minimize the End-to-end Response Time of Web Pages

 

By Dino Esposito

 

It s widely known that most ASP.NET data-bound controls don t persist their data source to the viewstate across postbacks. The reason is easy to guess: performance. Any sort of information stored to the viewstate finds its way to a hidden field, gets serialized, Base64-encoded, and, finally, downloaded to the client.

 

Is that all? Well, no there s more. You pay the price of the viewstate extra burden twice. What do you think happens when the user submits the page back to the Web server? The whole form contents are posted to the server, including the viewstate hidden field. You pay the price of viewstate extra data in both download and upload. To keep the viewstate size reasonable, most data-bound controls do not persist their data source, which is likely to be a pretty large chunk of data.

 

For this reason, an accepted practice suggests that you insulate your data initialization code into the Page_Load event. In particular, you place it in the false branch of an IF statement guarded by the value of the page s IsPostBack property. Is this enough to avoid that a large chunk of data flows into the viewstate? Definitely not.

 

List Controls Surprise

Let s consider a classic example. Imagine your page includes a dropdown list control filled with country names (a very popular form feature indeed). I can t really say how many countries there are in the world this week, but the order of magnitude of the actual number is certainly that of hundreds. Let s assume you have a few hundred names. Here s a possible way to fill the dropdown list:

 

(C#)

if (!IsPostBack)

{

 DataTable data = new DataTable();

 data.Columns.Add("Country", typeof(string));

 for(int i=0; i<500; i++) {

   DataRow row = data.NewRow();

   row["country"] = "Country #" + i.ToString();

   data.Rows.Add(row);

 }

 countries.DataSource = data;

 countries.DataTextField = "Country";

 countries.DataBind();

}

 

(VB.NET)

If Not IsPostBack Then

 Dim data As New DataTable

 data.Columns.Add("Country", GetType(String))

 For i As Integer = 0 To 500

   Dim row As DataRow = data.NewRow()

   row("country") = "Country #" + i.ToString()

   data.Rows.Add(row)

 Next

 countries.DataSource = data

 countries.DataTextField = "Country"

 countries.DataBind()

End If

 

Run the page with the tracing option turned on and take a look at the results. Figure 1 shows what happens. What s the circled number? Just the size in bytes of a portion (I repeat, portion) of your page s viewstate. That s right, you have more than 17 KB of viewstate in an otherwise straightforward page filled with only two controls.

 


Figure 1: A dropdown list control serializes the contents of its Items collection to the viewstate.

 

What s up? Simply put, the whole list of countries is happily persisted in the viewstate. But wait; we did say that most data-bound controls don t deliberately save their data source to the viewstate, didn t we?

 

The data source is not persisted, per se; however, when DataBind is called on a list control, the contents of the data source are loaded into the Items collection and used for rendering. The rub lies in the implementation of the Items collection (something similar happens with DataGrid controls, also).

 

In ASP.NET 1.1, there are two non-exclusive ways to feed list controls: through the Items or DataSource properties. Internally, the control loads everything into the Items collection and reads from there when it s about time to render out. The Items property is an instance of the ListItemCollection type. As you can see in the documentation, this type implements the IStateManager interface. The interface defines the properties and methods any class must implement to support viewstate management for a server control. Quite a few classes implement that interface in the ASP.NET framework, the most important of which are DataGridColumnCollection, Style, and, as mentioned, ListItemCollection. Anything you store in instances of these classes is persisted to the viewstate.

 

What can you do to work around this issue and have your viewstate undergo a dramatic slimming diet? Although the example discussed unveils an unsuspected misconception, the problem is just a new instance of a known issue; to solve it you must look at one of two known approaches:

  • Leave the viewstate on the server;
  • Disable the viewstate for some controls.

 

From the client s perspective, the effect is nearly the same. If you leave the viewstate on the server, no hidden field will ever be downloaded to the client and uploaded back during postbacks. If you disable the viewstate for all or some controls, you dramatically reduce the size of the field up to an absolute minimum of 20 bytes.

 

However, the server-side implications of both approaches are different. Different considerations apply to both techniques and make each suitable for different scenarios. In this article, I ll take a look at some practical implications of programming with a disabled viewstate.

 

Programming with a Disabled Viewstate

You can disable the viewstate on individual controls or all controls located in the page. To turn off viewstate on controls, you typically set the EnableViewState property to false. To turn off viewstate at the page level, you can use the EnableViewState attribute of the @Page directive.

 

Disabling the viewstate for the dropdown list control of the previous example results in a significant cut of the viewstate burden. It doesn t come free of issues, though. As Figure 2 shows, disabling the viewstate for the dropdown list control generates a much slimmer page. Note that the size of the viewstate never goes below 20 bytes, because that is just the size of the key added to protect the viewstate against tampering with.

 


Figure 2: The viewstate of the dropdown list control has been disabled.

 

It goes without saying that dropping the viewstate of a single control rarely brings the total size down to just a few bytes in a real-world page.

 

What are the drawbacks of this solution? The viewstate plays a fundamental role in ASP.NET pages because it takes a snapshot of the page and control s state and restores it once the page is posted back to the server. The viewstate is key to emulating a stateful model on top of a stateless protocol like HTTP. The viewstate is also indissolubly tied to a particular instance of the page and becomes part of its context. It is not used on the client, but travels with the page request. All in all, I believe that the actual implementation of the viewstate to be the best possible default approach because it balances out various factors: load on the client, speed of restoration, server memory, and code maintenance, readability, and flexibility. You must be aware, however, that there might be situations where the default approach just doesn t work all that well. Let s drill down into the countries dropdown list control.

 

Reasonably, you fill the control with the results of a database query or, perhaps, with contents read out of a text file. You bind the source to the dropdown list in the Page_Load event when IsPostBack is false. Well, in this case, the page works just fine as long as the viewstate is turned on. If not, the first time the page posts back the dropdown list control looks empty. But why?

 

Viewstate-effective Data Binding

With the viewstate feature turned off, there s no information to recreate the control s state as it was the last time that instance of the page was served. On the other hand, data binding takes place only the first time the page is loaded (when IsPostBack returns false). As you can see, there s no code path that ultimately refills the dropdown list. This is the main drawback of disabling the viewstate.

 

A page viewstate of just a few KB is acceptable. For example, a viewstate size of about 3 KB is optimal. For pure curiosity, I checked the size of the viewstate of some well-known pages. The home page of the MSDN site is less than 2 KB. The viewstate grows up to 90 KB in the control gallery page on http://www.asp.net. In this case, the page is a long list of descriptions and links, presumably created using a Repeater or a similar control.

 

What should you do when you have a viewstate that is too large? It depends on the characteristics of the data that crowds the field. Keep any data that really represents state information that can t be (easily) recalculated; drop any data that is constant, static, or that can be obtained with limited efforts in terms of computation and memory occupation. The list of world countries certainly belongs to the second category. You can read the data once and put it into the ASP.NET cache. Upon postback, you retrieve a reference to the data from the cache and bind it to the control. Figure 3 shows how to do that.

 

<%@ Page Language="C#" Trace="true" %>

<%@ Import Namespace="System.Data" %>

<SCRIPT runat="server">

private void Page_Load(object sender, System.EventArgs e)

{

 if (!IsPostBack)

 {

   LoadData();

 }

 BindCountryList();

}

private void LoadData()

{

 DataTable data = new DataTable();

 data.Columns.Add("Country", typeof(string));

   

 for(int i=0; i<500; i++)

 {

   DataRow row = data.NewRow();

   row["country"] = "Country #" + i.ToString();

   data.Rows.Add(row);

 }

 Cache["MyData"] = data;

}

private void BindCountryList()

{

 object o = Cache["MyData"];

 if (o == null)

 {

   LoadData();

   o = Cache["MyData"];

 }

 

 DataTable data = (DataTable) o;

 countries.DataSource = data;

 countries.DataTextField = "Country";

 countries.DataBind(); 

}

</SCRIPT>

<HTML>

<HEAD><title>Better Page</title></HEAD>

<BODY>

<FORM id="Form1" method="post" runat="server">

 <asp:dropdownlist runat="server" id=

  "countries" enableviewstate="false" />

 <asp:button runat="server" text="Refresh" />

</FORM>

</BODY>

</HTML>

Figure 3A: Viewstate-effective country list control (C#).

 

<%@ Page Language="VB" Trace="true" %>

<%@ Import Namespace="System.Data" %>

<SCRIPT runat="server">

Private Sub Page_Load(ByVal sender As Object,

                     ByVal e As System.EventArgs)

 If Not IsPostBack Then

   LoadData()

 End If

 BindCountryList()

End Sub

Private Sub LoadData()

 Dim data As New DataTable

 data.Columns.Add("Country", GetType(String))

   

 For i As Integer = 0 To 500

   Dim row As DataRow = data.NewRow()

   row("country") = "Country #" + i.ToString()

   data.Rows.Add(row)

 Next

 Cache("MyData") = data

End Sub

Private Sub BindCountryList()

 Dim o As Object = Cache("MyData")

 If Not (o Is Nothing) Then

   LoadData()

   o = Cache("MyData")

          End If

 

 Dim data As DataTable = CType(o, DataTable)

 countries.DataSource = data

 countries.DataTextField = "Country"

 countries.DataBind()

End Sub

</SCRIPT>

<HTML>

<HEAD><title>Better Page</title></HEAD>

<BODY>

<FORM id="Form1" method="post" runat="server">

 <asp:dropdownlist runat="server" id=

  "countries" enableviewstate="false" />

 <asp:button runat="server" text="Refresh" />

</FORM>

</BODY>

</HTML>

Figure 3B: Viewstate-effective country list control (VB.NET).

 

If you run the code shown in Figure 3, you experience an extremely slim and fast viewstate and no bad surprises when you post back. Really? Try selecting any item in the dropdown list different from the first and then post back. As you may have imagined, the index of the currently selected country is lost in the postback. Again, this is another side effect of disabling the viewstate.

 

You might think that the selected index of an index shouldn t go to the viewstate; moreover, it should be perhaps the only property out of a control to work fine regardless of the viewstate. As you can see, this is not the case, because SelectedIndex is implemented through the Selected property of the Items collection. In other words, it is indirectly persisted to the viewstate and, in case of a disabled viewstate, it is not automatically resolved through the IPostBackDataHandler interface. You must take a different route to solve the issue.

 

The simplest workaround consists of binding in Page_Init rather than Page_Load. In this case, the Items collection is not empty when the methods of IPostBackDataHandler are called and the value posted is correctly applied.

 

Separating Private and Public Viewstate

Following is what I suggest if you don t want to move code out of Page_Load (which is my preference). Web programming was possible for years before the advent of ASP.NET and the viewstate. How would you solve this in ASP, for instance? I guess you would have used the old faithful Request object. Why not here, then? The Request object collects input parameters extracted from the raw HTTP packet. The packet contains client-side values, which is the text of a textbox and the selected index of a dropdown list. Add the following code at the bottom of the BindCountryList method in Figure 3:

 

(C#)

if (IsPostBack)

{

 string item = Page.Request["countries"].ToString();

 ListItem tmp = new ListItem(item);

 int index = countries.Items.IndexOf(tmp);

 countries.SelectedIndex = index;

}

 

(VB.NET)

If IsPostBack Then

 Dim item As String = Page.Request("countries").ToString()

 Dim tmp As ListItem = New ListItem(item)

 Dim index As Integer = countries.Items.IndexOf(tmp)

 countries.SelectedIndex = index

End If

 

Pay attention to add the code right after the call to DataBind. The code snippet retrieves the text of the selected item, finds the index of the associated ListItem object, and simply selects that index in the dropdown control. In the end, you ll have a page that works as expected but is 17 KB slimmer. Be honest, it s not a bad deal!

 

Dino Esposito is a Wintellect trainer and consultant who specializes in ASP.NET and ADO.NET. Author of Programming Microsoft ASP.NET and Introducing ASP.NET 2.0, both from Microsoft Press, Dino also helped several companies architect and build effective products for ASP.NET developers. Dino is the cofounder of http://www.DotNet2TheMax.com, a popular portal for .NET programmers. Write to him at mailto:dinoes@wintellect.com or join the blog at http://weblogs.asp.net/despos.

 

Can t Wait for Control State to Arrive

As I explained in my October 2004 asp.netPRO article, Maintain State Control, the state of a control is logically split into two parts: private and public. In terms of the physical implementation in ASP.NET 1.1, though, the state is a monolithic chunk of data that can be atomically disabled or enabled. ASP.NET 2.0 introduces a neat separation between the two, and my previous article provides some guidance on how to accomplish that in ASP.NET 1.1, as well. With control state, each control can maintain a sort of private viewstate that is not affected by page-level viewstate settings. This private viewstate, on the other hand, should be limited to storing only those properties for which state is essential. Normally, appearance properties don t need to go to the viewstate unless they can change dynamically to reflect, say, a particular state of the control not otherwise computable.