.NET (295) administrative (41) Ajax (42) AngularJS (2) ASP.NET (144) bicycle (2) books (180) browser (8) C# (133) cars (1) chess (28) CodePlex (10) Coma (8) database (47) deployment (3) Entity Framework (2) essay (110) flash/shockwave (2) flex (1) food (3) friend (2) game (20) idea (5) IIS (8) javascript (82) LInQ (2) Linux (6) management (4) manga (42) misc (669) mobile (1) movies (90) MsAccess (1) murder (2) music (64) mysql (1) news (99) permanent (1) personal (68) PHP (1) physics (2) picture (307) places (12) politics (13) programming (501) rant (120) religion (3) science (43) Sharepoint (3) software (58) space (1) T4 (2) technology (11) Test Driven Development (4) translation (2) VB (2) video (97) Visual Studio (44) web design (45) Windows API (8) Windows Forms (3) Windows Server (4) WPF/Silverlight (63) XML (11)

Thursday, July 19, 2007

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:

[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


Here is the code for the page

using System;
using System.Web.UI;
using System.Web.UI.WebControls;
using AjaxControlToolkit;

public partial class _Default : Page
{
private int? _tabCount;

/// <summary>
/// Keep in ViewState the number of dynamically added tabs
/// </summary>
public int TabCount
{
get
{
if (_tabCount == null)
{
if (ViewState["TabCount"] == null)
TabCount = 0;
else
TabCount = (int) ViewState["TabCount"];
}
return _tabCount.Value;
}
set
{
_tabCount = value;
ViewState["TabCount"] = value;
}
}

protected void Page_Load(object sender, EventArgs e)
{
InitTabs();
}

/// <summary>
/// Add the dynamical tabs after each postback
/// </summary>
private void InitTabs()
{
for (int c = 0; c < TabCount; c++)
AddPanel();
}

/// <summary>
/// Dynamically add a panel to the TabContainer
/// </summary>
private void AddPanel()
{
TabPanel tp = new TabPanel();
tp.HeaderText = "Test Dinamic";
TextBox tb = new TextBox();
Button btn = new Button();
btn.Text = "Click me!";
tp.Controls.Add(btn);
tp.Controls.Add(tb);
TabContainer1.Tabs.Add(tp);
}

/// <summary>
/// Click event to add a new panel
/// and update the TabCount property
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
protected void btnAddPanel_Click(object sender, EventArgs e)
{
AddPanel();
// do this if you didn't have any staticly defined
// tabs or else the dynamic tabs will be invisible
If (TabCount==0) TabContainer1.ActiveTabIndex=0;
TabCount++;
}
}

55 comments:

Luis said...

Man 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 !!!!

Siderite said...

Are we talking about the same... reality? :)

Anonymous said...

May 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..

Siderite said...

I don't have the time to fix this rightaway, but I will try to do something about it. Keep checking this link.

Nuno said...

Hi,

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??

Siderite said...

Sorry, 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.

Anonymous said...

Thank you very much for your hint, the bug is still present in the current version of the toolkit...

Ironman said...

Hi 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 ... ?

Siderite said...

It 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.

Ironman said...

I 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.

lumpi said...

thanks a lot. You Saved my day. Wonderfull

jon_s said...

does 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

Siderite said...

well, 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.

santo2 said...
This comment has been removed by the author.
swapna said...

Thanks 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!

Anonymous said...

hi. 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?

Anonymous said...

ok. 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.

Siderite said...

Well, 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.

Anonymous said...

I found another way of fixing this. After adding or removing tabs dynamically, call this:

this.tabContainer.ActiveTabIndex = 0;

da5id said...

Thank 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.

gordon said...

You are a hero!!

thanks :-)

Siderite said...

Thank you, guys! :) Your comments make me write the blog.

Has anyone tried the ActiveTabIndex=0 approach? (except, obviously, the poster)

Mmmmagic said...

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.

ravindar (ravindar.thati@zoho.com) said...

Hey 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

Qui said...

After 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? ;)

Siderite said...

You are all welcome. Creating new things is a lot more complicated than fixing bugs in existing products. Give credit where it's due.

Corporate Dog said...

Hmmm. 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?

Siderite said...

Well, 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.

JannuD said...

Hi 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?

Siderite said...

The 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.

Marcelo said...

Man, thanks a lot. It solved my problem. Does anybody know if this bug is reported to microsoft?

it's MV said...

Thanks you very much. Your code is excellent. I searched so many sites.

But your help makes more useful to me.

Rep100 said...

Thanks, your code was very helpfully.
Excellent work!!!

Anonymous said...

Excellent work. Thanks you so much. Saved me a giant headache!

Justin-Credible said...

Epic win dude, epic win.

Anonymous said...

hello,

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

Core Development said...

thanks. it helps.

page init worked for me !!

protected void Page_Init(object sender, EventArgs e)
{
InitTabs();
}

ManiX said...

Thanks, It helped me a lot
Regards,
ManiX

Anonymous said...

I just can say "THANK YOU"!

Anonymous said...

Thanks a lot, your post was Gold!! I hope they add this to the next official release...

Quinton said...

I 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.

Siderite said...

Actually, 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.

Anonymous said...

This 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.

Siderite said...

Every 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.

Siderite said...

In 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.

Anonymous said...

Thanks 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

Siderite said...

If 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
};

Swapnil said...

I still get this error while any postback event occure like selecetedindex changg etc.

Please help me. !!!!!!!!!

Anonymous said...

http://ajaxcontroltoolkit.codeplex.com/releases/view/43475

You can download the latest version of toolkit and problem will be resolved.

Anonymous said...

Download latest version of AjaxControlToolkit

http://ajaxcontroltoolkit.codeplex.com/releases/view/43475

SunilPT said...

This piece of code really helps man... it saved my life, thank you very very much.!!!!!!!!

Minakshi said...

I 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 ...

sunil said...

hi 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

Siderite said...

Haven't been workin gin the area for quite some time. I am sorry I can't help you right now.

Jayaraman Kumar said...

Thanks. Can we get the fixed code for the dll version 3.0.2