Let's say you have an ASP.Net page. On this page is a PlaceHolder. In that page, controls may get dynamically loaded into the PlaceHolder. This may occur as a result of a postback event (e.g. Button or LinkButton click) or simply as a result of some logic that occurs when the page loads. However, even with EnableViewState set on the PlaceHolder, controls that you load are not automatically persisted in ViewState. What to do?
This is a problem I've run into at least a half dozen times in my .Net coding career. It's also a problem I've solved that many times. However, every time I come across it, I forget one small detail, or I do something a little differently that makes it all fall apart. So, at least for my own benefit and perhaps for the benefit of others that may stumble upon this post, I'm putting the solution here. I haven't yet seen anything that pulls all these techniques together; this might not be the first, but at least it's one I know. Links to other pages contained here may not be from where I first got my information, but I'm including them for further reference and information.
First: When you create your controls, make sure you add them to the page FIRST, and THEN set any properties on them. This is because, in order for the ViewState manager to consider the control for management, it has to detect a change, and it can only detect changes after the control has been added to the Controls collection. (Thanks to Jeffrey Palermo's blog for pointing this out, and explaining the reason for it.) But what if you don't have any properties to set? That is remedied by the next consideration.
Second: Set an explicit ID. When ViewState is saved and restored, the control IDs must match. (I've seen a reference that says you have to use a custom class and explicitly tag it "ViewStateModeById", but my experience has shown me that ID matters whether you do this or not. Sequence may or may not be as important.) If you don't specify an ID, ASP.Net will automatically generate one, and there is no guarantee that the one generated when you create it on the fly will be the same when you recreate it on PostBack. The result is, you may get the control back, but its state will not persist on the first PostBack (although it might on the second and subsequent PostBacks, since it will be consistently reloading during the PostBack from the same spot -- this behavior may be confusing).
The tricky part comes in play when you have to reload the controls on PostBack. You have to do it before ViewState gets loaded, otherwise the controls won't be there to get their ViewState restored to them. The event before LoadViewState is Init. But you have to somehow have the page remember what controls were loaded. Keeping that value in ViewState is the most convenient, but you can't retrieve that value from Init because ViewState hasn't loaded yet. What to do?
I've seen one solution to this as to use the Session. I'm not a big fan of this myself for a few reasons. First, it requires Session. I consider it a goal to write web applications that don't require Session at all, because they generally perform faster and are more scalable. Granted, I rarely succeed, and Session does have its place, so writing it off isn't a valid enough reason. Second, it is very easy to get the page state out of sync with the session state. Hit the "Back" button a couple times, and then submit. The page submits a ViewState with two controls, but the server checks the Session variable and loads four controls, and then tries to apply the ViewState to it. Third, that variable has the lifetime of the Session. At worst, if it's not cleared out or checked appropriately, its presence may cause confusion on another page. At best, it's just taking up a couple bytes of memory long after its purpose has been served.
So what's the solution then? Third: My trick is to override the SaveViewState and LoadViewState methods on the page. SaveViewState returns a serializable object that gets dumped to the page. It's fairly trivial to override this method to return an object[2] (which is still an object and still serializable), putting your own data in one element and the results of base.SaveViewState() in the other. What you need to store in that element is something you can use to recreate the controls in the order they exist on the page. Maybe it's just a number -- if you create the controls as a series and give them IDs of ID{x}, where {x} is the sequential number, then all you need is the number of controls to recreate. Or maybe it's a string array that contains the list of control IDs. Or perhaps, if the controls are of different types, it's an array of the control types that have to be loaded, or the paths to the .ascx files. Whatever it takes.
The LoadViewState method is overridden to check to see if the ViewState object is an object array, and if the first element is of the type we expect (the one we create in SaveViewState). If so, it recreates the controls based on that first value, and then calls base.LoadViewState on the second. (If it turns out not to be an object array or something is not of the expected type, I am in the habit of just calling base.LoadViewState directly, just as a safety catch in case the overridden SaveViewState got missed. It's never happened yet, though.)
The code, therefore, looks a little like this:
/// <summary>
/// Saves the view state, including the IDs of placeholder controls so they can be reloaded on postback
/// </summary>
/// <returns>new ViewState object</returns>
protected override object SaveViewState() {
object[] newViewState = new object[2];
List<string> docTypeSelectionControlIDs = new List<string>();
foreach (Control control in placeHolder1.Controls) {
if (control is DocTypeSelectionControl) docTypeSelectionControlIDs.Add(control.ID);
}
string[] arrayOfIDs = docTypeSelectionControlIDs.ToArray();
newViewState[0] = arrayOfIDs;
newViewState[1] = base.SaveViewState();
return newViewState;
}
/// <summary>
/// Loads the view state, including custom state information from the SaveViewState override
/// </summary>
/// <param name="savedState"></param>
protected override void LoadViewState(object savedState) {
//if we can identify the custom view state as defined in the override for SaveViewState
if (savedState is object[] && ((object[])savedState).Length == 2 && ((object[])savedState)[0] is string[]) {
//re-load the DocTypeSelectionControls into the placeholder and restore their IDs
object[] newViewState = (object[])savedState;
string[] arrayOfDocTypeSelectionControlIDs = (string[])(newViewState[0]);
foreach (string docTypeSelectionControlID in arrayOfDocTypeSelectionControlIDs) {
DocTypeSelectionControl dtsc = (DocTypeSelectionControl) LoadControl("DocTypeSelectionControl.ascx");
placeHolder1.Controls.Add(dtsc);
dtsc.ID = docTypeSelectionControlID;
}
//load the ViewState normally
base.LoadViewState(newViewState[1]);
} else {
base.LoadViewState(savedState);
}
}
/// <summary>
/// Loads the page, including some dynamic controls
/// </summary>
protected override void OnLoad(EventArgs e) {
if (!IsPostBack) {
Document[] documents = (Document[])(Session["Documents"]); //The size of this array varies
foreach (Document doc in documents) {
DocTypeSelectionControl dtsc = (DocTypeSelectionControl) LoadControl("DocTypeSelectionControl.ascx");
placeHolder1.Controls.Add(dtsc); //Add control FIRST
dtsc.ID = String.Format("DTSC_{0}", doc.DocumentID); //THEN set ID
dtsc.InitializeControl(doc); //control initializes itself based on this document, and saves info in its ViewState
}
}
}
Note that this is just a quick sample. The controls in the placeholder could be modified as a result of some control event, but it really doesn't matter. The part that controls saving and reloading the ViewState of the controls doesn't change.
UPDATE 4 Dec 2009 — I created a sample project, using Visual Studio 2005 and .Net 2.0, that probably does a better job at describing things than I'm trying to do here. The project is very simple, with a single page and a single control. The page starts by loading a single instance of the control into a placeholder, and a button lets you add as many more instances of the control as you want. Every time you add a control, there is a PostBack, and all existing controls get their state and values loaded and re-saved to ViewState. You can see that the controls themselves do nothing to save their values from PostBack to PostBack; it all happens automatically thanks to the standard ViewState manager. When you click the "Submit" button, the main page loops through all the controls, collects the selected values, and builds a table to display the results. There is no use of Session, and at any time you can use the Back (or even Forward) buttons to navigate to a previous state of the page and pick up from that point.