Tuesday, June 26, 2007

Configuring Dynamically Created User Controls Containing UpdatePanel

How Is UpdatePanel Different?

UpdatePanel is a rare ASP.NET (actually ASP.NET AJAX Extensions) control which benefits from the TemplateInstanceAttribute and especially TemplateInstance.Single case.  Its ContentTemplate property is decorated with [TemplateInstance(TemplateInstance.Single)] attribute.  This setting allows accessing declaratively created controls in the ContentTemplate via page fields generated by ASP.NET

The PreInit event is not intended for setting up any controls, but it is nice place to see a difference between Panel and UpdatePanel

If you have the following controls on your page:

        <asp:ScriptManager runat="server" ID="c_scriptManager">
</asp:ScriptManager>
<div>
<asp:Label runat="server" ID="c_label"></asp:Label>
<asp:UpdatePanel runat="server" ID="c_updatePanel">
<ContentTemplate>
<asp:Button runat="server" ID="c_buttonForTest"
Text="Just a Button" />
</ContentTemplate>
</asp:UpdatePanel>
<asp:Panel runat="server" ID="c_panel">
<asp:Button runat="server" ID="c_buttonForTest2"
Text="Just a Test" />
</asp:Panel>
</div>

and the following code behind:

    protected override void OnInit(EventArgs e) {
base.OnInit(e);
if (c_buttonForTest != null) {
c_label.Text += "c_buttonForTest ";
}
if (c_buttonForTest2 != null) {
c_label.Text += "c_buttonForTest2 ";
}
}

you will notice that label output is just c_buttonForTest2.  What does it mean?   It means that UpdatePanel does not instantiate its ContentTemplate until after the OnInit phase. 


So, be careful.  Public page fields holding references to controls might be yet un-initialized before the OnInit phase and it is not a good idea to access controls in PreInit phase.  You may get "Object reference not set to an instance of an object" exception.


Where Is It Important?


As I said earlier it is recommended to set up dynamically created controls before they are added to the parent control Controls collection.  This is necessary to avoid storing these settings in the ViewState.  ASP.NET starts tracking ViewState changes immediately after the OnInit event has fired.


This code snippet is used to add a button to the placeholder control:

               TextBox textBox = new TextBox();
textBox.ID = "c_textBox";
textBox.ForeColor = Color.Green;
c_placeholder.Controls.Add(textBox);

It follows recommendation and sets control properties before it is added to the placeholder.  The same is possible for user controls loaded from the .ascx file:

                WebUserControl3 userControl = 
(WebUserControl3)LoadControl(@"~/Scenario 3/WebUserControl3.ascx");
userControl.ID = "c_rightUserControl";
userControl.ImportantColor = Color.BlueViolet;
c_bodyPlaceHolder.Controls.Add(userControl);

This sample also works for WebSuserControl3.ImportantColor implemented as:

    public Color ImportantColor {
get {
return c_importantLabel.ForeColor;
}
set {
c_importantLabel.ForeColor = value;

}
}

and c_importantLabel being simple static control inside the user control like:

<%@ Control Language="C#" ClassName="WebUserControl3" %>
<h2>
WebUserControl3</h2>
<p>
<asp:Label ID="c_importantLabel" runat="server" Text="Important Label"></asp:Label></p>

However, if you anytime decide to move c_importantLabel inside the UpdatePanel, you will get "Object reference not set to an instance of an object" exception.


User control OnInit fires inside the call to c_bodyPlaceHolder.Controls.Add(userControl) and c_importantLabel gets its value their. 


What Is the Correct Way to Create Controls Dynamically?


I posted a long story about the problems you can face while creating controls dynamically and how to avoid these problems.  One of the important conclusions was to set control properties before adding them to the parent control Controls collection.  As it is shown here, it is not good for user controls containing UpatePanel. 


MSDN says that Init event is raised after all controls have been initialized. 



Init: Raised after all controls have been initialized and any skin settings have been applied. Use this event to read or initialize control properties.


The same applies to user controls.

So, approach demonstrated in my previous post is a little incorrect.  It works fine almost everywhere, but there are cases when it does not.  It is necessary to move initialization of user control properties to its Init event handler.  C# 2,0 anonymous methods are very helpful in this case.  The problem is fixed by wrapping user control initialization in:

                WebUserControl3 userControl = 
(WebUserControl3)LoadControl(@"~/Scenario 3/WebUserControl3.ascx");
userControl.ID = "c_rightUserControl";
userControl.Init += delegate {
userControl.ImportantColor = Color.BlueViolet;
};
c_bodyPlaceHolder.Controls.Add(userControl);

This code works regardless of whether c_importantLabel is inside the UpdatePanel or not.  The c_importantLabel field is already initialized when Init event fires.


Since the old approach works almost in all cases, I do not think it is necessary to wrap initialization of any control in an anonymous method.  However,  I would do it for initialization of any user control from the outside.


I have updated the sample application from the post about Dynamically Created Controls in ASP.NET to use UpdatePanels everywhere.  The only other change is using anonymous method to initialize WebUserControl1.


Attachment: DynamicallyCreatedControlsAjax.zip


Alternative Point of View: Bug in User Control


An alternative point of view on the same problem can be: "User control shall be ready to handle assignment to its properties before its children are completely initialized". 


The converted sample application demonstrates that WebUserControl2 which follows this principle works fine without any changes and allows initialization of its properties before the Init event.


Anyway, you may have methods in your user control returning internal controls and they will return null before Init event.


What about Other Controls?


TabPanel and AccordionPane are similar to UpdatePanel in their behavior. 

4 comments:

Andres V said...

I've created user controls in the way you suggested and they are created fine. The problem I'm facing is when trying to create more than one control, in other words, I have a page with a button, every time the button is pressed I would need to add an user control into the update panel (inside a place holder or table within the update panel). This is not working, because every time the button is pressed the controls are missed, so I always have one control created. How can I preserve the controls created between async post backs? hope this explanation is clear enough.

Yuriy Solodkyy said...

See my post about "dynamically created controls: dynamic rows sample" in this blog. You need to load your controls into the placeholder on each postback.

Andres V said...

Hi, it's me again, first I want to say thank you cuz I already implemented the dynamic user controls with your help,
now I'm facing another problem and I want to see if you can help me.
I have some User Controls and them will be populated from different web services, each one of the web services has its own response time.
I need to load the screen as fast as I can, also users don't want to wait until the entire data is pulled to see the page, so I need to start showing the controls on the page as soon as I get them populated from the web services.
what I did was to create threads for each one of the web services calls, in that way I can call all the web services at the same time and start receiving data to populate the user controls.
I'm doing this when the page loads. The user controls are inside an update panel.
The problem is that the page doesn't show anything until the page load event finishes. I tried with response.flush() method with no luck, also I've tried to manually update the panel content each time a single thread finishes using the UpdatePanel1.Update() method but I'm still not luck.
Also sometimes, the page loads but user controls are not populated, usually the ones that belong to the second or third threat.
hopefully you have faced something similar before and you can help me with it.

Scotty said...

Hi,

Fabulous demonstration!

I've noticed that in your example when you click from one item in the listbox or treeview to another item, there are two postbacks. One with the "Old" count and one with the "New" count.

My implementation of this is inserting records into a database in one particular situation and I'm ending up with two record inserted because of the what I explained above.

Can you help? How would you suggest I isolate the first vs the second postback?

Thanks!