4. Changes in the ASP.NET View State
Two important changes occurred to the view-state
implementation starting with ASP.NET 2.0. As a result, two major
drawbacks have been fixed, or at least greatly mitigated. The size of
the view state is significantly reduced as a result of a new
serialization format. The contents of the view state have been split
into two states: classic view state and control state. Unlike the
classic view state, the control state can’t be disabled programmatically
and is considered a sort of private view state for a control. This
feature is ideal for developers building third-party ASP.NET controls,
as it helps them to keep critical persistent properties out of the reach
of end page developers.
The Serialization Format
In Figure 2,
you see the same page running under ASP.NET 1.x (the bottom window) and
ASP.NET 2.0 or ASP.NET 3.5 (the top window). A client-side button
retrieves the view-state string and calculates its length. The
JavaScript code needed is pretty simple:
<script language="javascript">
function ShowViewStateSize() {
var buf = document.forms[0]["__VIEWSTATE"].value;
alert("View state is " + buf.length + " bytes");
}
</script>
As you can see, the size of the view state for
the same page is quite different, and significantly smaller, in newer
versions of ASP.NET. Let’s see why.
The
contents persisted to the view state are ultimately the results of the
serialization process applied to an object graph. The object graph
results from the hierarchical concatenation of the view state of each
individual control participating in the page’s construction. The data
going to the view state is accumulated in tuples made of special
containers such as Pair and Triplet. Pair and Triplet are extremely simple classes containing two and three members, respectively, of type object. Each object is serialized to the stream according to various rules:
Simple types such as strings, dates, bytes, characters, Booleans, and all types of numbers are written as is to a binary stream.
Enum types and Color, Type, and Unit dictionary objects are serialized in a way that is specific to the type.
All
other objects are checked to see whether there’s a type converter
object to convert them to a string. If there is, the type converter is
used.
Objects that lack a type converter
are serialized through the binary formatter. If the type is not
serializable, an exception is thrown.
Pair, Triplet, and ArrayList objects are recursively serialized.
The resulting stream is hashed (or encrypted,
based on configuration settings), Base64 encoded, and then persisted to
the hidden field. This is more or less what happens already in ASP.NET
1.x. So where’s the difference?
The difference is all in a little detail lying
in the folds of type serialization. The type of each member being
serialized is identified with a byte—exactly, nonprintable characters in
the 1 through 50 range. In ASP.NET 1.x, printable characters—such as
<, >, l, s, I, and a few more—were used for the same purpose. This
change brings two benefits—a smaller size and a little more speed in
deserialization.
Note
The class behind the new serialization format of the ASP.NET 2.0 view state is a new type of formatter object—the ObjectStateFormatter
class. Specifically designed in ASP.NET 2.0 to serialize and
deserialize object graphs to and from a binary writer, the class is a
yet another .NET formatter object, as it also implements the IFormatter interface. In ASP.NET 2.0 and beyond, the ObjectStateFormatter class replaces the LosFormatter class used in ASP.NET 1.x. LosFormatter
writes to a text writer. This change is not an optimization per se, but
it allows a number of other improvements. For example, it allows
indexing of strings and more compact storage, as non-string values (such
as numbers and Booleans) are written as binaries and take up much less
space. |
The Control State
It is not uncommon for a server control to persist information across postbacks. For example, consider what happens to a DataGrid
control modified to support autoreverse sorting. When the user clicks
to sort by a column, the control compares the current sort expression
and the new sort expression. If the two are equivalent, the sort
direction is reversed. How does the DataGrid
track the current sort direction? If you don’t place the sort direction
property in the control’s view state, it will be lost as soon as the
control renders to the browser.
This kind of property is not intended to be
used for plain configurations such as pager style or background color.
It has an impact on how the control works. What if the control is then
used in a page that has the view state disabled? In ASP.NET 1.x, the
control feature just stops working. Private or protected properties that
are to be persisted across two requests should not be placed in the
view state. In ASP.NET 1.x, you can use the session state, the ASP.NET
cache, or perhaps another, custom hidden, field.
In ASP.NET 2.0 and beyond, the control state
is a special data container introduced just to create a sort of
protected zone inside the classic view state. It is safer to use the
control state than the view state because application-level and
page-level settings cannot affect it. If your existing custom control
has private or protected properties stored in the view state, you should
move all of them to the control state. Anything you store in the
control state remains there until it is explicitly removed. Also the
control state is sent down to the client and uploaded when the page
posts back. The more data you pack into it, the more data is moved back
and forth between the browser and the Web server. You should use control
state, but you should do so carefully.
Programming the Control State
The implementation of the control state is left
to the programmer, which is both good and bad. It is bad because you
have to manually implement serialization and deserialization for your
control’s state. It is good because you can control exactly how the
control works and tweak its code to achieve optimal performance in the
context in which you’re using it. The page’s infrastructure takes care
of the actual data encoding and serialization. The control state is
processed along with the view state information and undergoes the same
treatment as for serialization and Base64 encoding. The control state is
also persisted within the same view state’s hidden field. The root
object serialized to the view state stream is actually a Pair object that contains the control state as the first element and the classic view state as the second member.
There’s no ready-made dictionary object to hold
the items that form the control state. You no longer have to park your
objects in a fixed container such as the ViewState
state bag—you can maintain control-state data in plain private or
protected members. Among other things, this means that access to data is
faster because it is more direct and is not mediated by a dictionary
object. For example, if you need to track the sort direction of a grid,
you can do so using the following variable:
private int _sortDirection;
In ASP.NET 1.x, you resort to the following:
private int _sortDirection
{
get {
object o = ViewState["SortDirection"];
if (o == null) return 0;
return (int) o;
}
set {ViewState["SortDirection"] = value;)
}
To restore control state, the Page class invokes the LoadControlState
on all controls that have registered with the page object as controls
that require control state. The following pseudocode shows the control’s
typical behavior:
private override void LoadControlState(object savedState)
{
// Make a copy of the saved state.
// You know what type of object this is because
// you saved it in the SaveControlState method.
object[] currentState = (object[]) savedState;
if (currentState == null)
return;
// Initialize any private/protected member you stored
// in the control state. The values are packed in the same
// order and format you stored them in the SaveControlState method.
_myProperty1 = (int) currentState[0];
_myProperty2 = (string) currentState[1];
...
}
The LoadControlState method receives an object identical to the one you created in SaveControlState.
As a control developer, you know that type very well and can use this
knowledge to extract any information that’s useful for restoring the
control state. For example, you might want to use an array of objects in
which every slot corresponds to a particular property.
The following pseudocode gives you an idea of the structure of the SaveControlState method:
private override object SaveControlState()
{
// Declare a properly sized array of objects
object[] stateToSave = new Object[...];
// Fill the array with local property values
stateToSave[0] = _myProperty1;
stateToSave[1] = _myProperty2;
...
// Return the array
return stateToSave;
}
You allocate a new data structure (such as a Pair, a Triplet,
an array, or a custom type) and fill it with the private properties to
persist across postbacks. The method terminates, returning this object
to the ASP.NET runtime. The object is then serialized and encoded to a
Base64 stream. The class that you use to collect the control state
properties must be serializable.