One area where ASP.NET developers seem to have the most difficulty with is working with dynamic controls. This difficulty is understandable, as there are a plethora of subtleties in getting everything to work right. I've actually authored a number of articles on working with dynamic controls:
The other day I was talking to a colleague who was having some problems with dynamic controls. His site was designed as a single “master” page that had on it navigation controls (skmMenu, to be precise) and a PlaceHolder. Whenever a skmMenu menu item was clicked, this colleague was wanting to dynamically load in a corresponding user control into the PlaceHolder.
His first attempts at knocking this out was to do the following:
- In his “master“ page he had a method called LoadPageControls() that was invoked from the Page_Init event handler. This LoadPageControls() method looked at a Session variable - if the session variable was set to a user control path, that user control was loaded into the PlaceHolder (using Page.LoadTemplate(path)- see An Extensive Examination of User Controls for more info on dynamically loading user controls). If, on the other hand, the session variable was not set (such as on the first page load before the user had clicked an item from the skmMenu), a default user control was loaded.
- When a skmMenu menu item is clicked the MenuItemClicked event fires. This colleague then created an event handler for this event, set the Session variable to the menu item's CommandName property and recalled LoadPageControls(), thereby loading in the appropriate user control based on the menu item clicked. And since this information was saved in the Session, if the dynamically loaded user control caused a postback, for example, upon reloading the “master“ page the appropriate user control would be re-added, thereby remembering the user control to display across postbacks. (Side note: this information could have also been serialized to view state. I don't know if the Session option was chosen out of ignorance over using ViewState or if it was chosen because he wanted the user to be able to return to the “master“ page sometime later and have the last loaded menu item for that user brought back up. One issue with Session, though, is that this “master“ page will not work for those who have cookies disabled in their browser.)
This approach worked well, or so it seemed. The user could click a skmMenu menu item and its appropriate user control would be loaded up in the PlaceHolder. If the user control caused a postback - say it contained a Button Web control - even when the postback was invoked, everything worked as expected - the page was reloaded and the appropriate user control was displayed.
There was, however, one problem. One dynamically loaded user control contained an editable DataGrid. If a user clicked the Edit button in the DataGrid, nothing happened. If he clicked it a second time, the DataGrid row became editable. There was a one-click pause, so to speak. Setting a breakpoint in the code, my colleague was able to determine that the event handler wasn't being invoked until the second (and all subsequent) clicks.
Anytime you are working with dynamic controls and events seem to go missing, the first thing to do is a View/Source. View the rendered HTML of the page that's sent to the browser before you do the action that doesn't trigger the server-side event (but should), and compare it to the HTML of the page right before the action does trigger the server-side event. Specifically, pay attention to the IDs of the Web controls since its the ID is what is passed back during postback to indicate what triggered the postback...
When examining the HTML after the editable DataGrid was loaded the first time, the DataGrid's ID was something like _ctrl1_DataGridID. After clicking the Edit button (which did not make the row editable), the returned HTML differed in that the DataGrid's ID was now _ctrl0_DataGridID. Note the change from 1 to 0 in the ID. Since the ID differed from the first time the DataGrid user control was loaded to all subsequent postbacks, the event wasn't picked up that first time, when the ID was still in flux.
Once we had identified that this was the problem (as it usually is when missing an event once, but not in subsequent tries), the next step was to figure out why this was happening. The problem could be traced back to the chain of events that unfolded when the skmMenu menu item was clicked. When the “master” page is visited for the very first time, the LoadPageControls() method fires during the Init event and, since there is no Session variable set, the default user control is loaded. Now, when the skmMenu menu item to load the editable DataGrid is clicked, the page posts back and the LoadPageControls() method runs first, and it says, “Hey, I still have no Session variable,” so it loads the default user control. Then, later in the page lifecycle, the MenuItemClicked event handler runs, which sets the Session variable and then recalls the LoadPageControls() method, which says, “Ah, yes, here is this Session variable, let me load the associated user control.”
The problem is, the PlaceHolder has had two controls added to its Controls collection, hence the reason why we get the _ctrl1 in the DataGrid's ID! (This happened even though the PlaceHolder's Controls collection was Clear()ed each time the LoadPageControls() method was called...) When the Edit button was clicked for the first time, the page posted back and the LoadPageControls() method was called from the Init event. This time it said, “I do have a Session variable set, so let me add the DataGrid user control.“ Note that in this sequence only one control is added to the PlaceHolder instead of the two that were added in the previous page lifecycle. Hence we get the _ctrl0 in the DataGrid's ID the second (and all subsequent) times. Furthermore, in this lifecycle, the _ctrl1 ID was what was sent back in the post headers, so when the Page class can't tie the event that caused the postback back to the DataGrid, and that was why the DataGrid's Edit button wasn't firing the DataGrid's EditCommand event on the first click.
The solution? Simply give a specific, named ID to the user control being dynamically added. That is, my colleague's code, before this change, looked like (very rough, I'm leaving out the Session variable check):
PlaceHolderID.Controls.Clear()
Dim c as Control = Page.LoadControl(Session(”pathToUC”))
PlaceHolderID.Controls.Add(c)
Everything worked fine and dandy once the code was changed to:
PlaceHolderID.Controls.Clear()
Dim c as Control = Page.LoadControl(Session(”pathToUC”))
c.ID = “someStaticString“
PlaceHolderID.Controls.Add(c)
With that change, the DataGrid's ID was always the same thing, something like someStaticString_DataGridID. As you can see, working with dynamic controls can introduce all sorts of hard to find and diagnose subtleties. To be able to debug dynamic control scenarios, the following things are paramount:
- Patience! :-) This is probably true for all debugging but moreso for debugging dynamic controls.
- A solid, air-tight, profound understanding of the ASP.NET page lifecycle. You need to know what events fire when, what methods run in response, and what happens when controls are added to the control hierarchy mid-way through the page's lifecycle. Some articles worth reading for more information on this include:
- A good understanding of the ASP.NET event model, and the ability to dig through the rendered HTML for a page and spot differences that may be causing problems.
Happy Programming!