ViewState Complexity
Non-Parsed Controls
Complex Controls
Aggregating Controls in a Custom Control
Page_Load and CreatChildControls
ViewState and Templated Controls
Correctly handling ViewState (and manipulating controls that have ViewState) is one of the more fragile pieces of ASP.NET in my mind. I've just spend a hellish 2 days tracking down a problem in a web custom control that I was building that was totally an issue in ViewState management.
ViewState is ASP.NET's way of allowing a page to maintain information that may not be visible to the user. ViewState can be used for several purposes, but one significant one is to allow controls with dyanmic content to deal with postbacks without having to re-acquire content.
Each control on a page can have ViewState that it controls. ViewState can be of any serialiazable type so you can store pretty complex information. When rendering a page, the ASP.NET infrastructure gathers up the ViewState from all the controls on the page and encodes it as a single string. It stored this string in a hidden field on the page. When the pages is submitted back to the server, ASP.NET grabs the string from the posted values, parses it, and assigns it to the appropriate controls on the page.
That all seems pretty straightforward, but one quickly gets into situation in which one cares about things like: When does ViewState get saved and restored? and What state does a control need to be in when its state is restored? Here's a few things I have discovered.
A non-parsed control, quixotically, is one in which the inner HTML of the control is parsed as HTML (rather than as extended properties of the control itself). You make a control non-parsed by applying the attribute ParseChildren(false). I was building a control that basically wrapped some formatting around its inner HTML. So I overrode AddParsedSubObject() to grab hold of the inner HTML elements that I wanted and then overrode CreateChildControls() to emit the formatting that I needed along with the inner HTML I had grabbed before.
This worked fine with some simple HTML contained within the control and I moved on to other things. Later I started using the control with more complex inner HTML -- specifically with controls that maintained ViewState. These new pages didn't work properly. ViewState appeared to be lost on postback. At first I assumed something was wrong with the controls themselves. I kept trying to debug them and figure out what they were doing wrong. Eventually I decided to think out of the box and put the inner controls on a page by themselves. They worked! I realized that the problem was in the container control and not the inner controls.
For controls added dynamically to a page, ViewState is loaded as a consequence of the control being added into the control hierachy of the page. In my non-parsed control, this happened in CreateChildControls(). However, CreateChildControls() wasn't being called until relatively late in the game. Specifically, the inner controls were adding their content to the page before CreateChildControls() was being called on the non-parsed control. Because the inner controls were not yet part of the page control hierarchy, when they added content to their child control set, there was no ViewState available to those child controls.
No one migth have expected that when the non-parsed control finally did get a change to add the inner HTML to its child control set, that ASP.NET might have recursively gone through and applied ViewState as appropriate. That, however, does not seem to be the case.
The fix for this was that my non-parsed control had to call EnsureChildControls() in AddParsedSubObject() as soon as it had grabbed all the inner HTML that it needed. This forced CreateChildControls() to occur earlier in the page processing cycle, and guaranteed that the inner HTML was attached to the page control hierachy before the inner HTML controls started building up their control structure -- which meant that their ViewState was then available.
My next round of grief came from trying to use a DataGrid within my own custom control. At first I made the stupid mistake of calling DataBind() on the DataGrid even on postback. This is not quite as stupid as it appears at first, because in a custom control, its not completely obvious when you are handling a postback (unlike the Page object, the WebControl does not have an IsPostBack attribute). So unfortunately what I was doing on postback was calling DataBind() without having set the DataSource on the DataGrid. This is a highly effective way to obliterate any ViewState the DataGrid has.
Unfortuantely, it did not make ViewState in the DataGrid work either. My original code had looked something like:
DataGrid dg = new DataGrid();
... fiddle with dg ...
this.Controls.Add(dg);
After hunting around on the Net for a while I discovered a posting from Scott Mitchell that basically said that style is bad as far as ViewState goes. The problem is that ViewState has a notion of whether state is being tracked or not. When state is not being tracked, it doesn't get remembered. And, wouldn't you know, but tracking of ViewState is false when a control is created (usually) and is turned on when the control is added to a child control set. So, in the above code, all the manipulations of the DataGrid prior to the last line are not tracked. The DataGrid renders properly (because the changes really do happen), but has no ViewState and so postback processing doesn't behave correctly.
So, naturally, I change my code to look like:
DataGrid dg = new DataGrid();
this.Controls.Add(dg);
... fiddle with dg ...
And, lo, if it still doesn't work! After banging my head into the wall for a while I managed to get a version that did work, and it looked like this:
DataGrid dg = new DataGrid();
dg.Columns = this.BuildColumnSpec();
this.Controls.Add(dg);
dg.DataSource = ...;
dg.DataBind();
The ordering is important. The column specifications for the DataGrid need to be set up before it is added to the control hierachy. This is because the column specification must be present when the ViewState is loaded (which happens as a result of this.Controls.Add()). However, the DataBind() call has to happen when ViewState tracking is on (which happens as a results of this.Controls.Add()).
Jeesh!
A related type of ViewState issue came up when I tried to aggregate some controls directly into the custom control I was building. By aggregate, I mean this: My custom control had some buttons on it. So, I thought I would simply declare ButtonControls as public attribtues of my class, initialize them in my constructor, and attach them to the page in my CreateChildControls() call. As with the above examples, this worked fine when my control was first rendered out to the browser, but ViewState was lost on the postback.
The problem here was caused by the tracking of ViewState that is mentioned in the previous example. An attribute of the buttons was being modified in a code-behind page during the Page_Load() handler. At this point in time, CreateChildControls() had not yet been called by ASP.NET, and so the button controls had not been added to the page control hierarchy. As a result, ViewState tracking was off for them. So the attribute changes made in the code-behind page were not being placed into the ViewState, and appeared to be lost on postback (of course, they weren't really lost on postback, but never made it into the ViewState in the first place).
I could have fixed this by forcing ViewState tracking on in my constructor, but was unsure of whether there would be any potential problems caused by having tracking on so early. Instead, I created public attributes for the aspects of the button that I wanted to expose, and then transferred the values of those attributes onto private member variables containing the button controls.
When to carry out this transfer turned out to be a slight issue in itself. My first stab was to place the code at the beginning of the Render() method. However, as with some many of my attempts, this resulted in a page that rendered correctly on load, but which did not have the correct ViewState on postback. As our friends as MS clearly document, ViewState is saved prior to the rendering phase, and so attribute changes made during rendering are not captured in ViewState.
The solution I usedwas to create a handler for the PreRender event and carry out the attribute transfer there. I find it a bit odd that there is not a standard method to override for PreRender, and you have to explicitly register a handler, but there it is.
As I was working my way through various ViewState issues I found myself wanting to know the relative order of various events in the page lifecycle. The MS ASP.NET web site documents the page cycle, but didn't answer at least one question that I really wanted that answer to: What is the relative order of the Page_Load event and the call to CreateChildControls() for a control contained on that page.
Sadly, the answer is that there isn't a fixed order. I find this a pain because there are certain actions that one would like to take prior to the Page_Load event (like having dynamic controls properly incorporated into the page's control hierarchy) and it seemed like CreateChildControls() was a natrual for this. Only it isn't.
In fact, CreateChildControls() seemed to get called at different times depending on whether one is dealing with a normal page load or a postback. In general, it seems as though CreateChildControls() is called after Page_Load during a normal page load, but is called before Page_Load during a postback.
I'm not sure that this is always the case though. I do know that during a postback, ASP.NET calls FindControl() after the page is initialized, but before Page_Load. I think this is part of applying posted state to the controls. At any rate, FindControl() calls EnsureChildControls() which in turn calls CreateChildControls(). So CreateChildControls() happens fairly early in the postback process.
The end result of this is that you can't rely on CreateChildControls() getting called at a particlular point in the page lifecycle.
This might not be a major issue except for the various ViewState issues that I have noted above which tend to require that controls get created and added to the page at just the right time in order for ViewState to be handled correctly. I'm not really happy with the solutions I am using now which tend to involve accumulating a bunch of state my control and then transferring that state over to the actual child controls during PreRender.