Tuesday, July 10, 2007

What's Wrong with Accordion Control?

Briefly

  1. Controls in a AccordionPane are not instantiated until PreRender
  2. Values of TextBoxes, CheckBoxes and other controls are not preserved between postbacks.

With More Details and Solution of the Problems

Accordion control is a nice visual control in the Ajax Control Toolkit which can be used similar to TabContainer control.  It allows user to switch between different pages of content. 

The very simple sample of how to use the Accordion control:

            <act:Accordion ID="Accordion1" runat="server">
<Panes>
<act:AccordionPane runat="server" ID="pane1">
<Header>
Header 1</Header>
<Content>
Content 1</Content>
</act:AccordionPane>
<act:AccordionPane runat="server" ID="AccordionPane1">
<Header>
Header 2</Header>
<Content>
Content 2</Content>
</act:AccordionPane>
</Panes>
</act:Accordion>

Content and Header are ITemplate properties of AccordionPane control.  Both these properties are decorated with

[TemplateInstance(TemplateInstance.Single)]

attribute, which makes controls declared inside this template accessible as page properties like any other controls on the page.  Thus if you have the following accordion on the web page

<act:Accordion ID="Accordion1" runat="server">
<Panes>
<act:AccordionPane runat="server" ID="pane1">
<Header>
Header 1</Header>
<Content>
<h2>
Test</h2>
<asp:CheckBox runat="server" ID="CheckBox1"
Text="CheckBox" />
</Content>
</act:AccordionPane>
</Panes>
</act:Accordion>

you should be able to write the following code:

    protected void Page_Load(object sender, EventArgs e)
{
if (CheckBox1.Checked) {
//
}
}
And indeed TemplateInstance.Single works and you can compile this code. However, when you attempt browsing your page you get:


Object reference not set to an instance of an object.

Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.

Exception Details: System.NullReferenceException: Object reference not set to an instance of an object.

Source Error:





