Sunday, June 17, 2007

I spent some time before I find a pattern to work with dynamically created controls in ASP.NET that satisfies my requirements. I tried multiple approaches and faced multiple problems. If you like to avoid getting into the same problems see them here.

(All the code snippets here can be incrementally applied to sample ASPX file provided at the end)

It is not important to assign ID in this scenario, but I consider it as a good habit to do so. I will explain later why.

It is also important to configure your dynamically created control before you add it to the parent control. Once control is added to the parent control ViewState tracking mechanism is in place and every change you make to the control properties is recorded in the page ViewState. Moreover, you will fail to configure controls differently, if you attempt to set up the control differently on post-back. See http://weblogs.asp.net/infinitiesloop/archive/2006/08/03/Truly-Understanding-Viewstate.aspx for more details.

Using Dynamically Created Controls

Once you have added code that dynamically creates controls on your page you will likely need to get values posted by these controls. It is always not a good idea in ASP.NET to retrieve values posted by controls from the Request.Form collection. At least, you will have to guess the correct control ID. It is better to go the regular ASP.NET way.

Quite often you don't need to store references to your dynamically created controls. The alternative way is to add event handlers to your controls and get values from the sender in the event handler. The choice should depend on your needs.

Scenario 2: Changing Page Layout in Response to User Actions

Scenario 1 is a simple and reliable way to create pages with configurable layout. You can add plug-in modules, custom fields or change parts of the page depending on currently logged in user this way.

However, creating controls in the Init event handler is not always possible. You may need to load particular UserControl or generate custom input form depending on selected item in the drop-down list. The traditional way is to use MultiView control. MultiView approach is again simple, good and reliable unless you have to create hundreds of views inside or it takes quite long time instantiate each view.

For example, if you have multiple reports on your server and you need to create input boxes for report parameters when user select a report, it is not a good idea to create views with input for each report on page Init. Apparently, it is better to create only input boxes for active report template, but you cannot do it on page Init. It is not possible, because you don't know what user choice is at this moment. Every control on the page still has its default(original) value.

Handling Events

A natural way to respond to user actions is to handle the appropriate events. I have added a drop-down list to the sample page to choose the page layout.

This code should create one, two or three buttons on the page depending on the selected sample. If you replace stub event handler SampleDropDownList_SelectedIndexChanged in the sample ASPX with this code snippet, you can see that it indeed creates buttons as described. So, what is wrong? If you try clicking these buttons, they just disappear. Even worse, if you try to handle Click event for these buttons, the event is not fired.

and add this handler to buttons before adding them to c_placeholder.Controls collection:

button.Click += newEventHandler(Button_Click);

See: Scenario 2/Default_06.aspx

If you run the website, choose any sample in the drop-down and then click one of the 0, 1 or 2 buttons you will not get an exception. Why does it happen? There is nobody to fire Click event on postback. You have not created your buttons.

Re-creating Controls on Post-back

It is now obvious that controls created in response to user action must be re-created on each postback until they should not disappear because of user action. The first intention is to create them on page Init, but if you try following this way, you will find that c_sampleDropDownList control still has its default selected value. Unfortunately it means that Scenario 1 approach is not applicable here. You need to find a place to re-create controls where controls have their values loaded.

The first candidate for such a place is page Load event. (It is good enough, but not the best. It will be clear why later.) If you refactor existing code a little and move code which creates buttons into separate method you should come with something like:

If you run this code, you will see that it works almost as expected. First of all, page shows button "0" immediately as "Sample 1" is the default value in the drop-down. Moreover you get error page on attempt to click on this button. (Rare case when it is good to see the error page.) But if you try to change the value in the drop-down you will be disappointed. You get more controls than you expected. I you set a breakpoint in CreateButtons method you will see that execution hits this point twice after you change value in drop-down. A mere attempt to fix a problem by clearing c_placeholder.Controls in the CreateButtons shows that the problem is a little deeper. If you add to the beginning of CreateButtons

c_placeholder.Controls.Clear();

