Fixing TabContainer to work with dynamic TabPanels
Update: The 30 September 2009 release of the AjaxControlToolkit doesn't have the error that I fix here. My patch was applied in July and from September on the bug is gone in the official release as well. Good riddance! :)
==== Obsolete post follows
Update: On June 20th 2009, Codeplex notified me that the patch I did for the ACT has been applied. I haven't tested it yet, though. Get the latest source (not latest stable version) and you should be fine.
This post was updated on the 1st of July 2008 with some clearer explanations and some error corrections thanks to Santoé who pointed out some mistakes.
My ASP.Net app uses a TabContainer, with a TabPanel in the *x code and with additional TabPanels added dynamically in codebehind.
Well, I got a lot of errors so I've decided to debug and change the control in order to fix it.
Step 1: download the AjaxControlToolKit with source included and open the project locally.
First error : Specified argument was out of the range of valid values. Parameter name: index, somewhere in the TabPanelCollection indexer. The problem actually occurs in TabContainer in LoadClientState(string clientState) where there is a for (int i = 0; i < tabState.Length ; i++). It doesn't take into account the possibility that the number of Tabs and the number of values taken from the tabState can be different. So the code must look like this: for (int i = 0; i < tabState.Length && i < Tabs.Count; i++).
Step 2: In the AjaxControlToolkit\AjaxControlToolkit\Tabs\ folder there is a TabContainer.cs file. Change for (int i = 0; i < tabState.Length ; i++) to for (int i = 0; i < tabState.Length && i < Tabs.Count; i++).
The second error is actually a thrown error in the ActiveTabIndex property setter: if (value >= Tabs.Count) { throw new ArgumentOutOfRangeException("value"); }, but it all comes from this: if (Tabs.Count==0 && !_initialized), because it doesn't take into account the possibility that the Tabs.Count is smaller than the ActiveTabIndex, but not zero. So that should look like this: if (value >= Tabs.Count && !_initialized).
I've downloaded the latest AjaxControlToolKit (version Version 1.0.20229 - Feb 29 2008) and the scratched fix above doesn't seem to work anymore. Instead, try patching the ActiveTabIndex property like this:
In other words, remove the ArgumentException code block and move the condition inside the next block, where you set the real _activeTabIndex to the highest legal value, yet you put the real value in _cachedActiveTabIndex.
Step 3: in the AjaxControlToolkit\AjaxControlToolkit\Tabs\ folder there is a TabContainer.cs file. Change the ActiveTabIndex property with the code above.
The same thing must be done in Javascript, in the Tabs.js file, if you intend to use a TabContainer with no static tabs defined. In case you do that, you will get a javascript error "Microsoft JScript runtime error: Sys.ArgumentOutOfRangeException: Specified argument was out of the range of valid values.
Parameter name: value". The fix is to change the set_activeTabIndex function of the TabContainer in the file tabs.js to this:
Step 4: in the AjaxControlToolkit\AjaxControlToolkit\Tabs\ folder there is a tabs.js file. Change the set_activeTabIndex function with the code above.
This fixed it for me for now.
Step 5: Compile the now patched AjaxControlToolKit and use the resulting dll in your project instead of the default one.
As a reference, my test app does the following things:
==== Obsolete post follows
Update: On June 20th 2009, Codeplex notified me that the patch I did for the ACT has been applied. I haven't tested it yet, though. Get the latest source (not latest stable version) and you should be fine.
This post was updated on the 1st of July 2008 with some clearer explanations and some error corrections thanks to Santoé who pointed out some mistakes.
My ASP.Net app uses a TabContainer, with a TabPanel in the *x code and with additional TabPanels added dynamically in codebehind.
Well, I got a lot of errors so I've decided to debug and change the control in order to fix it.
Step 1: download the AjaxControlToolKit with source included and open the project locally.
First error : Specified argument was out of the range of valid values. Parameter name: index, somewhere in the TabPanelCollection indexer. The problem actually occurs in TabContainer in LoadClientState(string clientState) where there is a for (int i = 0; i < tabState.Length ; i++). It doesn't take into account the possibility that the number of Tabs and the number of values taken from the tabState can be different. So the code must look like this: for (int i = 0; i < tabState.Length && i < Tabs.Count; i++).
Step 2: In the AjaxControlToolkit\AjaxControlToolkit\Tabs\ folder there is a TabContainer.cs file. Change for (int i = 0; i < tabState.Length ; i++) to for (int i = 0; i < tabState.Length && i < Tabs.Count; i++).
I've downloaded the latest AjaxControlToolKit (version Version 1.0.20229 - Feb 29 2008) and the scratched fix above doesn't seem to work anymore. Instead, try patching the ActiveTabIndex property like this:
[DefaultValue(-1)]
[Category("Behavior")]
[ExtenderControlProperty]
[ClientPropertyName("activeTabIndex")]
public virtual int ActiveTabIndex
{
get
{
if (_cachedActiveTabIndex > -1)
{
return _cachedActiveTabIndex;
}
if (Tabs.Count == 0)
{
return -1;
}
return _activeTabIndex;
}
set
{
if (value < -1)
throw new ArgumentOutOfRangeException("value");
if (Tabs.Count == 0 && !_initialized)
{
_cachedActiveTabIndex = value;
}
else
{
if (ActiveTabIndex != value)
{
if (ActiveTabIndex != -1
&& ActiveTabIndex < Tabs.Count)
{
Tabs[ActiveTabIndex].Active = false;
}
if (value >= Tabs.Count)
{
_activeTabIndex = Tabs.Count-1;
_cachedActiveTabIndex = value;
}
else
{
_activeTabIndex = value;
_cachedActiveTabIndex = -1;
}
if (ActiveTabIndex != -1
&& ActiveTabIndex < Tabs.Count)
{
Tabs[ActiveTabIndex].Active = true;
}
}
}
}
}
In other words, remove the ArgumentException code block and move the condition inside the next block, where you set the real _activeTabIndex to the highest legal value, yet you put the real value in _cachedActiveTabIndex.
Step 3: in the AjaxControlToolkit\AjaxControlToolkit\Tabs\ folder there is a TabContainer.cs file. Change the ActiveTabIndex property with the code above.
The same thing must be done in Javascript, in the Tabs.js file, if you intend to use a TabContainer with no static tabs defined. In case you do that, you will get a javascript error "Microsoft JScript runtime error: Sys.ArgumentOutOfRangeException: Specified argument was out of the range of valid values.
Parameter name: value". The fix is to change the set_activeTabIndex function of the TabContainer in the file tabs.js to this:
set_activeTabIndex : function(value) {
if (!this.get_isInitialized()) {
this._cachedActiveTabIndex = value;
} else {
if (this._activeTabIndex != -1) {
this.get_tabs()[this._activeTabIndex]._set_active(false);
}
if (value < -1 || value >= this.get_tabs().length) {
this._activeTabIndex = this.get_tabs().length-1;
this._cachedActiveTabIndex=value;
} else {
this._activeTabIndex = value;
this._cachedActiveTabIndex=-1;
}
if (this._activeTabIndex != -1) {
this.get_tabs()[this._activeTabIndex]._set_active(true);
}
if (this._loaded) {
this.raiseActiveTabChanged();
}
this.raisePropertyChanged("activeTabIndex");
}
},
Step 4: in the AjaxControlToolkit\AjaxControlToolkit\Tabs\ folder there is a tabs.js file. Change the set_activeTabIndex function with the code above.
This fixed it for me for now.
Step 5: Compile the now patched AjaxControlToolKit and use the resulting dll in your project instead of the default one.
As a reference, my test app does the following things:
- Starts with a TabContainer with single TabPanel defined in the aspx
- Has a button that adds new tabs to the TabContainer dynamically on the Click event
- The panels have buttons in them that can be clicked
- The active tab must be preserved during postbacks
- The page must work both on synchronous and asynchronous postbacks
Comments
Thanks. Can we get the fixed code for the dll version 3.0.2
Jayaraman KumarHaven't been workin gin the area for quite some time. I am sorry I can't help you right now.
Sideritehi all, this is sunil. I am trying to remove a tab dynamically and adding them. but the newly added tab is not getting active on tabchanged event Plz. help me
sunilI gone through your blog... its amazing .... thanks ... whenever i tried to remove tab i am showing confirmation message, on click of "yes" i am getting "microsoft jscript runtime error 'null' is null or not an object" Please help me ...
MinakshiThis piece of code really helps man... it saved my life, thank you very very much.!!!!!!!!
SunilPTDownload latest version of AjaxControlToolkit http://ajaxcontroltoolkit.codeplex.com/releases/view/43475
Anonymoushttp://ajaxcontroltoolkit.codeplex.com/releases/view/43475 You can download the latest version of toolkit and problem will be resolved.
AnonymousI still get this error while any postback event occure like selecetedindex changg etc. Please help me. !!!!!!!!!
SwapnilIf it is an ASP.Net Button, yes it does. The thing that you see on web pages as an attribute (OnClick) in the code behind is just Click. A weird ASP.Net only convention. So you do something like *btnDynamic.Click+=method;* or you could even use lambda expressions if you use .Net 3.5 *btnDynamic.Click+=(sender,args)=>{ // do something };*
SideriteThanks Siderite. I think I understand what you said above but how do I add an event to a dynamically created button (i.e. "Click Me") and how do I handle it in the code behind? It's dynamically added but there is no click event on the button. -Chris
AnonymousIn the code above, you have the TabCount view state property to hold to the count of created tabs, so that you can recreate them in InitTabs. It's the principle I was telling you above. Since the Click event is after Load, you need to create the control in the Click event as well.
SideriteEvery time you put a control in the aspx or ascx, you actually instruct it to add that control in the init phase. It's like loading the aspx with a LoadXml method in the Init phase. Controls that are added to a control collection try to "catch up" with the state of their parent. So even if you add a control in the Load phase, it will still execute everything until and including the load phase. In order for events to fire, you need to have the control created after you postback, and in the Init or Load phases, not later. The OnClick event is later. That being said, it means that if you add a control dynamically in a click event, you need to create it in the click handler method (and have an ID), but if you want it to have events after you postback you also have to recreate any control you added in the Init or Load phases.
SideriteThis is great stuff, Thanks. A question though: How do you handle the Click event of a dynamically added button on one the dynamically added TabPanels? Say you want to save some data on a panel and the user clicks a save button on that panel. I suppose you could have one button and then keep tract of the active tab but you still need to reference text boxes, etc.
AnonymousActually, the problem is deeper than this. In the sources from July, the changes in my patch were added, I tested it myself. However, it seems somebody patched the patch. Somebody wanted that damn error throwing there, so they just added it again. I am sorry, but I can't really work on this right now. Hopefully in the coming weeks I will be able to test it everything and see what is going on there.
SideriteI just downloaded the latest source from "http://ajaxcontroltoolkit.codeplex.com/SourceControl/ListDownloadableCommits.aspx#DownloadLatest", and the fixes above have not been added as of today. I made the changes according to your suggestions and it works perfectly for me. Thank you for taking the time to let everyone about this.
QuintonThanks a lot, your post was Gold!! I hope they add this to the next official release...
AnonymousI just can say "THANK YOU"!
AnonymousThanks, It helped me a lot Regards, ManiX
ManiXthanks. it helps. page init worked for me !! protected void Page_Init(object sender, EventArgs e) { InitTabs(); }
Core Developmenthello, I saw your code and implemented the same in my project. But i made one change. ie call your InitTab(); in Page_Init that way you will get your dynamic TabPanel... hope this will solve your problem.. ashish
AnonymousEpic win dude, epic win.
Justin-CredibleExcellent work. Thanks you so much. Saved me a giant headache!
AnonymousThanks, your code was very helpfully. Excellent work!!!
Rep100Thanks you very much. Your code is excellent. I searched so many sites. But your help makes more useful to me.
it's MVMan, thanks a lot. It solved my problem. Does anybody know if this bug is reported to microsoft?
MarceloThe session is used for storing values, like strings, integers, serialized objects. You cannot really store a tab panel there. What you need to do is recreate the tab panel every time, based on values in the session.
SideriteHi Siderite, You have done a wonderful job. I am using IDs for each tab I dynamically create. Between postbacks I am putting the tabs in session so that I can build it back along with the new tab request. However, since, I am populating the tabs from session, the script handler is lost & an exception is thrown... Can you help me with this?
JannuDWell, look for me in the blog chat when I am at work, or leave me an email as a message in the same chat and I will contact you for the code sample. If you can make a small project demoing the bug, I can try to find a fix.
SideriteHmmm. Still seems like there's a problem here. I have a delete button on each tab that triggers a click event in codebehind, which removes the active TabPanel from the TabContainer. The problem crops up in subsequent postbacks (for instance, a 'Save' postback) when the deleted tab WASN'T the last tab in my container. The viewstate still appears to contain data (or at least placeholders) for that deleted tab, and inserts that data into the (now smaller) set of tabs. So a tab gets populated with the garbage viewstate data, and all of my valid tab viewstate data gets shifted to the next tab, pushing the last tab's data into oblivion. I'm probably explaining this poorly, but I suspect the issue lies in (or around) Siderite's code that loops through the tabstate, and uses the tab count as an upper limit. Anyone else experience this?
Corporate DogYou are all welcome. Creating new things is a lot more complicated than fixing bugs in existing products. Give credit where it's due.
SideriteAfter all this time since you posted your entry your still a live-saver. Worked like a charm! Amazing Microsoft still has not solved this bug. Perhaps they should hire you? ;)
QuiHey friend, if you were with me, i would really hug you to express my happyness. you saved my time and made me to feel happy. I have been trying for the solution for the past 2 days. Atlast you saved me thank you
ravindar (ravindar.thati@zoho.Will you marry me? I have been banging my head against the wall for a couple of days trying to integrate this tab control into dot net nuke. thanks for your help.
MmmmagicThank you, guys! :) Your comments make me write the blog. Has anyone tried the ActiveTabIndex=0 approach? (except, obviously, the poster)
SideriteYou are a hero!! thanks :-)
gordonThank You! I really hope you have submitted this for the next release. I thought that I was in for a couple of hours of trying to find the right value to ace in the ViewState, then I found your blog post. Thanks again.
da5idI found another way of fixing this. After adding or removing tabs dynamically, call this: this.tabContainer.ActiveTabIndex = 0;
AnonymousWell, yes, if your active tab is set to 5 and you delete the 5th tab, you will have problems there. The best solution, I think, is to make a check and see if the activetabindex is equal or greater than tab count and in that case automatically set it to tabcount-1. But that is, again, a tweak of the TabContainer sources.
Sideriteok. i'm going to answer my own question. when removing a tab, it generates a javascript error. to prevent this, you'll need to set the activeTab! if (TabContainerMain.Tabs.Count > 0) { TabContainerMain.Tabs.RemoveAt(0); TabCount--; if (TabContainerMain.Tabs.Count > 0) TabContainerMain.ActiveTab = TabContainerMain.Tabs[TabContainerMain.Tabs.Count - 1]; } hope this helps.
Anonymoushi. thanks. your article helped with the add tab issue. however, doing the remove throws a javascript error i'm executing tabcontrolmain.tabs.remove(tabcontrolmain.activetab) Sys.ArgumentOutOfRangeException : any ideas?
AnonymousThanks a ton !! It helped me get the dynamic tabs on my page work... Did anyone let the ajaxtoolkit developers know of this? It would be good if they fix this in their next release... Anyways Thanks again for your detailed explanation!
swapnaThis comment has been removed by the author.
santo2This comment has been removed by a blog administrator.
Anonymouswell, it does solve the problem of creating the dynamic tabs, but if you want to create them during init time you need to find a solution to remember the dynamic tab count since ViewState is not accessible in Init.
Sideritedoes this help solve the problem where dynamically generating tabs needs to occur in the page_init. I am building tabs based on the URL and my getUrl class occurs after the controls on the page init event
jon_sthanks a lot. You Saved my day. Wonderfull
lumpiI did a rebuild of the ajax toolkit and seems to work out right now. Stupid of me not to rebuild, but I'm relative new to .NET Thanks.
IronmanIt seems to me that you changed the project but did not refresh the DLL reference. Or maybe you referenced the DLL in Release and compiled in Debug or something like that. I haven't even installed 3.5 yet, so I don't know what the differences between the two versions of AjaxControlToolkit are. As soon as I will, I will probably update this post.
SideriteHi SideRite, With the latest toolkit for framework 3.5 I still got the problem. As you can read here http://www.codeplex.com/AtlasControlToolkit/WorkItem/View.aspx?WorkItemId=15658 there is someone else also complaining. I tried to modify the .cs and .js but then I got the error stating the source and the DLL are not the same. What should I do ... ?
IronmanThank you very much for your hint, the bug is still present in the current version of the toolkit...
AnonymousSorry, mate, I am up to my ears in boring work. Can you please post the relevant code so I can debug it and find a fix? That would significantly speed things up.
SideriteHi, thanks for this fantastic solution, but I'm also having the problem with the wrong tab count... Have you come to any conclusion about this already??
NunoI don't have the time to fix this rightaway, but I will try to do something about it. Keep checking this link.
SideriteMay many thanks to u.. its solved my problem, but i m alog geting one more problem, whenevr i create some tabs dynamically , i m geting a wrong tab count on any other event of page..
AnonymousAre we talking about the same... reality? :)
SideriteMan thanks, you really helped it out. You know I could have sword that the error was on my code because MS would never release something with a bug on it !!!!
Luis