Line 13:     protected void Page_Load(object sender, EventArgs e)
Line 14: {
Line 15: if (CheckBox1.Checked) {
Line 16: //
Line 17: }

 


Well, this is not what you expect from TemplateInstance.Single.  CheckBox1 is not instantiated on Page_Load.  Moreover, if you try handling CheckedChanged event, you will find that it does not fire.

<asp:CheckBox runat="server" ID="CheckBox1" Text="CheckBox" 
AutoPostBack=true OnCheckedChanged="CheckBox1_CheckedChanged" />

and even more, checkbox checked state is not preserved between postbacks.  (TextBox value always remains the same if you replace CheckBox with TextBox.)


So, what's wrong with Accordion and AccordionPane?


I could explain not instantiating controls before the OnLoad event by the fact that when page is loading for the first time CreateChildControls is first called in PreRender page lifecycle phase.  However, post data processing usually triggers CreateChildControls and any controls receiving post data are created.  So, accordion is different.


When response Html is rendered, client id of my checkbox control becomes ctl03_CheckBox1 and its name is ctl03$CheckBox1.  So, when ASP.NET processes post data it first looks for ctl03 control and then if it is found asks ctrl03 control to find CheckBox1 control.  Ctl03 control must be marked with INamingContainer.  If you enable page trace and look for Ctl03 control, you find that it is AccordionContentPanel control.  AccordionContentPanel is an internal control in the AccordionPane and it is naming container.


However, when I try to find ctl03 control in my page, I cannot find it until PreRender phase when AccordionPanes instantiate their templates an so does ASP.NET.


I looked into the source code. CreateChildControls is overridden in both Accordion and AccordionPane classes, but both these controls are not naming containers; while ASP.NET asks only naming containers to create their children.  ASP.NET would ask ctl03 to create its children if it had exist.


I verified that adding a INamingContainer marker interface to Accordion and AccordionPane class fixes all problems described above.  (Well, I am too lazy to check if it does not break data binding features).


However, as Accordion and AccordionPane controls are not naming containers in current build of Ajax Control Toolkit, workaround for these problems is required.


Workaround


If you have declaratively created accordion controls on your web page, add to your page_init handler the following:

        Accordion1.FindControl("nothing");
for each accordion on the page. It helps!
I posted a bug request to www.codeplex.com hoping this can be fixed: http://www.codeplex.com/AtlasControlToolkit/WorkItem/View.aspx?WorkItemId=11615
 

34 comments:

Anonymous said...

You are a genius! I have been trying to figure out why a reference error comes up when a standard asp control is inserted into the accordian. I saw work-arounds on other sites, but they do not explain why. I think your solution is the most simple of them all. Indeed, there is a bug and you found it.

Thanks,
Ty

Anonymous said...

Thanks for the help. I wish Microsoft would test these controls better.

Anonymous said...

Brilliant! Thanks for your work to find this workaround. It is the most elegant solution out there.

Anonymous said...

This work around seems to work for me, too. I've got a panel on each accordion pane and my data entry controls (textboxes, etc) in the panels. Adding the FindConrol for just the panels seemed to load all the child controls so that I didn't have to find each control.

Anonymous said...

Hi!
I've been looking for this problem al my weekend. And finally found your blog on monday morning.
i added your fix and it works great!
Thank you very much

Anonymous said...

Strong work, Yuriy. I was having this same issue and couldn't figure out why my, in this case, google map control, wasn't being recognized.

I've also been having issues with the ajax modal control not rendering a google map properly (i.e. it shows part of the map while the rest is grayed out). the map displays perfectly outside the modal but not when the modal is activated. do you know of a solution to this issue? Thanks.

Yuriy Solodkyy said...

Which control do you use to display Gogle Maps in ASP.NET?

Anonymous said...

Yuriy,

I have a databound accordion and am still having problems even though I have implemented your fix. I have an ImageButton in the header that loses its ItemCommand bind if I have to rebind the accordion (the users have the option to sort it). After rebind, I have to click the button twice to get the ItemCommand to fire. Have you encountered this problem and do you know of a workaround?

Many thanks

Yuriy Solodkyy said...

Unfortunately I am on vacation.and will be able to look into the problem only in a week

Anonymous said...

I have a DropDownListBox in one AccordionPane which databinds to an ObjectDataSource, it wouldn't bind. Do you a fix to this? many thanks.

Unknown said...

Another little tip allows you to do all the panes @ once.

foreach(AjaxControlToolkit.AccordionPane pan in Accordion1.Panes){
pan.FindControl("nothing");
}

Shaun

Anonymous said...

< asp:UpdatePanel ID="UpdatePanel4" runat="server">
< ContentTemplate>
< ajaxToolkit:Accordion ID="Accordion1" runat="server" AutoSize="limit" ContentCssClass="accContent"
FadeTransitions="true" FramesPerSecond="150" HeaderCssClass="accHeader" Height="100%"
SelectedIndex="0" TransitionDuration="250" Width="100%">
< Panes>
< ajaxToolkit:AccordionPane ID="apShipToMe" runat="server">
< Header>
< div class="accDiv" style="background-image: url(Assetts/Images/Buttons/accButton.gif);
background-repeat: no-repeat;">
< span class="accSpan">< a href="" onclick="return false" style="text-decoration: underline;"
title="Ship to Me">Ship to Me< /a> < /span>
< /div>
< /Header>
< Content>
< asp:Panel ID="Panel1" runat="server" Height="50px" Width="125px">
< table style="background-image: url(Assetts/Images/Layout/mailbg.gif); background-repeat: no-repeat;
position: static; height: 225px" width="215">
< tr>
< td align="left" style="width: 197px; height: 21px" valign="top">
< asp:RadioButton ID="rbMail1" runat="server" AutoPostBack="True" Enabled="False"
ForeColor="White" GroupName="mail" Text="PLEASE INKJET & MAIL DIRECTLY" Width="203px" />< /td>
< /tr>
< tr>
< td align="center" class="greenFontSmall" style="width: 224px; height: 20px" valign="middle">
< asp:TextBox ID="mailQty" runat="server" BorderStyle="None" CssClass="aspTextbox"
Enabled="False" Height="10px" Style="border-right: medium none; border-top: medium none;
margin-top: -2px; border-left: medium none; border-bottom: medium none" Width="178px"
AutoPostBack="True" CausesValidation="True" OnTextChanged="mailQty_TextChanged">0< /asp:TextBox>
< /td>
< /tr>
< tr>
< td align="center" class="greenFontSmall" style="width: 197px; height: 15px" valign="top">
< asp:RangeValidator ID="RangeValidator2" runat="server" ControlToValidate="mailQty"
CssClass="greenFontSmall" ErrorMessage="Amount must be less than min. amount."
ForeColor="" MaximumValue="10000000" MinimumValue="0">< /asp:RangeValidator>< /td>
< /tr>
< tr>
< td align="left" style="width: 197px; height: 21px" valign="top">
< asp:RadioButton ID="rbMail2" runat="server" AutoPostBack="True" Enabled="False"
ForeColor="White" GroupName="mail" Text="Presort Standard Mail" />< /td>
< /tr>
< tr>
< td align="left" style="width: 197px; height: 21px" valign="top">
< asp:RadioButton ID="rbMail3" runat="server" AutoPostBack="True" Enabled="False"
ForeColor="White" GroupName="mail" Text="Presort First Class" />< /td>
< /tr>
< tr>
< td align="left" style="width: 197px; height: 21px" valign="top">
< asp:RadioButton ID="rbMail4" runat="server" AutoPostBack="True" Checked="True" Enabled="False"
EnableTheming="True" ForeColor="White" GroupName="mail" Text="Standard First Class" />< /td>
< /tr>
< tr>
< td align="left" style="width: 197px; height: 45px" valign="top">
< div class="greenFontSmall" style="margin-left: 18px">
*Includes Postage, Duplicate Removal, CASS Certification, Water Seal, Ink Jetting,
and Handling!< /div>
< /td>
< /tr>
< tr>
< td align="left" style="width: 197px; height: 26px" valign="bottom">
< div style="font-size: 7pt; margin-left: 28px; color: white">
< span class="greenFontSmall">Mailing Guide < /span>| Additional Information< /div>
< /td>
< /tr>
< /table>
< /asp:Panel>
< ajaxToolkit:FilteredTextBoxExtender ID="FilteredTextBoxExtender1" runat="server"
FilterType="Numbers" TargetControlID="mailQty">
< /ajaxToolkit:FilteredTextBoxExtender>
< /Content>
< /ajaxToolkit:AccordionPane>
< ajaxToolkit:AccordionPane ID="apShipToClient" runat="server">
< Header>
< div class="accDiv" style="background-image: url(Assetts/Images/Buttons/accButton.gif);
background-repeat: no-repeat;">
< span class="accSpan">< a href="" onclick="return false" style="text-decoration: underline;">
Ship to my Client< /a> < /span>
< /div>
< /Header>
< Content>
THIS IS JUST TEXT TO HOLD THE PANEL
THIS IS JUST TEXT TO HOLD THE PANEL
THIS IS JUST TEXT TO HOLD THE PANEL
THIS IS JUST TEXT TO HOLD THE PANEL
THIS IS JUST TEXT TO HOLD THE PANEL
THIS IS JUST TEXT TO HOLD THE PANEL
THIS IS JUST TEXT TO HOLD THE PANEL
THIS IS JUST TEXT TO HOLD THE PANEL
< /Content>
< /ajaxToolkit:AccordionPane>
< ajaxToolkit:AccordionPane ID="apDirectMail" runat="server" ContentCssClass="" HeaderCssClass="">
< Header>
< div class="accDiv" style="background-image: url(Assetts/Images/Buttons/accButton.gif);
background-repeat: no-repeat;">
< span class="accSpan">< a href="" onclick="return false" style="text-decoration: underline;">
Direct Mailing Options< /a> < /span>
< /div>
< /Header>
< Content>
THIS IS JUST TEXT TO HOLD THE PANEL
THIS IS JUST TEXT TO HOLD THE PANEL
THIS IS JUST TEXT TO HOLD THE PANEL
THIS IS JUST TEXT TO HOLD THE PANEL
THIS IS JUST TEXT TO HOLD THE PANEL
THIS IS JUST TEXT TO HOLD THE PANEL
THIS IS JUST TEXT TO HOLD THE PANEL
THIS IS JUST TEXT TO HOLD THE PANEL
< /Content>
< /ajaxToolkit:AccordionPane>
< /Panes>
< /ajaxToolkit:Accordion>
< /ContentTemplate>
< Triggers>
< asp:AsyncPostBackTrigger ControlID="ddlShipping" EventName="SelectedIndexChanged" />
< /Triggers>
< /asp:UpdatePanel>

Anonymous said...

Sorry. Even with the work around, I still get an error.

If you could help... please email me at twhetstone@westcamp.com. I will also check back regularly.

Anonymous said...

God Bless you. Thanks a lot.

p said...

Wow! I love the accordion control, and now it's actually useable.

Thanks for the workaround!

Matthew R Hall said...

Greatly appreciate you posting this valuable tip Yuriy. Thank you!

Anonymous said...

I was beginning to lose my sanity over the Accordion control (which happens to be extremely useful, especially for "crammed" pages) and the problem that you have solved...
Thank you Yuriy, excellent job!

Anonymous said...

Oh my!!!! What a saving find! Thank you for resolving a mind bender I had to give up on. I had resorted to spraying various links above the accordian control before. I tried your trick and now am able to put everything in the pane (no pun intended) that it should be in. Major kudos to you.

Thanks!

Anonymous said...

Superb! Nice work fella!

miasni said...

Impressive. You saved my head XD.


Muchas gracias!!!

miasni said...
This comment has been removed by the author.
jswanson said...

Stellar sleuthing! I fought with this for an hour before I found your post. Works like a charm. Thank you!

Anonymous said...

Sorry, Your fix not working in my case, as i am using atlas tool kit.
Do u fix for atlas tool kit accordion?.

Anonymous said...

Man, you're my hero. Thank you!

Anonymous said...

Acutally, this did not solve entirely my problem: the naming container is still not reseted when rebinding.

The only way I found to reset it is to call Controls.Clear(). Unfortunately, other controls than the panes exist in the accordion Controls collection. So here is my hack of Accordion.cs to solve this:

internal void ClearPanes()
{
List<Control> noPaneControls = new List<Control>();
for (int i = Controls.Count - 1; i >= 0; i--)
{
if (!(Controls[i] is AccordionPane))
{
noPaneControls.Add(Controls[i]);
}
}
// Must call this to reset the naming container
Controls.Clear();
foreach (Control c in noPaneControls)
{
Controls.Add(c);
}
}

Anonymous said...

Hi All. Same for me, the solution didn't quite fix the problem for me, but went a whole way forward. I had nested Accordions, collapsiblePanels, Panels and then finally a Button in an Update Panel that was a trigger for an Image in another Update Panel outside of the 1st Accordion.

All the controls within the first Accordion appeared null due to the problem with the naming container mentioned in the above article. I had to declare each of the nested AJAX controls in turn, to then get the button control and assign it the same ID as the one in the update panel as a trigger:

protected override void OnInit(EventArgs e)
{
base.OnInit(e);
MyAccordion.FindControl("nothing"); //Force Top Level Accordion's Controls to Memory
ReportAccordian.FindControl("nothing"); //Force Next Level Accordion's Controls to Memory
CollapsiblePanelExtender3.FindControl("nothing"); //Force CollapsiblePanelExtender3's Controls to Memory

//Get Trigger Control and Register it
Control mycontrol = CollapsiblePanelExtender3.FindControl("ReportBtn");

foreach (AsyncPostBackTrigger apbt in UpdatePanel1.Triggers)
//Even though Trigger Control says ID='ReportBtn' actually it is 'TabContainer3$MyAccordion$..'
//and the Update Panel doesn't know it's called that, so we need to set UpdatePanel Trigger ID
//to the same.
{
if (mycontrol.UniqueID.EndsWith(apbt.ControlID))
{
apbt.ControlID = ReportBtn.UniqueID;
}
}
}

Hope this helps too...

Anonymous said...

You are AWESOME!! Thank you, thanks you!

Unknown said...

Thanks a Million....Cheers

Nick Vaidyanathan said...

Well done, Hero

I had the weirdest behavior, my accordion was working perfectly for 2 panes with 3 update panels (set to have children as triggers and conditional update) inside of them. Every time I had my AutoPostBack control fire its event inside of those panels it worked no problem. I tried to add a fourth UpdatePanel on a different pane, and for that particular panel (same properties and everything) the first trigger of the event wouldn't fire. This seems to have resolved the problem, but the behavior is still very (?_?)

Anonymous said...

You rock!!!! Thanks v much!!!

Anonymous said...

You are a superman of software industry

Anonymous said...

Cheers mate, really helpful

Spade said...

Thanks! You are the greatest.

Anonymous said...

AMAZING man!

You saved my life!

I've been stuck in that for weeks!

You are genius :)

Thank you soooooooooooooooo much.