
Introduction
This Tab Control is designed for ASP.NET applications. Although you can find many tab controls for ASP.NET from different 3rd parties but the beauty of this control is that its source code reveals internal working of ASP.NET server controls. Once you understand these deep internals, you can write your own professional controls just like 3rd party controls.
Background
In ASP.NET 1.1 and 2.0 there is no tab control. People use IE web controls or 3rd party controls for tab control functionality. Some people write their own logic for tab control by putting their contents in <DIV>
tag and changes it visibility when tab header is click. Even you can use ASP.NET 2.0 MultiView control for designing tab control and switches Views by using its ActiveViewIndex
property. But the TabsView
control is written from the ground means you can change it’s working according to your specific needs.
Using the code
You can add different tab pages in Tabs property of TabsView control. The code below shows how to add tab pages in the control.
<cc1:TabsView ID="TabsView1" runat="server" CurrentTabIndex="0"
SelectedTabCSSClass="SelectedStyle"
UnSelectedTabCSSClass="UnSelectedStyle" >
<Tabs>
<cc1:TabPage runat="server" ID="t1" Text="Karachi">
<asp:TextBox ID="TextBox1" runat="server">Marko Cash&Carry
</asp:TextBox>
<asp:TextBox ID="TextBox2" runat="server"></asp:TextBox>
This is Karachi the heart of Pakistan.
</cc1:TabPage>
<cc1:TabPage runat="server" ID="t2" Text="Islamabad">
<asp:TextBox ID="TextBox15" runat="server">1234 S</asp:TextBox>
<asp:Button ID="Button2" runat="server" Text="Click Me" />
This is Islamabad the capital of Pakistan. <br />
<asp:Label ID="Label1" runat="server" Text="Lux" Font-Bold="True">
</asp:Label>
</cc1:TabPage>
<cc1:TabPage runat="server" ID="t3" Text="Lahore">
This is Lahore the second largest city of Pakistan.
</cc1:TabPage>
</Tabs>
</cc1:TabsView>
You can add tab pages using SmartTags in VS2005 IDE. Simply right click on the control and click show smart tags.


