2011-01-14

ASP.Net, Dynamic Controls, and ViewState, revisited

At my current job, we are encouraged to share tips and ideas with other developers. I thought it could be useful to demonstrate the problem of dynamic controls and ViewState and my solution (posted three years ago here), since it not only is a problem that could come up in our web development, but it provides a useful opportunity to review the page life cycle.

So I grabbed my sample code and opened it in Visual Studio 2010. The good news is, it still works as advertised. However, I wanted to demonstrate the problem along with the solution; so I removed all my "extra" code. I was rather startled to find that the old problem didn't manifest itself. When I typed in data to one control and clicked a button to add another, the first control retained all its data.

It seems that the .Net Framework got some improvements over the years. The first improvement is that it seems ASP.Net is far more consistent in naming controls that are added to the page at run-time. (Part of the original problem was, when a control was loaded on page load vs. later in an event handler, the dynamically-assigned ID would be different.) The second is, if a control is loaded later in the life cycle, it does actually go back to the ViewState and re-load any applicable data. (It used to be very unreliable in this regard.)

I did find that things were not all roses. If you add a bunch of controls and start removing controls from the middle of the list, control data would get lost. Also, if you delete controls in the middle of your list and re-add controls, the controls may get added in the middle of the list instead of the end.

The solution is much easier than it used to be:

  • Create a member variable to hold the list of IDs (or whatever data is required to recreate the control and its ID) — in this example, I'm using private List<string> _childControlIds to just store the IDs, since the control type and location is always the same constant.
  • Create a Page Load event handler that looks like this:
    private void Page_Load(object sender, EventArgs e) {
     if (!IsPostBack) {
      _childControlIds = new List<string>();
      addAField(null).InitializeNewControl(); //Optional - create a new control, and initialize its data
     } else {
      if (ViewState["ControlCount"] as string[] != null) {
       _childControlIds.AddRange((ViewState["ControlCount"]) as string[]);
      }
      foreach (string controlId in _childControlIds) {
       addAField(controlId); //Create an existing control with its already-established ID
      }
     }
    }
  • The addAField method looks like this:
    private CustomChildControl addAField(string fieldId) {
     CustomChildControl cc = (CustomChildControl)LoadControl("CustomChildControl.ascx");
     if (String.IsNullOrEmpty(fieldId)) {
      cc.ID = String.Format("CUST{0}", DateTime.Now.Ticks); //new control; create a unique ID
      _childControlIds.Add(cc.ID);
     } else {
      cc.ID = fieldId; //existing control; reuse ID
     } 
     this.CustomControlsPlaceHolder.Controls.Add(cc);
     cc.DeleteControlClick += new EventHandler(DeleteCustomControl);
     return cc;
    }
    Notes:
    1. It no longer appears to be necessary to add the control before setting its ID — the ViewState manager seems to pick it up just fine either way.
    2. The custom control in my example has its own delete control and fires an event, that this page subscribes to. Your implementation may vary.
  • The DeleteCustomControl method looks like this:
    private void DeleteCustomControl(object sender, EventArgs e) {
     CustomChildControl cc = sender as CustomChildControl;
     if (cc != null) {
      _childControlIds.Remove(cc.ID);
      this.CustomControlsPlaceHolder.Controls.Remove(cc);
     }
    }
  • The method to add a control (in my case, a button on the page) is simply:
    private void AddButton_Click(object sender, EventArgs e) {
     addAField(null).InitializeNewControl(); //Create a new control, and initialize its data
    }
  • And finally, a Page PreRenderComplete event handler (because it's late enough in the page lifecycle; PreRender itself may be sufficient for your needs) that sticks the control ID list in ViewState:
    private void Page_PreRenderComplete(object sender, EventArgs e) {
     ViewState["ControlCount"] = _childControlIds.ToArray();
    }

And that's it. Surprisingly simple.

I don't know at what point this changed (or if I even over-architected the original solution — a distinct possibility). This could be an improvement in .Net 3.5, or it could be something "fixed" in a service pack along the way. The only thing I can say for certain is this much simpler method works quite well in my admittedly simple example.

No comments: