I thought I understand ViewState, until I came cross this exception:
Failed to load viewstate. The control tree into which viewstate is being loaded must match the control tree that was used to save viewstate during the previous request. For example, when adding controls dynamically, the controls added during a post-back must match the type and position of the controls added during the initial request.
This is a question asked by someone on a .NET mailing list. My first guess of what causing the problem is that on a page postback, when LoadViewState() is invoked to restore the saved ViewState values to the page and its controls (both Control tree and ViewState tree have been created at this stage), somehow, the ViewState tree doesn't match the control tree. So when ASP.NET tries to restore a ViewState value to a control, no control or a wrong control is found and then the exception occurs.
Note: the ViewState tree (type of Triplet or Pair) is NOT the ViewState property (type of StateBag) of the page or any of its controls. You can think it as an object representation of the ViewState value on the html page (the __VIEWSTATE hidden field), which contains all the values need to be written back to the controls during a page postback. If you don't change the default behavior, during the page initialize/load phrase, the ViewState tree will be created by de-serializing the value __VIEWSTATE field by LoadPageStateFromPersistenceMedium(), and the values on the ViewState tree will be put into the controls ViewState bag in LoadViewState() . During the page save/render phrase, the ViewState tree will be created again by SaveViewState (), then serialized and written onto html page by SavePageStateToPersistenceMedium ()
So, I thought I could reproduce same exception with something simple like this:
Defualt.aspx
<form id="form1" runat="server">
<asp:Button ID="btnPostback" runat="Server" Text="Postback" />
</form>
Default.aspx.cs
public partial class _Default : Page
{
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
if (!IsPostBack)
{
Button btnClickMe = new Button();
form1.Controls.Add(btnClickMe);
}
}
}
It is indeed a very simple page with a button named btnPostback created statically on .aspx file, and another button named btnClickMe created dynamically in Page.OnInit(), and I will not recreate the btnClickMe for postbacks. So on a page postback, by the time OnInit() and LoadPageStateFromPersistenceMedium() is executed, the control tree and ViewState tree would have different structure, the ViewState tree will have value for btnClickMe, but the control tree will not have the control btnClickMe. I thought it would be good enough to cause the exception, but soon I was proved wrong, there was no exception thrown.
To find out why, let's have a look of the actual ViewState value generated on the html page
“/wEPDwUKMTQ2OTkzNDMyMWRkOWxNFeQcY9jzeKVCluHBdzA6WBo=”
With a little help from ViewState Decoder I got this:
<viewstate>
<Pair>
<Pair>
<String>1469934321</String>
</Pair>
</Pair>
</viewstate>
There is no view state data for the neither of the buttons! I did expect something like <Pair /> for a control has empty state though.
So, I think here is the first thing I learned:
For a control on the Control tree, there may not be a corresponding item on the ViewState tree (if there is no state for this control need to be saved). If there is nothing found on ViewState tree for a control, the control’s LoadViewState() will not be invoked.
So, let's do something to make the button "dirty" and its ViewState saved.
public partial class _Default : Page
{
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
if (!IsPostBack)
{
Button btnClickMe = new Button();
form1.Controls.Add(btnClickMe);
btnClickMe.Text = "Click me";
}
}
}
The ViewState now became:
/wEPDwUKMTQ2OTkzNDMyMQ9kFgICAw9kFgICAw8PFgIeBFRleHQFCENsaWNrIG1lZGRkaZj77nQ7KGQERj05RRc1lk+fvNA=
<viewstate>
<Pair>
<Pair>
<String>1469934321</String>
<Pair>
<ArrayList>
<Int32>3</Int32>
<Pair>
<ArrayList>
<Int32>3</Int32>
<Pair>
<Pair>
<ArrayList>
<IndexedString>Text</IndexedString>
<String>Click me</String>
</ArrayList>
</Pair>
</Pair>
</ArrayList>
</Pair>
</ArrayList>
</Pair>
</Pair>
</Pair>
</viewstate>
The format of the ViewState looks quite interesting, but let's worry about it later.For now, it does look like Text property of the btnClickMe having been saved. Great!
But when I ran it, still no exception was thrown.
So, I guess that is just the way it works:
For an Item on the ViewState tree, if there is no corresponding control can be found on control tree, this ViewState Item will be ignored.
So, how about creating a different control instead? Something like this:
public partial class _Default : Page
{
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
if (!IsPostBack)
{
Button btnClickMe = new Button();
form1.Controls.Add(btnClickMe);
btnClickMe.Text = "Click me";
}
else
{
Label label = new Label();
form1.Controls.Add(label);
}
}
}
Still no exception! And it is very interesting, after the the page postback, the btnClickMe was gone and a label was shown with text "Click me"! I didn’t assign any value to its Text Property. Why "Click me" was there? The ASP.NET has restored the ViewState value onto label, but the value actually doesn't belong to it!
So, here is another interesting thing:
ASP.NET doesn’t really know which control a ViewState item belongs to. It only matches a item on the ViewState tree and a control on Control tree by the index.
If we have a look of the format of the saved ViewState, it contains nothing but just the indices of the control and the value-keys, so there is no way for ASP.NET can figure out which control exactly it belongs to. Anyway, I think this make perfect sense, we do want the _VIEWSTATE fields as small as possible, don't we?
The above sample has demonstrated a ViewState value for a Button's Text property was restored to a Label's Text Property on page postback. Now it comes to some interesting questions: what will happen if
1. The second control doesn't have the property with same name?
2. The second control has the property with different data type?
3. 2 controls are very different, say, a button and a GridView?
Let's find out!
public partial class _Default : Page
{
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
if (!IsPostBack)
{
Button btnClickMe = new Button();
form1.Controls.Add(btnClickMe);
btnClickMe.Text = "Click me";
btnClickMe.CommandName = "XXX";
}
else
{
Label label = new Label();
form1.Controls.Add(label);
}
}
}
This time I have assigned a value to button's CommandName property, but Label doesn't have this property. If you run this code, still no exception will occur. Is it "XXX" for CommandName just simply ignored? Let's have a look of the saved ViewState after postback:
/wEPDwUKMTQ2OTkzNDMyMQ9kFgICAw9kFgICAw8PFgQeBFRleHQFCENsaWNrIG1lHgtDb21tYW5kTmFtZQUDWFhY
ZGRk7q5i15YA6gDUPW8m/IVLqGXnb+4=
<viewstate>
<Pair>
<Pair>
<String>1469934321</String>
<Pair>
<ArrayList>
<Int32>3</Int32>
<Pair>
<ArrayList>
<Int32>3</Int32>
<Pair>
<Pair>
<ArrayList>
<IndexedString>Text</IndexedString>
<String>Click me</String>
<IndexedString>CommandName</IndexedString>
<String>XXX</String>
</ArrayList>
</Pair>
</Pair>
</ArrayList>
</Pair>
</ArrayList>
</Pair>
</Pair>
</Pair>
</viewstate>
Note this is the saved ViewState after a post back, the values are for the label. So even a label doesn't have CommandName property, the value was still written to Label's ViewState bag, and then saved. So if you dynamically change a control at runtime, the new control may silently "inherit" some rubbish ViewState from control was previously sitting at the position, and carry it all the time, pass it from server to client, and client back to server.
To test the second question out, there is a bit more code I had to write, as I couldn't find a property which on two different controls with different data type, so I have defined my own ones.
public class MyButton : Button
{
public string MyProperty
{
get { return ViewState["MyProperty"] == null ? String.Empty : ViewState["MyProperty"] as String; }
set { ViewState["MyProperty"] = value; }
}
}
public class MyLabel: Label
{
public Color MyProperty
{
get { return ViewState["MyProperty"] == null ? Color.Black : (Color)ViewState["MyProperty"]; }
set { ViewState["MyProperty"] = value; }
}
}
public partial class _Default : System.Web.UI.Page
{
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
if (!IsPostBack)
{
MyButton btnClickMe = new MyButton();
form1.Controls.Add(btnClickMe);
btnClickMe.MyProperty = "XXX";
}
else
{
MyLabel label = new MyLabel ();
label.ID = "label";
form1.Controls.Add(label);
}
}
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
if (IsPostBack)
{
MyLabel label = form1.FindControl("label") as MyLabel;
label.Text = label.MyProperty.ToString();
}
}
}
As you can see both MyButton and MyLabel have Property called MyProperty, though MyButton.MyProperty is string type, but MyTextBox.MyProperty is Color type.
From previous test, we have learned that the LoadViewState() will write the ViewState values into controls' ViewState bag directly without being bothered to go through the controls' property. So, I would expect "XXX" will be written into MyLabel’s ViewState bag successfully even though MyLabel.MyProperty really expects a Color value, but we are going to have problem if we try to access the value in MyLabel.MyProperty.
My guess was right this time, if you run the code, an InvlaidCastException will be thrown by (Color)ViewState["MyProperty"] when the Property is accessed in OnLoad().
OK, the last one now, what will happen if two controls are very different? Ok, maybe we don't need something complicated as GridView, let's just try a DropDownList:
public partial class _Default : Page
{
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
if (!IsPostBack)
{
Button btnClickMe = new Button();
form1.Controls.Add(btnClickMe);
btnClickMe.Text = "Click me";
}
else
{
DropDownList ddl = new DropDownList();
form1.Controls.Add(ddl);
}
}
}
When I ran the page, after clicking Postback button, I got a page returned with this error message:
Failed to load viewstate. The control tree into which viewstate is being loaded must match the control tree that was used to save viewstate during the previous request. For example, when adding controls dynamically, the controls added during a post-back must match the type and position of the controls added during the initial request.
Aha, finally get it!
To find out why, I made some little changes, first I defined MyDropDownList:
public class MyDropDownList : DropDownList
{
protected override void LoadViewState(object savedState)
{
base.LoadViewState(savedState);
}
}
There is nothing in MyDropDownList, it just overrides the LoadViewState(), so I can place a break point there.
And then I changed page to use MyDropDownList:
public partial class _Default : Page
{
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
if (!IsPostBack)
{
Button btnClickMe = new Button();
form1.Controls.Add(btnClickMe);
btnClickMe.Text = "Click me";
}
else
{
MyDropDownList ddl = new MyDropDownList();
form1.Controls.Add(ddl);
}
}
}
Let’s see what is going to happen, put a break point at base.LoadViewState(), run it, click the Postback button, MyDropDownList created, the its LoadViewState() invoked, program hanged at base.LoadViewState(), good, just worked as expected! Hold on, this looks like a problem: LoadViewState(object savedState) seems to be expecting a Triplet object as a parameter, but what is actually passed in here is a Pair!
It does make sense, doesn't it? Don't forget the Pair object is the saved ViewState left behind from the btnClickMe, and on the postback, ASP.NET doesn’t know which control it belongs to, what the ViewState tree can tell is "it belongs to the 3rd control on the form1" On the postback, the 3rd control on form1 now became a DropDownList, but ASP.NET is silly enough to try to restore it with the Pair object. For a DropDownList, only a Triplet object can be used to restore it, so, of course, when LoadViewState() is trying to do something like "Triplet triplet1 = (Triplet)savedState;", an exception will occur.
After having inspected some ASP.NET framework code using Lutz Roeder's .NET Reflector, expecially SaveViewState() and LoadViewState() method, I finally got a better picture of the what happened. A Control actually has full responsibility of saving/loading its ViewState. In ASP.NET, most of controls inherit their parent’s behavior defined in Control, WebControl or ListControl, but a control have complete control over it, and in theory, a control can have any data structure for holding its saved ViewState, as long as it is serializable and both SaveViewState() and LoadViewState() understand it. Normally, the ViewState of a WebControl will be saved as a Pair object, if Pair is not enough, a Triplet object may be used, like what ListControl does(ListControl needs a another object to hold the states for its child items). When ASP.NET tries to restore a control's ViewState with a velue which is saved for another control, if the two controls have different save ViewState object type, the Exception above will be thrown.
Conclusion:
When ASP.NET tries to restore ViewState values to a page and its controls, a ViewState tree will be created by deserializing the _VIEWSTATE value on html page. The ViewState tree contains only control indices and key-value pairs. ASP.NET finds the control for a ViewState item by the index*, and directly writes the value into control's ViewState bag. If you dynamically create/remove controls at runtime, it will be very likely to fool ASP.NET to restore ViewState values to a wrong control and causing a problem. Depending on what control is dynamically created/removed, following problems may occur:
1 Runtime exception when restoring ViewState
2 Runtime exception when accessing a property of a control
3 A control's property may have an unexpected value
4 A control may carry rubbish ViewState value and increase the size of _VIEWSTATE field on html page
Problems above may be difficult to notice and debug, especially 3 and 4
*One can override this behavior using ViewStateModeByIdAttribute.