See: Scenario 2/Default_08.aspx

you will see that number of buttons on the page is correct, but they stopped working again. And it is interesting that they have stopped working for one click only. So, what happens?

Assigning ID to Dynamically Created Controls

If you view the source HTML before you click the not-working button and after you have clicked it, you will notice a small difference. The buttons have different HTML IDs before and after the post-back. I got ctl04 and ctl05 before the post-back and ctl02 and ctl03 after the post-back.

ASP.NET button recognizes click by checking for a value for its ID in the Request.Form collection. (In truth it happens differently and controls do not check Request.Form collection by themselves. Page passes post data to controls by their IDs and to controls that are registered to be notified about post data). ASP.NET does not fire Click event, because button ID has changed between the post-backs. The button you have clicked and the button you see after are different buttons for the ASP.NET.

As I said on the beginning, assigning ID to dynamically created controls is a good practice, but it can also lead to problems if misused. A sample code with ID assigned to dynamically created buttons goes here:

This time you get error on the first click even after you change the value in the drop-down.

The described approach is good enough for many cases, but it has its limitations as well. I will recap steps you have to follow to get dynamically created controls working, before showing more problems:

Create a method (CreateDynamicControls) which creates your controls (you may inspect values of other controls to decide which controls you should create).

Check that you assign all properties to the dynamic controls before adding controls to the placeholder.

Call your CreateDynamicControls method inc all event handlers that must change the layout of dynamic part of the page.

Store references to your controls in private fields if you need to access their values somewhere. (optionally)

Step 6 can be often omitted, but as you can change control values in the event handlers, it is better to re-create your controls once again.

More Hidden Problems

So, why is it necessary to cerate controls twice when page layout changes? Is it not enough to create controls for new page layout only and omit step 6 in the suggest scenario? To see what difference make step 6 I suggest replacing throwing an exception in Button_Click handler with the following code snippet:

If you click any of dynamically created buttons it becomes in bold and blue. Bold and blue style is reset by changing the value in the drop-down.

Now, if you try removing the call to CreateButtons in the SampleDropDownList_SelectedIndexChanged, you can see that dynamically created controls still work. The only noticeable difference is that bold and blue style is no longer reset by changing value in the drop-down. This behavior is explained by the fact that persisted ControlsViewState is removed from the collection once control has consumed it. When you create controls twice, controls consume ControlsViewState on the first time. Then they are created without ViewState second time. This behavior leads to the following question - "What if dynamically created controls after the post-back are different than original?" I have made minor changes to the last sample to illustrate it:

Note that for the "Sample 3" ImageButtons are created instead of Buttons. If you try changing value in the drop-down and clicking buttons you will occasionally receive the exception:

An error has occurred because a control with id 'c_button_0' could not be located or a different control is assigned to the same ID after postback. If the ID is not assigned, explicitly set the ID property of controls that raise postback events to avoid this error.

This is because you tried to feed Button with a view state from ImageButton. And that is why I said that re-creating control in page Load event is not the best place for doing this.

The simplest solution to the encountered problem is to use different IDs for ImageButtons and Buttons. If you try this you will find that it works. Usually it is not hard to follow this requirement, but if dynamically created controls come from different parts of the system and from different developers, it is easy to get unexpected behavior.

I would recommend going another way and keep everything more compliant with ASP.NET page lifecycle.

Creating Controls in CreateChildControls Method

To solve the problem with exceptions after change of the drop-down selected item, you need to create controls for old value of the drop-down first and then for the new value in the even handler. This first time could be on Init or LoadViewState phases of the page lifecycle.

Every ASP.NET control has a CreateChildControls method. It is intended to be used by control authors to create composite controls. The Page itself is a some kind of composite control. The CreateChildControls method is invoked when the EnsureChildControls method is invoked first time for the control. By default the CreateChildControls is called on PreRender phase if page is not in the postback mode and may be called before processing post-back data if page is in the postback mode.

