How To: Lazy-load TabPanel's within the AjaxControlToolkit's TabContainer Control
I am currently working on a data-centric web application. The page I most recently worked on allows the user to browse data from six different tables (a primary table, and five other tables that have 1-M relationships to the rows in the primary table). A row from the primary table is displayed in a DetailsView. Below the DetailsView is a TabContainer control with five tabs; each tab contains a GridView that displays the related rows in the other five tables. Below is a screen shot of what the TabContainer portion of the page looks like.
Live Demo | Download Sample Site
When I first developed the page, I bound the DetailsView and each of the GridViews to the corresponding DataSources like I usually do. Everything worked great in our development environment. The grids filled nicely and I could move around from tab to tab with no problems. However, when we moved the page into our production environment loading the page was terribly slow. In production, we have n-times more data and the queries that filled the various GridViews performed much slower than they were in our development environment. And because just loading the page causes all 6 of the queries to execute, there were some serious performance issues with this page.
To solve the problem I moved each of the GridViews in the TabContainer into its own UpdatePanel and delayed loading them until the user clicked on the tab they wanted to view. I set the default tab to the one I felt that users were most often interested in. This way only 2 queries were executed the first time the page loaded, and the other 4 queries would execute on-demand so only the users who were interested in the data for the tab would pay the price of waiting for the query. The following are the steps I took to accomplish this ...
Define the DataSources
You can create your DataSources just like you normally would. In my example I am using SqlDataSource's to populate the grids. The example I created uses the Customer, Orders and Employees tables from the Northwind database and the DataSources are defined as follows.
<asp:SqlDataSource ID="sqldsCustomers" runat="server" /> <asp:SqlDataSource ID="sqldsOrders" runat="server" /> <asp:SqlDataSource ID="sqldsEmployees" runat="server" />
Add the TabContainer to the Page
Next, you can add the markup for the TabContainer control to your page. Also, add the TabPanel as children of the TabContainer. For each of the TabPanels you can set the HeaderText to the text value you would like displayed for each of the tabs. Because the three tabs in my sample are for Customers, Orders and Employees data, this is exactly what I named my tabs.
<ajaxToolkit:TabContainer ID="tabContainer" runat="server"> <ajaxToolkit:TabPanel runat="server" HeaderText="Customers"> <ContentTemplate>...</ContentTemplate> </ajaxToolkit:TabPanel> <ajaxToolkit:TabPanel runat="server" HeaderText="Orders"> <ContentTemplate>...</ContentTemplate> </ajaxToolkit:TabPanel> <ajaxToolkit:TabPanel runat="server" HeaderText="Employees"> <ContentTemplate>...</ContentTemplate> </ajaxToolkit:TabPanel> </ajaxToolkit:TabContainer>
Add the GidView to the TabPanel's ContentTemplate
Once you have created the TabPanels, you can go ahead and populate the ContentTemplates with the GridView's the tab will be displaying. Also, since we already know we will be needing each of these GridViews to be loaded via async-postbacks we can go ahead and create them as children of UpdatePanels.
<ajaxToolkit:TabContainer ID="tabContainer" runat="server"> <ajaxToolkit:TabPanel runat="server" HeaderText="Customers"> <ContentTemplate> <asp:UpdatePanel runat="server" UpdateMode="Conditional"> <ContentTemplate> <asp:GridView runat="server" AllowPaging="true" AllowSorting="true" PageSize="10" DataSourceID="sqldsCustomers" /> </ContentTemplate> </asp:UpdatePanel> </ContentTemplate> </ajaxToolkit:TabPanel> <ajaxToolkit:TabPanel runat="server" HeaderText="Orders"> <ContentTemplate> <asp:UpdatePanel runat="server" UpdateMode="Conditional"> <ContentTemplate> <asp:GridView runat="server" AllowPaging="true" AllowSorting="true" PageSize="10" DataSourceID="sqldsOrders" /> </ContentTemplate> </asp:UpdatePanel> </ContentTemplate> </ajaxToolkit:TabPanel> <ajaxToolkit:TabPanel runat="server" HeaderText="Employees"> <ContentTemplate> <asp:UpdatePanel runat="server" UpdateMode="Conditional"> <ContentTemplate> <asp:GridView runat="server" AllowPaging="true" AllowSorting="true" PageSize="10" DataSourceID="sqldsEmployees" /> </ContentTemplate> </asp:UpdatePanel> </ContentTemplate> </ajaxToolkit:TabPanel> </ajaxToolkit:TabContainer>
Add Triggers for each of the UpdatePanels
Next, we will wire the triggers for each UpdatePanel's. In my example, the Tab containing the Customers is displayed first. I want this content bound when the page is first loaded, and this UpdatePanel should only be refreshed when a postback is triggered from a child control. For the other non-default tabs, I also want them to refresh when a postback is triggered from a child control, but I also want to define an explicit trigger that I can fire when the user clicks on the Tab. To accomplish this I add a couple of hidden HTML input elements to the page and register these as explicit AsyncPostBackTrigger's for the other two UpdatePanels. These elements will be used to cause the postback via __doPostBack from a javascript function created in the next step. This is a similar technique that I used in a previous post.
<input id="btnOrdersTrigger" runat="server" type="button" style="display:none" onserverclick="BtnOrdersTrigger_Click" /> <asp:UpdatePanel runat="server" UpdateMode="Conditional"> <Triggers> <asp:AsyncPostBackTrigger ControlID="btnOrdersTrigger" /> </Triggers> <ContentTemplate> <asp:GridView ID="gvOrders" runat="server" /> </ContentTemplate> </asp:UpdatePanel>
Handle the OnClientActiveTabChanged Event
The TabContainer control exposes a OnClientActiveTabChanged event that is fired on the client when the active tab index is changed. We are going to hook into this event in order to determine when to force a Tab's content to update. First, you can set this attribute to the name of the javascript function you want to trigger the load. In my sample this function is called clientActiveTabChanged.
<ajaxToolkit:TabContainer ID="tabContainer" runat="server" ActiveTabIndex="0" OnClientActiveTabChanged="clientActiveTabChanged" />
Next, we can go ahead and implement this function as follows. First, we need to determine what the active tab is, once we know that we can decide if we need to trigger the loading of the Tab's content. The TabContainer javascript object exposes a get_activeTabIndex() property that returns the index of the currently active tab. In my example, if the index is 1 I need to load the Orders tab and if it is 2 I need to load the Employees tab. Then, we can use the $get function to see if the DOM element for the tabs GridView is already loaded, if it is we don't need to do anything, otherwise we need to start an asyncpostback so the GridView can be properly loaded. The complete javascript function that does this is as follows.
function clientActiveTabChanged(sender, args) { // see if the table elements for the grids exist yet var isTab1Loaded = $get('<%= this.gvOrders.ClientID %>'); var isTab2Loaded = $get('<%= this.gvEmployees.ClientID %>'); // if the tab does not exist and it is the active tab, // trigger the async-postback if(!isTab1Loaded && sender.get_activeTabIndex() == 1){ // load tab1 __doPostBack('btnOrdersTrigger', ''); } else if(!isTab2Loaded && sender.get_activeTabIndex() == 2){ // load tab2 __doPostBack('btnEmployeesTrigger', ''); } }
Handle the ServerClick events of the UpdatePanel triggers
Next, we need to manually force the binding of data to the GridViews. This is done by handling the ServerClick event of the input buttons for both the Orders and Employees GridViews. This is done as follows:
/// <summary> /// /// </summary> /// <param name="sender"></param> /// <param name="args"></param> protected void BtnOrdersTrigger_Click(object sender, EventArgs args) { this.gvOrders.Visible = true; this.gvOrders.DataBind(); } /// <summary> /// /// </summary> /// <param name="sender"></param> /// <param name="args"></param> protected void BtnEmployeesTrigger_Click(object sender, EventArgs args) { this.gvEmployees.Visible = true; this.gvEmployees.DataBind(); }
In addition to this we need to make sure that databinding does not occur when the page is first loaded. This can be done by setting the Visible property of the GridViews in the Orders and Employees tabs to false. Controls that have a Visible value of false are not rendered and will not be databound.
<asp:GridView ID="gvEmployees" runat="server" Visible="false" />
Handle the pageLoad, beginRequest and endRequest Events for Displaying a Progress Indicator
Finally, the last step is to wire up some sort of progress indicator so the user can see that their action has caused the content within the tab to update. In my example I am doing this by displaying an animated gif as a progress indicator. I have a few other posts (here and here) on this blog describing how this can be done so I will not explain it here. You can also download the sample site to view the source for the complete page.
That's it, Enjoy!
Comments
Hi Matt,
i tried to implement it to extensible panel extender, but without much success.
can you tell me what needs to be changed for it to work?
thanks
Great Article you solved 1 of my big problem
How to with master page?
Great Article!
I kept having it post back the form, but not fire the other supporting server side events. I finally figured out adding .UniqueID to the __doPostBack got it to work.
__doPostBack('', '');
Matt,
Great post, this is exactly what I'm looking for. However, everytime the I click on a tab it seems to post back but looks like it reloads the page and does not load the other gridview. Any ideas?
Thanks!
Nice, just what I need!
I second the comment about using the postback button's ClientID or UniqueID if your server event isn't being fired. Had me puzzled all morning...
Hi Matt,
thank you it's useful information you post. I have a question. What if I would like to use programmatically databinding. I mean drop the gridview in the panel and assign the member and dataset to datasource in code-behind. The intellisence doesn't show the gridview control (C#). Do you have any suggestions?
cheers, mesut
How can I add a dynamic tab using code behind?
here is my codes,
AjaxControlToolkit.TabPanel thePanel = new AjaxControlToolkit.TabPanel();
thePanel.HeaderText = "New Tab"; thePanel.ContentTemplate = Page.LoadTemplate("holeInfo.ascx");
tabHoleContainer.Controls.Add(thePanel);
but when I was trying to add the second tab, I received error message "Specified argument was out of the range of valid values. Parameter name:index"
Thanks.
@Wei
Try creating your TabPanels dynamically within the Page_Init method.
Your article helped me with my work today, sending my thanks.
Nice article.. just what i wanted to do.. but i was wondering if it possible to assign the header text.. based on the userId
Thanks for posting. Anyone know what the "$get(" javascript function is all about? I've never seen that before looking at the ajax tab control. Is it just a string parse function? Thanks.
its like document.getElementById(id)
Does any one know how can i load a user control (.ascx) in the content template of the Tab panel programatically.
Berseth,
Good post! I'm trying to use your idea in a web user control but i found some problems. Can you help me?
The first one is about the input control id. Since the moment we are in a user control context the code
__doPostBack('btnOrdersTrigger', '');probably must be changed to
__doPostBack('&greater%=this.btnOrdersTrigger.ClientID %&lesser', '');am i right?
The second one is about the onserverclick event handler name declared in the input controls. My web user control doesn´t call the handlers. Is it because i am in a user control context and so i need to make some changes?
thanks in advance!
Just to reiterate the __doPostBack problem that a few people have highlighted.
To fix,
change:
__doPostBack('btnOrdersTrigger', '');
to:
__doPostBack('LESSTHANSIGN%= btnOrdersTrigger.UniqueID %GREATERTHANSIGN','');
Hi Matt,
I want to say thank you for your this article, it makes me understand the way to use Ajax control toolkit more clearly. I'm looking for your new great Ajax articles.
Thank you and best regards
Tu
I did it in a slightly different way:
1. Put the tab container in an update panel with the default settings
and and make sure the tab container has
OnClientActiveTabChanged="ActiveTabChanged"
2. Add this only as a client-side function
function ActiveTabChanged(sender, e) {
__doPostBack(sender.get_id(), sender.get_activeTabIndex());
}
This is it. Now all you have to do is use a select..case statement int he server side activetabchanged method to grab the currently active tab and do the appropriate databinds
Hey Tu Dang Tuan
I liked the "simpler" method of wrapping a update panel around the Tab Container.
This really helped me out.
Also thanks to Matt for discussing this very important topic.
I avoided the Tab Control until I can implemenent efficient data binding when a tab panel is clicked and not doing all the queries at page load time.
Happy day to all, Paul
Matt,
Thanks for the article. When you have time could you explain how to load .ascx controls on tab clicks? I have a situation that prevents me from statically declaring the tab panels and content templates; my tabs are built from a list of nav items, and I don't know how many nav items there will be (or which ones) at design time. So, I need to build the tabs on init, but then lazy load ascx controls into dynamically created tab panels each time a tab is clicked. If I've been clear enough such that you understand what I am trying to do, would you be able to explain to me how I can do this?
Thanks,
Chris
Matt,
Your posts on the AjaxControlToolkit have been extremely helpful. I am using the Tab extender. On ActiveTabChanged I am evaluating information about the data on the old (leaving) tab. If certain conditions are not met, I want to stop the tab change and revert to the original tab. However, at the point of ActiveTabChanged, the tab has already changed, and I have not found a way to programmatically change the tab index back.
Is there a way to stop a tab change or to programmatically change a tab?
Thanks,
kw.
Hi
thanks for the tutorial...I had some issues both with modal pop up and lazy loading with respect to the UI in IE 7. Did anyone else face the same problem ? This is the tag i have in my master page :
Matt,
Excellent article. I am using your idea in one of my project..Everything works fine except one problem...I have ajax tab control with exact same functionality described in above article inside user control which is on master page and update panel inside content page...Everything works fine if i navigate first through all tabs in master page and then do some activity inside content page..but whenever I do some activity inside content page and then try to click one one of the tabpanel then it causes full postback...Further, I found out that after __dopostback("tab1",,) pagerequestmanager.beginrequest is never called..
Do you know how can i fix this problem? Any help will be greatly appreciated..
Thanks.
Thanks... it's very helpfull
Anyone figure out the masterpage issue?
thanx,its wonderfull
Hi,
I'm using almost the same thing but I'm loading the ASCX controls dynamic. The problem is that on the production server sometimes my responses from ajax are not put into the update panel. What i mean is that sometimes I got not or another or a wrong ajax response back on my updatepanel on a tabpage. It was working on casini but not on my server with IIS 6.0 and win2k3.
I am creating my controls dynamically, including the htmlinputbuttons and the triggers. Everything works except it will not trigger the sub in the "code-behind" to set the gridview to databind and be visable. Anyone have any ideas? Really beating my head against the wall on this one, so any suggestions would be appreciated. Thanks.
I have a tab container with 5 tabs, but I want the first tab just load, the rest of the tabs can be loaded but befor that user click on tab and after loading the first tab, I mean I dont went user be waiting for loading other tabs, how ever cab see the first tab fast.
Thansx
i tried to use matts code, but instead of gridview i try to lazyload reportviewer-controls. it all seems to work well... the data is rendered correctly and in firefox all is ok but in ie there is an "object expected"-exception after all data is rendered (visualstudio shows the exception in MicrosoftAjaxWebForms.js at scrollingsection).
does anyone know this problem and knows what to do?
I wrote this in .Net 2.0 and everthing worked fine, but the project was recently converted to 3.5 and the tabcontainer went berzerk. It seems that the postback called triggered by the tab index changing gets fired many times, causing lots of weird problems. Any suggestions?