by Paul S.
ASP.NET provides you with several life-cycle events for you to handle and insert all the functionality of your web application. This article will explore the operations that all web-pages need to do, and map them to the ASP.NET lifecycle events. Then we will look at the tradeoffs you need to think about to maximize the efficiency of your pages and deal with the ViewState.
What a Web Page Does
I’m at a high risk of over-generalizing, but I think it’s safe to say that at their very essence, web pages display data, and provide forms that allow the user to post data back to the server. To add a small twist, user inputs can impact what data the web page displays. This leads us to defining these three types of data:
- Read-only data
- Dependant read-only data (that is, data that depends on some input from the user to decide which data to display)
- Read-write data. This can be split further into two categories:
- Not persisted—if your data can be completely reconstructed from the form parameters in the request, than you don’t need to keep a copy of the data in the session.
- Persisted—if you cannot completely reconstruct all the data you need to save from the request, you will need to store it somewhere in memory. If it’s data specific to the user, you should store it in session memory (via ASP.NET’s Session property), or if the data can be shared across all users, you can store it in the cache or application memory (Cache, Application)
Where the ASP.NET LifecycleFfits
The following diagram (click the image to enlarge) shows the four places, in the ASP.NET Control lifecycle, where you can perform the tasks needed to read and display data.
I’ve simplified the ASP.NET lifecycle to only those events that are useful to this discussion. See the MSDN article for a complete description.
- Init – Occurs for each control after the server has parsed the .ascx or .aspx file. All of the controls that you have defined in the .ascx and .aspx file are instantiated and initialized.
- Load – Occurs after the server has read the form parameters and populated the controls with them. Because all of the user-inputs have been loaded into the controls, you can reconstruct your non-persisted read-write data here, but you may also wait until PreRender.
- Control events – Here is where the control-specific events occur. These are events that occur when data has changed or when the user has done something. This includes the Click, TextChanged, and SelectionChanged events. For persisted read-write data, this is a great place to process user inputs and update the persisted data.
- PreRender – Occurs after all the control-specific events. You can setup your outputs here. For persisted read-write data this is the only time that you have data complete with changes from the user, and therefore the only place you can setup your page to display the current data. For all other types of data, there is no penalty for waiting until PreRender to setup your outputs, so you might as well be consistent and always do it on PreRender.
Defining the ViewState
So far in this article I have ignored the ViewState, and if you do the same for you web applications, chances are they will not be anywhere near as efficient as they could be.
The ViewState is a place where ASP.NET stores the state all of its controls, so that on a post-back you don’t have to reinitialize them all. This allows ASP.NET to keep up its abstraction of treating web pages like .NET windows forms. (The ViewState actually represents a significant leak in this abstraction. See Joel Spolsky’s article about Leaky Abstractions.) So when you bind data to a GridView and it now has 5 rows. When your page posts back, you can see that your GridView still has 5 rows, without you having to worry about setting up its state again, or storing its state somewhere.
But ASP.NET has to persist the ViewState somewhere, and it isn’t so presumptive as to use your session memory (at least, not by default). Instead ASP.NET persists the ViewState right on the form in the response you send to the user. So when the user sends a POST request back to the server, all of the ViewState data comes back with it.
Programmatically, the ViewState is a property on the Control object (see the MSDN reference). After the PreRender event, the server collects the contents of the ViewState on the Page object and all of its controls, and packs this data together. Next it serializes the data to a string which it places on a hidden field in the response. The result looks like this (click the image to enlarge):
The ViewState shown here is 11KB on a 169KB page, taking up roughly 6.5% of the page. Keep in mind this is 11KB of data that have to travel to your users in the response and back to the server when the users submit the form. In this example, 11KB isn’t too bad. It’s the size of a small image, but it is, by no means, a trivial part of the page. You probably do not want a whole lot of bandwidth just dedicated to this overhead of moving around the ViewState. Instead you would probably prefer to dedicate this bandwidth to serving content that your users will actually see and use. Additionally, you will find that you frequently don’t need the view state, especially if you’re using persisted read-write data. When using persisted read-write data, you’re already persisting all the data you need in the session, so also having it persisted in the ViewState is redundant. However, on many pages, the ViewState may be a necessary evil. The trick is in knowing how to manage it.
Managing the ViewState
So, how can you manage the ViewState? The Control class has an EnableViewState property that ASP.NET considers when it compiles the data to place into the response. Predictably, setting a Control or Page’s EnableViewState to false will cause ASP.NET to skip over that control or page and not include its ViewState data to the response, effectively cutting down the size of your response and requests.
This blog post does a good job of thoroughly discussing how the ViewState works and how to manage it. It explains how ASP.NET only persists the changes you have made to the ViewState. In theory, you could programmatically bind data to controls before the ViewState starts tracking changes. However, for all practical purposes, the ViewState starts tracking changes before the page’s Init event, so there isn’t a clear place to do this. There are ways around this issue, but I won’t go into them here.
So for our purposes, setting the EnableViewState property to false is really the only way we can avoid putting data into the ViewState that doesn’t need to be there.
Before we just disable the ViewState for everything, we first need to realize the important implications of disabling the ViewState. And to understand them, you need to know how each control uses the ViewState and how it processes posted form parameters.
First, it is important to understand that the ViewState from the request gets loaded into each of the Controls before the Load event. This is important because many ASP.NET controls will compare the input from the form parameters with the current state of the control, and issue a “changed event” if they are different. This is how the TextBox control issues TextChanged events and the DropDownList issues SelectedIndexChanged events.
With ViewState enabled, the ViewState data is loaded into each control first, and then the form parameters are compared with the current value of the control. If they are different, the control triggers a “changed event.” If you have disabled the ViewState, than when the form parameters are loaded into the control, there is no value preloaded into them to compare against, and the “changed event” does not fire. To resolve this issue, you need to setup these controls before the form parameters are loaded which will that set up a “before state.” So when the form parameters are loaded into the control there is already a value to compare against and cause the “changed event” to fire. The only place you can set this “before state” is on the Init event, since ASP.NET loads the form parameters between the Init and the Load events.
Another complication with disabling the ViewState is when you disable it for controls that dynamically create their sub-controls. This applies to Repeaters, DataLists, and GridViews. For these controls, their sub-control hierarchies are also stored in the ViewState and are re-created when the ViewState is loaded. With ViewState disabled, the sub-controls and you cannot access any form parameter inputs that would be associated with them. This can be resolved the same way as fixing the Changed event issue. If you bind data to your controls on Init, than the sub-control hierarchies will exist, just as if they were loaded from the ViewState, and they can receive their inputs from the form parameters.
The big question of whether to enable or disable the ViewState boils down to whether or not the alternative is more expensive. Here’s a look at the expenses associated with different types of data:
Read-Only and Dependant Read-Only Data
Because you’re not worried about capturing events for controls associated with read-only data, the only downside of disabling their ViewState is that you have to set their properties on every post-back. Though this data binding could involve a significant amount of calculation and creating a lot of objects, it is unlikely that this is more expensive than adding a lot of data to your ViewState, so you might as well disable the ViewState for controls containing this kind of data.
Persisted Read-Write Data
For persisted read-write data, you already have the data persisted in session memory, so you don’t need to persist it again in the ViewState. However, the downside of avoiding the ViewState is that you now have to set your control properties twice: once on Init to setup the Controls “before state” so that the sub-control hierarchies exist and the changed events still work. Then you need to bind data to your controls again on PreRender to setup the controls with the updated data you actually want the user to see. If you have doubts about this double-binding you may want to run some profiling on your code just to make sure you’re not spending more resources setting up your data than you save by not using the ViewState.
Not Persisted Read-Write Data
This is similar to persisted read-write data because the data is persisted on the form itself, and its appearance in the ViewState is redundant. However, because you’re not persisting this data anywhere else, it’s impossible for you to set up a “before” state to get them working again. If all you’re worried about are the “changed” events that you’re just stuck with treating all of the data from the form as if it were new data. This means that you have to validate everything from the request every time. You have a more serious problem, if you use controls that dynamically create sub-controls along with your not persisted read-write data. Because there is no ViewState to recreate these text fields, ASP.NET cannot find them to setup their properties with form values, and there is no for you to read what the user entered into these fields. To get around this, you could try and find the values in the Request object itself, but that would involve knowing how ASP.NET assigns form names to its Controls. However, trying to figure out how ASP.NET assigns form names to its controls relies heavily on how ASP.NET does things and could be considered coding to the implementation of ASP.NET and not to its interface.
So if you use Repeaters, GridViews, or DataLists with not persisted read-write data, you’re stuck with either using the ViewState or moving to persisted read-write data.
Overall, disabling the ViewState for your controls can add an extra bit of complexity to your code, so you may want to verify that the savings in bandwidth are worth it.
You now have some background information on how ASP.NET works and how its life-cycle events correspond to what you need to accomplish when processing a web request. ASP.NET allows you to put together a functional website very quickly, but it can be very inefficient if you do not know how to use it correctly.
– Paul S.