If you try creating your controls in the CreateChildControls you have to get the old value somehow. In some cases, controls like TextBox or DropDownList hold their old values after view state has been restored, but this behavior is due to implementation details. You should not therefore rely on this behavior. Some other controls (TreeView, CheckBoxList) never store their value in the view state and therefore you rely on controls to provide old value. The typical solution is to store this value in property backed in the ViewState.

The property then shall be set in the change event and used to determine which controls should be created dynamically. Its value is already available in CreateChildControls on postback as it is called after the ViewState is loaded.

There are several pitfalls with this approach. CreateChildControls is first called before PreRender event is fired in non post-back case. Therefore, anytime you need to access your controls you have to call EnsureChildControls. Another problem arises if you call EnsureChildControls too early. You will not be able to create your controls until ViewState is restored.

If you attempt to implement the same inside the user control you may be lucky or may be not. It is possible that you will find that CreateChildControls is not called until PreRender phase. The solution is to call EnsureChildControls after (inside before existing) the LoadViewState method. Unfortunately LoadViewState method is only called if you saved anything on SaveViewState. To get it working I wrapped the result of the SaveViewState into the Pair object with second value null.

On of typical scenarios when you need to load user controls dynamically is to respond to change of selected node in the TreeView control or list box.

The sample application for the scenario 3 demonstrates how to use the describe technique to nest dynamically created controls and dynamically loaded user controls. UI consists of three parts: navigation style selection drop-down, left navigation panel and right content panel. The navigation style selection drop-down changes appearance of the navigation panel. The navigation panel allows you choosing what content should be displayed on the right. Again different nodes create different number of TextBoxes or Buttons on the right.

Application is composed from four ASP.NET files: Default.aspx, MasterWebUserControl.ascx, WebUserControl1.ascx and WebUserCorntrol2.ascx. (See full source code at the end).

Default.aspx is a simple sample showing how to load user controls dynamically. It always loads the same controls without any attempt to configure it.

WebUserControl1.ascx and WebUserControl2.ascx show that demonstrated approach works not only for web pages, but for user controls as well and even if user controls are instantiated dynamically. Internally these two user controls are very different. The first one is very simple and its behavior is controlled from the outside world. The Initialize method creates TextBoxes dynamically inside the user control. The second user control (WebUserControl2.ascx) is organized differently. It behaves like a standalone control and its content is controlled via NumberOfControls public property.

Internally the second control uses the same approach to dynamically created controls with CreateChildControls as described in scenario 2. You can set NumberOfControls property anytime and it will recreate controls if CreateChildControls has been already called.

The most interesting is MasterWebUserControl.ascx. It manages the whole appearance of the application. The dynamic layout of this user control is controlled by two parameters: list display mode and selected item in the list. Both these parameters are stored in the ViewState backed properties: Mode and SelectedItemValue correspondingly. The LoadViewState method is forced by saving wrapper Pair object into the ViewState.

CreateRightControl method loads one of the WebUserControl1.ascx or WebUserControl2.ascx depending on the item selected on the left. WebuserControl2.ascx knows how to recreate its dynamically created controls, so MasterWebUserControl.ascx does not re-create it in the case when only NumberOfControls needs to be changed.

The code snippet is too long to place it here, so I refer you to appendix or attachment.

Brief Summary

While its much easy to create web pages with static controls in ASP.NET, nothing prevents you from creating control dynamically and using the consistent approach for this purpose.

The consistent approach to creating controls dynamically may be following:

Create controls in the CreateChildControls method

Call EnsureChildControls in the LoadViewState

Wrap and unwrap view state in the Pair object to force calling LoadViewState

Save layout of dynamic part of the page int the properties backed in ViewState

I am currently working on dynamically loading a set of custom controls, and each of those controls dynamically loads checkbox inside a table.

I was able to use scenario 1 (loading controls on init) since I know beforehand how many controls I will need (it's dynamic so that it can vary without having to change the code).

I then used scenario 3 for the more complex matter of reconstructing the entire table, and checkboxes inside. It took a little while to make sure I was properly setting the IDs and recreating the objects, but it worked!