What's Wrong with Accordion Control?
Briefly
- Controls in a AccordionPane are not instantiated until PreRender
- 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:
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.Object reference not set to an instance of an object.
Exception Details: System.NullReferenceException: Object reference not set to an instance of an object.
Source Error:
|
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:
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
Thanks for the help. I wish Microsoft would test these controls better.
Brilliant! Thanks for your work to find this workaround. It is the most elegant solution out there.
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.
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
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.
Which control do you use to display Gogle Maps in ASP.NET?
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
Unfortunately I am on vacation.and will be able to look into the problem only in a week
I have a DropDownListBox in one AccordionPane which databinds to an ObjectDataSource, it wouldn't bind. Do you a fix to this? many thanks.
Another little tip allows you to do all the panes @ once.
foreach(AjaxControlToolkit.AccordionPane pan in Accordion1.Panes){
pan.FindControl("nothing");
}
Shaun
< 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>
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.
God Bless you. Thanks a lot.
Wow! I love the accordion control, and now it's actually useable.
Thanks for the workaround!
Greatly appreciate you posting this valuable tip Yuriy. Thank you!
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!
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!
Superb! Nice work fella!
Impressive. You saved my head XD.
Muchas gracias!!!
Stellar sleuthing! I fought with this for an hour before I found your post. Works like a charm. Thank you!
Sorry, Your fix not working in my case, as i am using atlas tool kit.
Do u fix for atlas tool kit accordion?.
Man, you're my hero. Thank you!
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);
}
}
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...
You are AWESOME!! Thank you, thanks you!
Thanks a Million....Cheers
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 (?_?)
You rock!!!! Thanks v much!!!
You are a superman of software industry
Cheers mate, really helpful
Thanks! You are the greatest.
AMAZING man!
You saved my life!
I've been stuck in that for weeks!
You are genius :)
Thank you soooooooooooooooo much.
Post a Comment