Thursday 19 July 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:

  1. 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 !!!!

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

    ReplyDelete
  3. 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..

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

    ReplyDelete
  5. 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??

    ReplyDelete
  6. 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.

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

    ReplyDelete
  8. 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 ... ?

    ReplyDelete
  9. 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.

    ReplyDelete
  10. 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.

    ReplyDelete
  11. thanks a lot. You Saved my day. Wonderfull

    ReplyDelete
  12. 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

    ReplyDelete
  13. 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.

    ReplyDelete
  14. This comment has been removed by a blog administrator.

    ReplyDelete
  15. 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!

    ReplyDelete
  16. 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?

    ReplyDelete
  17. 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.

    ReplyDelete
  18. 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.

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

    this.tabContainer.ActiveTabIndex = 0;

    ReplyDelete
  20. 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.

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

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

    ReplyDelete
  22. 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.

    ReplyDelete
  23. ravindar (ravindar.thati@zoho.com)28 November 2008 at 06:18

    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

    ReplyDelete
  24. 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? ;)

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

    ReplyDelete
  26. 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?

    ReplyDelete
  27. 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.

    ReplyDelete
  28. 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?

    ReplyDelete
  29. 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.

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

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

    But your help makes more useful to me.

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

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

    ReplyDelete
  34. 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

    ReplyDelete
  35. thanks. it helps.

    page init worked for me !!

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

    ReplyDelete
  36. Thanks, It helped me a lot
    Regards,
    ManiX

    ReplyDelete
  37. I just can say "THANK YOU"!

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

    ReplyDelete
  39. 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.

    ReplyDelete
  40. 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.

    ReplyDelete
  41. 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.

    ReplyDelete
  42. 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.

    ReplyDelete
  43. 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.

    ReplyDelete
  44. 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

    ReplyDelete
  45. 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
    };

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

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

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

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

    ReplyDelete
  48. Download latest version of AjaxControlToolkit

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

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

    ReplyDelete
  50. 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 ...

    ReplyDelete
  51. 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

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

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

    ReplyDelete