Known Issues
- When you add new tab page from the smart tag, you have to assign ID property of new tab page on your own.
- Whenever you drag new server control on the designer, a unique id of the control is not guaranteed to be generated. Therefore, you have to assign your unique id to control.
How the Controls Works
TabsView control is a Composite Server control. A composite server control is a server control that contains other server controls which are know as its child controls. Internally TabsView control contains Table and Panel server controls. In ASP.NET, composite server control is represented by CompositeControl class. TabsView control is derived from this class.
public class TabsView : CompositeControl
TabsView class contains Designer attribute which represents the designer which is responsible for providing design time functionality to TabsView control.
Designer(typeof(TabViewDesigner))
We create actual child controls in override CreateChildControls
() method. This method is called whenever the post back is performed or when the page is first loaded. Whenever you are developing composite server control, always create your child controls in this method. Important thing here is that all child controls should have unique ID in order to avoid naming conflicts. Thanks to the INamingContainer
interface which does all the work required for generating the Unique ID of the controls. CompositeControl
class already implement this interface so there is no need to implement this interface in our code.
TabsView control contains two portions:
- Header: Containing tab pages heading. Each header is represented by an instance of
LinkButton
server control. Each heading is enclosed in table cell for proper alignment.
- Contents: Containing content of tab pages. Contents are represented by an instance of
TabPage
class.
Header Rendering
The TabsView
control needs to store the IDs of each tab page header and tab page content. One client side their IDs are stored in arrays. The name of these IDs are Control ID qualified meaning if your TabsView has an ID TabsView1 than will be tabButtons_TabsView1 (ID of headers) and tabContents_TabsView1 (ID of contents).This is necessary as a web page contain more than one instance of TabsView server control.
Header array:
var tabButtons_TabsView1 = new Array("TabsView1_tp0","TabsView1_tp1",
"TabsView1_tp2");
This is due to courtesy of the CreateTabHeaders
helper method. This method accepts a Table parameter, a parent table which the control whole alignment of TabsView controls. The contents of above java script array are stored in a variable arrBtns which is an instance of the StringBuilder
class.
StringBuilder arrBtns = new StringBuilder();
I used Table tblTabs for controlling the alignment of header.
Table tblTabs = new Table();
tblTabs.CellPadding = tblTabs.CellSpacing = 0;
We make sure that CurrentTabIndex property should be less than total no of tab pages. This is done in the VerifyTabIndex
method.
private void VerifyTabIndex()
{
if (CurrentTabIndex >= Tabs.Count)
throw new Exception("Invalid Tab Index");
}
This method is called in CreateTabHeaders
method in order to make sure the correct tab page index of TabsView
control. Than we traverse each tab page which is stored in a Tabs collection property, and add LinkButton
in a Table cell. The CommandArgument
of LinkButton
is initialized with 0 based indexes.
VerifyTabIndex();
foreach (TabPage tp in Tabs)
{
tc = new TableCell();
lk = new LinkButton();
lk.ID = "tp" + i.ToString();
lk.Text = " " + tp.Text + " ";
lk.CommandArgument = i.ToString();
In case of AutoPostBack mode we make sure that current tab page is set to focus after post back. This is done as follows
if (AutoPostBack) lk.Click += new EventHandler(lk_Click);
In the lk_Click
method I called a java script function SelectTab
which set focus to current tab.
void lk_Click(object sender, EventArgs e)
{
LinkButton lk = (LinkButton)sender;
this.CurrentTabIndex = Convert.ToInt32(lk.CommandArgument);
this.Page.ClientScript.RegisterStartupScript(this.Page.GetType(),
"SelectTab_LinkButton", "<script language='JavaScript'>SelectTab("
+ CurrentTabIndex +
",'" + this.ClientID + "','" + this.ClientID + "_hf" + "','" +
UnSelectedTabCSSClass + "','" + SelectedTabCSSClass + "')" +
"</script>");
}
In case of no post back we simply call SelectTab javascript method on OnClientClick property of LinkButton. The hidden field instance hf is used to store the current tab index of TabsView control.
lk.OnClientClick = "return OnTabClick(this," + i.ToString() + ",'" +
this.ClientID + "','" + this.ClientID + "_" + hf.ClientID + "','" +
UnSelectedTabCSSClass + "','" + SelectedTabCSSClass + "');";
We than add link button into the table cell, wrap the table cell alignment and returns a string representation of arrBtns
from the function.
tc.Controls.Add(lk);
tr.Cells.Add(tc);
++i;
}
tblTabs.Rows.Add(tr);
tc = new TableCell();
tc.Controls.Add(tblTabs);
tc.Controls.Add(hf);
tr = new TableRow();
tr.Cells.Add(tc);
tbl.Rows.Add(tr);
return arrBtns.ToString();
}
Contents Rendering
In CreateTabContents
method we are accepting the parent table parameter that is responsible for controlling the whole layout of TabsView
control. Similar to header rendering this function contains tblContents
an instance of Table which controls the layout of contents. In the foreach
loop we are adding each tab page into the cell of table and appending the id of each tab page in StringBuilder instance arrTabPages
.
foreach (TabPage tp in Tabs)
{
tpId = this.ClientID + "_" + tp.ID;
tc.Controls.Add(tp);
if (i == tabs.Count - 1)
arrTabPages.AppendFormat("\"{0}\"", tpId);
else
arrTabPages.AppendFormat("\"{0}\",", tpId);
++i;
}
Child Controls Creation
The CreateTabHeaders
and CreateTabContents
are called from CreateChildControls
overriden method.These functions return the string array containing the id’s of header link buttons and tab pages. Using these id’s we register the java script array declaration ClientScript.RegisterArrayDeclaration
method.
js = CreateTabHeaders(ref tbl);
this.Page.ClientScript.RegisterArrayDeclaration("tabButtons_"
+ this.ClientID, js);
js = CreateTabContents(ref tbl);
this.Page.ClientScript.RegisterArrayDeclaration("tabContents_" +
this.ClientID, js);
Now the tricky part of the code, how the TabsView
control know the current index when user moves between different tabs without any post back. This is why we used hidden field instance hf so that we can store the index in the hidden using java script (TabsView.js OnTabClick
function)
document.getElementById(hfId).value = i;
We read the value of this hidden field using Request object. You see from the code that I used following if condition before reading the Request object.
if ( !this.DesignMode )
This is because in a designer the Request objects is null therefore, reading Request object in this case produces error. We use Control base class DesignMode
property for checking this condition. You can also use HttpContext.Current != null
here.
Properties.
- Tabs: Tabs is a collection property. Whenever you use collection property the most appropriate approach is to write your own custom collection class. Although use can also use built in collections like
ArrayList
,Stack
etc. I implemented Tabs as Inner default property. Whenever you use inner property, you have to specify its persistence mode. Without the persistence mode ASP.NET does not know how to persist these properties. You can not see this property in property window through the courtesy of Browsable(false) attribute.
[
PersistenceMode(PersistenceMode.InnerDefaultProperty),
DefaultValue(null) ,
Browsable(false)
]
public virtual TabPageCollection Tabs
{
get
{
if (tabs == null)
{
tabs = new TabPageCollection(this);
}
return tabs;
}
}
Every tab page is inherited from Panel Server control. Therefore, all properties of Panel control are applicable to individual tab page. For example you can create scrollable tab page.
<cc1:TabPage runat="server" ID="t3" Text="Text" ScrollBars="Auto">
<!-—your contents-->
</cc1:TabPage>
CurrentTabIndex
: Get the current tab index. The set part is fairly simple. In the get part I raise TabSelectionChangingObject event when the new index and the previous index are not same
SelectedTabCSSClass
and UnSelectedTabCSSClass
are simple and they use ViewState to persist their values across postback.
Events
- §
TabSelectionChangingHandler
: Whenever you support events in your server control the recommended method is to use add and remove keywords.Every server control maintains a list of delegates.This list contain all events associated with server control.This list is expose through Events property. You use AddHandler and RemoveHandler to attach and remove events to server controls.
public event TabSelectionChangingHandler TabSelectionChanging
{
add { Events.AddHandler(TabSelectionChangingObject, value); }
remove { Events.RemoveHandler(TabSelectionChangingObject, value); }
}
Designer Working
TabsView designer is written using the TabsViewDesigner class.This class is inherited from CompositeControlDesigner which provides the desin time behavior for composite server control.To understand its working considere the following steps.
First of all you need to capture an instance of actual composite server control. This is done in overridden Initialize method.
public override void Initialize(IComponent component)
{
base.Initialize(component);
tabView = (TabsView)component;
}
Than you create the child controls of TabsView control in overridden method CreateChildControls.For each part of the control we have to associate an id so that we can know about which region is actually clicked. I associate different ids for TabsView header and single id for each TabsView content region because only single content is active in the designer.
if (tabViewTable.Rows.Count > 0)
{
headerTable = (Table)tabViewTable.Rows[0].Cells[0].Controls[0];
if (headerTable != null)
{
int i;
for (i = 0; i < headerTable.Rows[0].Cells.Count; i++)
headerTable.Rows[0].Cells[i].Attributes[
DesignerRegion.DesignerRegionAttributeName] = i.ToString();
contentsTable = (Table)tabViewTable.Rows[1].Cells[0].Controls[0];
contentsTable.Rows[0].Cells[0].Attributes[
DesignerRegion.DesignerRegionAttributeName] = i.ToString();
}
}
Than handle designer onclick
event. This is called when you click on the TabsView header region. To make sure that we click on header I check the name of the header.
if (e.Region.Name.IndexOf("Header") == -1)
return;
if (e.Region != null)
{
currentTabIndex = tabView.CurrentTabIndex =
int.Parse(e.Region.Name.Substring("Header".Length));
base.UpdateDesignTimeHtml();
}
Than you need to tell about designer region and editable designer region. Our headers are designer region and content tabs are editable designer region. After specifying this I select the current tab with regions[tabView.CurrentTabIndex].Highlight = true;
Table tabViewTable = (Table)tabView.Controls[0];
int i = 0, designerRegionIndex = 0;
string designTimeHtml = "";
if (tabViewTable.Rows.Count > 0)
{
Table headerTable = (Table)tabViewTable.Rows[0].Cells[0].Controls[0];
for (i = 0; i < headerTable.Rows[0].Cells.Count; i++)
{
regions.Add(new DesignerRegion(this, "Header" +
designerRegionIndex.ToString()));
++designerRegionIndex;
}
Table contentTable = (Table)tabViewTable.Rows[1].Cells[0].Controls[0];
{
EditableDesignerRegion editableRegion =
new EditableDesignerRegion(this,
"Content" + tabView.CurrentTabIndex, false);
++designerRegionIndex;
editableRegion.UserData = i;
regions.Add(editableRegion);
}
if (tabView.Tabs.Count > 0)
regions[tabView.CurrentTabIndex].Highlight = true;
designTimeHtml = base.GetDesignTimeHtml(regions);
}
else designTimeHtml = GetDesignTimeHtml();
return designTimeHtml;
Provide persistence among individual tabs using overridden GetEditableDesignerRegionContent and SetEditableDesignerRegionContent.I persist the individual tab content with ControlPersister.PersistControl(tb, host).
public override string GetEditableDesignerRegionContent(
EditableDesignerRegion region)
{
IDesignerHost host = (IDesignerHost)Component.Site.GetService(
typeof(IDesignerHost));
if (host != null)
{
TabPage tb;
tb = (TabPage)tabView.Tabs[tabView.CurrentTabIndex];
return ControlPersister.PersistControl(tb, host);
}
return String
For every change in individual tab content we have to update the TabsView control.The SetEditableDesignerRegionContent
does the same. It parses the individual tab contents from its string representation and update the TabsView
control.
public override void SetEditableDesignerRegionContent(
EditableDesignerRegion region, string content)
{
if (content == null)
return;
IDesignerHost host =
(IDesignerHost)Component.Site.GetService(typeof(IDesignerHost));
if (host != null)
{
TabPage view = (TabPage)ControlParser.ParseControl(host, content);
if (view != null)
{
int tabIndex = int.Parse(region.Name.Substring("Content".Length ));
tabView.Tabs.RemoveAt(tabIndex);
tabView.Tabs.AddAt(tabIndex, view);
}
}
}
Adding Custom Action List
To add new panel in smart tags we have to override the ActionLists
property and need to create an instance of DesignerActionListCollection
class.
public override DesignerActionItemCollection GetSortedActionItems()
{
if (items == null)
{
items = new DesignerActionItemCollection();
items.Add(new DesignerActionMethodItem(this, "CreateTabPage",
"Add Tab Page", true));
}
return items;
}
Every individual item in this panel is represented by instance of DesignerActionMethodItem
class. Individual panel items are associated together to form a panel by a call to the GetSortedActionItems
method of the DesignerActionList
class.
public override DesignerActionItemCollection GetSortedActionItems()
{
if (items == null)
{
items = new DesignerActionItemCollection();
items.Add(new DesignerActionMethodItem(this, "CreateTabPage",
"Add Tab Page", true));
}
return items;
}
Whenever the Add Tab Page command is executed it calls a CreateTabPage
method defined in the class. This method in turns add new tab page callback.
public void CreateTabPage()
{
TabsView tabView = (TabsView)_parent.Component;
TransactedChangeCallback toCall =
new TransactedChangeCallback(AddTabPage);
ControlDesigner.InvokeTransactedChange(tabView, toCall, null,
"Add Tab Page");
}
Here we need to understand the concept of designer transactions. Whenever, you add new tab page using smart tag it performs and atomic designer transaction which adds new tab page. Whenever, you undo this action it will simply rollback your last action resulting the removal of last added tab page. You can see the Undo option in your Edit menu as show below:
