2008-01-16

ASP.Net, Dynamic Controls, and ViewState (oh my)

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.

DynamicControls.zip

2008-01-07

Come join the Social!

I don't have a Zune, but I thought it might be kind of fun, since your Zune Card and your Gamercard are automatically one, to set up my Zune Card account and join "the Social", as it were.

For a company that usually has "integration" down to a science, they're really dropping the ball on this, aren't they?

I have Windows Media Player and Windows Live Messenger. With a simple flick of an option on both sides, I can have my status message on Live Messenger automatically update to show what I'm listening to in Windows Media Player. It should be pretty much the same thing for Zune, no?

No. I have to download the Zune player. While it has some nice little features of its own (I like the collage of album art you can have in the background of the "now playing" mode), it lacks a ton of other features that are in WMP11, like visualizations, minimizing to a toolbar, updating the Live Messenger status message, responding to the media keys on my keyboard...

I did some searching on the 'net, and all I could find were echos of the questions going through my mind: Why didn't they just make a plug-in for WMP11 and integrate Zune functionality into their already-very-capable and useful media player?

2008-01-06

Water heater want a blankie?

Our hot water has always been pretty hit-or-miss. Some mornings, I would get up, turn the water on full hot until it warms up, and have to back it off to a comfortable temperature; others, I would leave it on full and have a lukewarm shower. I don't know if lately it had been getting worse, or if we just decided to do something about it, but we finally got an insulation blanket for it.

The difference was immediate. The very next morning, I noticed that the time I had to wait before I got hot water was dramatically shorter. And, the water was a lot hotter. It was very nice. Since it had been known to happen on occasion before, though, I was hesitant to directly attribute it to the new blanket; but we installed it on New Year's Day, and for the past five days since, the results have continued to be positive. I've also noticed that water out of other taps (like the kitchen sink, for instance) also get hotter water faster.

So how much money have we thrown away over the years? Honestly, probably not a lot. It's said one of the problems with a water heater is that it has to constantly re-heat the same water in the tank as it sits and cools when it's not used, and it would seem that our water heater didn't do very much re-heating once the water cooled. It's a gas-fueled appliance, and our gas bill during the summer months has always been pretty low. Although add on the amount of water dumped down the drain waiting for hot water to flow, and even if it's still not a lot, it's still waste. Plus, we've been throwing away comfort. Even if it's not a big thing, it's nice to know that we managed to do something to improve our lives. Not only is it an improvement, it helps to take the edge off knowing there are so many bigger improvements that we just haven't gotten around to, or can't afford, yet.