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, whenLoadViewState ()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 whenASP. NETTries 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 (typeTripletOrPair) Is NOT the ViewState property (typeStateBag) Of the page or any of its controls. You can think it as an object representation of the ViewState value on the html page (_ VIEWSTATEHidden 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_ VIEWSTATEFieldLoadPageStateFromPersistenceMedium (),And the values on the ViewState tree will be put into the controls ViewState bag inLoadViewState (). During the page save/render phrase, the ViewState tree will be created againSaveViewState (), Then serialized and written onto html pageSavePageStateToPersistenceMedium ()
So, I thought I cocould 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 namedBtnPostbackCreated statically on. aspx file, and another button named btnClickMe created dynamically inPage. OnInit (), And I will not recreateBtnClickMeFor postbacks. So on a page postback, by the timeOnInit ()AndLoadPageStateFromPersistenceMedium ()Is executed, the control tree and ViewState tree wocould have different structure, the ViewState tree will have valueBtnClickMe, But the control tree will not have the controlBtnClickMe. I thought it wocould 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 distinct CT 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:
/Samples + 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 ofBtnClickMeHaving 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 page postback,BtnClickMeWas 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. anyway, I think this make perfect sense, we do want_ VIEWSTATEFields 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'sCommandNameProperty, but Label doesn' t have this property. If you run this code, still no exception will occur. Is it "XXX"CommandNameJust simply ignored? Let's have a look of the saved ViewState after postback:
/Configure
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 haveCommandNameProperty, 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 previusly 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 bothMyButtonAndMyLabelHave Property calledMyProperty, ThoughMyButton. MyPropertyIsStringType,MyTextBox. MyPropertyIsColorType.
From previous test, we have learned thatLoadViewState ()Will write the ViewState values into controls 'viewstate bag directly without being bothered to go through the controls 'Property. So, I wowould perform CT "XXX" will be writtenMyLabel'S ViewState bag successfully even thoughMyLabel. MyPropertyReally expectsColorValue, but we are going to have problem if we try to access the value inMyLabel. MyProperty.
My guess was right this time, if you run the code,InvlaidCastExceptionWill be thrown(Color) ViewState ["MyProperty"]When the Property is accessed inOnLoad ().
OK, the last one now, what will happen if two controls are very different? OK, maybe we don't need something complicatedGridView, Let's just tryDropDownList:
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 definedMyDropDownList:
Public class MyDropDownList: DropDownList
{
Protected override void LoadViewState (object savedState)
{
Base. LoadViewState (savedState );
}
}
There is nothing inMyDropDownList, It just overridesLoadViewState (),So I can place a break point there.
And then I changed page to useMyDropDownList:
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 pointBase. LoadViewState (),Run it, click the Postback button,MyDropDownListCreated, the itsLoadViewState ()Invoked, program hangedBase. LoadViewState (), Good, just worked as expected! Hold on, this looks like a problem:LoadViewState (object savedState)Seems to be expectingTripletObject as a parameter, but what is actually passed in here isPair!
It does make sense, doesn' t it? Don't forgetPairObject is the saved ViewState left behind fromBtnClickMe, 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 becameDropDownList, But ASP. NET is silly enough to try to restore it withPairObject. ForDropDownList, OnlyTripletObject can be used to restore it, so, of course, whenLoadViewState ()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, expeciallySaveViewState ()AndLoadViewState ()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 inControl,WebControlOrListControl, 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 bothSaveViewState ()AndLoadViewState ()Understand it. Normally, the ViewState of a WebControl will be saved asPairObject, ifPairIs not enough,TripletObject may be used, like whatListControlDoes (ListControlNeeds a another object to hold the states for its child items). WhenASP. NETTries 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 will be thrown.
Conclusion:
WhenASP. NETTries to restore ViewState values to a page and its controls, a ViewState tree will be created by deserializing_ VIEWSTATEValue on html page. The ViewState tree contains only control indices and key-value pairs.ASP. NETFinds 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_ VIEWSTATEField on html page
Problems abve may be difficult to notice and debug, especially 3 and 4
* One can override this behavior usingViewStateModeByIdAttribute.