Introduction
It was always in my mind, why not provide a simple web tab control, something that developers can extend to their needs and requirements. For sure, there are a lot of third party controls providing rich and advanced tab controls, but I couldn't find much articles done on this subject, so far.
Splitting web form controls is really a necessity when you have a large number of controls inside the form, also it helps organizing forms according to specific categories.
Throughout this article, I will focus more on the control designer, facilitating the developer to provide a similar look and feel as the Windows Forms Tab
control. While giving the look and feel at runtime is an obvious requirement, it is just a matter of playing with the HTML table cell colors and some images, which will help in providing the 3D look.
Background
Simply, a tab control consists of two things: tab titles and tab body. In my solution, the tab control is represented as a table, with cells containing the titles and tab bodies, similar to the following table:
|
Body of tab 1 |
Body of tab 2 |
.. |
Body of tab n |
And, this exactly is how the tab control is represented at run time, with the difference that only the active tab body will be visible, whereas all other tabs will be hidden.
Deign and runtime view of our tab control
Basically, the tab control will consist of a tab page collection, MyTabPageCollection
, which is a collection of MyTabPage
controls. This control contains two main properties, which are the tab title, Title
, and the tab body, TabBody
of type ITemplate
. The System.Web.UI.ITemplate
will allow designing and holding other controls.
public class MyTabPage : System.Web.UI.WebControls.PlaceHolder
{
private string _title = "";
private ITemplate _tabBody;
public string Title
{
get { return _title; }
set { _title = value; }
}
[
PersistenceMode(PersistenceMode.InnerProperty),
DefaultValue(null),
Browsable(false)
]
public virtual ITemplate TabBody
{
get { return _tabBody; }
set { _tabBody = value; }
}
}
The tab control MyTabControl
is a sub class of CompositeControl
, which holds all the tabs, and contains some properties to hold the active tab at design time through the CurrentDesignTab
property, and at run time through the SelectedTab
property. These properties are used to hold the index of the active tab. The tab control designer, MyTabControlDesigner
, will switch between the tab templates and activate them based on this index. Multi-tabs are kept in the TabPages
collection of type MyTabPageCollection
; this collection will allow defining many tabs. The MyTabControl
implements two major methods: OnPreRender
and CreateChildControls
.
protected override void OnPreRender(EventArgs e)
{
base.OnPreRender(e);
if (DesignMode)
{
_tabPages[_currentDesignTab].TabBody.InstantiateIn(this);
}
}
protected override void CreateChildControls()
{
Controls.Clear();
Table tabControlTable = new Table();
tabControlTable.CellSpacing = 1;
tabControlTable.CellPadding = 0;
tabControlTable.BorderStyle = BorderStyle;
tabControlTable.Width = this.Width;
tabControlTable.Height = this.Height;
tabControlTable.BackColor = ColorTranslator.FromHtml("inactiveborder");
tabControlTable.Attributes.Add("ActiveTabIdx", _selectedTab.ToString());
BuildTitles(tabControlTable);
BuildContentRows(tabControlTable);
Controls.Add(tabControlTable);
}
At design time, the OnPreRender
will instantiate the active tab represented by the template, with an index _currentDesignTab
in the tab control, whereas the CreateChildControls
method is responsible for drawing the control contents, according to the layout described earlier. Also, it will expose the ActiveTabIdx
to the attributes of the table which represents the tabs container. The tab titles will be rendered through the BuildTitles
method, which iterates through the TabPages
collection and draws the titles as table cells. Also, it will create the OnClick
event handler call to the JavaScript function ShowTab
, which is responsible for showing the active tab.
Our tab control is represented by the table which holds the titles and tab contents. After rendering the titles, we need to render the tab contents. Well, the BuildContentRows
method is responsible for rendering the contents, where each tab body is represented by a cell in the table. Here, notice that since the first row of the table will hold the titles then, the tab content row indexing will start from 1, so, the active tab is located at the row index, ActiveTabIdx+1
.
For design time view, we don’t need to instantiate all tabs, but only the active tab, as you can see in the method BuildContentRows
.
private void BuildContentRows(Table tabControlTable)
{
if (DesignMode)
{
TableRow contentRow = new TableRow();
TableCell contentCell = BuildContentCell(contentRow);
_tabPages[_currentDesignTab].TabBody.InstantiateIn(contentCell);
tabControlTable.Rows.Add(contentRow);
}
else
{
int counter = 0;
foreach (MyTabPage tabPage in _tabPages)
{
TableRow contentRow = new TableRow();
TableCell contentCell = BuildContentCell(contentRow);
if (tabPage.TabBody != null)
{
tabPage.TabBody.InstantiateIn(contentCell);
}
if (_selectedTab == counter)
{
contentRow.Style["display"] = "block";
}
else
{
contentRow.Style["display"] = "none";
}
contentRow.Cells.Add(contentCell);
tabControlTable.Rows.Add(contentRow);
counter++;
}
}
}
For runtime view, the procedure will iterate throw all the tabs and instantiate the tab bodies in the content rows.
Switching between tabs at the client side
When the user clicks on a tab title, a client-side event handler, ShowTab
, will switch the tabs knowing the clicked tab and its current index; the inline comments provide a detailed description of how this procedure works.
<script type="text/javascript" language="javascript">
function ShowTab(tabTitleCell, idx)
{
var tabsTable = tabTitleCell.parentElement.parentElement.parentElement;
var activeTabIdx = Number(tabsTable.getAttribute("ActiveTabIdx"));
tabsTable.rows[0].cells[activeTabIdx].style["backgroundColor"] = "inactiveborder";
tabsTable.rows[0].cells[idx].style["backgroundColor"] = "darkgray";
tabsTable.rows[activeTabIdx + 1].style.display = "none";
tabsTable.rows[idx + 1].style.display = "";
tabsTable.setAttribute("ActiveTabIdx", idx);
}
<script>
Managing tab control templates and switching the design view
The tab control designer should take care of:
- Declaring the design area.
- Switching between tabs.
- Persisting tab bodies, and viewing them in the designer.
The CompositeControlDesigner
provides the basic designer, allowing to control the editable regions and creating the child controls. The MyTabControlDesigner
inherits the CompositeControlDesigner
, to support the tab control design view. The designer contains a private reference, tabControl
, to the tab control. This reference gets initialized in the Initialize
method. This reference could be used through the designer logic to refer to the tab control.
Declaring the design area
At the time of getting the design time HTML, the overridden method GetDesignTimeHtml
will add design regions for all header cells; the region name is prefixed with HEADER_PREFIX
, and extended with the tab page index. This way, we can extract the tab index from the region name. Then, a design region is created with similar naming formats as the titles prefixed by CONTENT_PREFIX
and the index of the current active tab.
public override String GetDesignTimeHtml(DesignerRegionCollection regions)
{
int i = 0;
foreach (MyTabPage tabPage in tabControl.TabPages)
{
regions.Add(new DesignerRegion(this,
HEADER_PREFIX + i.ToString()));
i++;
}
EditableDesignerRegion editableRegion =
new EditableDesignerRegion(this,
CONTENT_PREFIX +
tabControl.CurrentDesignTab, false);
regions.Add(editableRegion);
regions[tabControl.CurrentDesignTab].Highlight = true;
return base.GetDesignTimeHtml();
}
All tab title cells will be marked with the designer region attribute holding the index of the tabs. Also, the content cell will be marked in the design region.
protected override void CreateChildControls()
{
base.CreateChildControls();
Table table = (Table) tabControl.Controls[0];
if (table != null)
{
for (int i = 0; i < tabControl.TabPages.Count; i++)
{
table.Rows[0].Cells[i].Attributes[
DesignerRegion.DesignerRegionAttributeName] =
i.ToString();
}
table.Rows[1].Cells[0].Attributes[
DesignerRegion.DesignerRegionAttributeName] =
(tabControl.TabPages.Count).ToString();
}
}
Switching between tabs
When the user clicks on the control, OnClick
will catch the event. Here, only if the clicked area is a known region, the code will extract the tab index from the region name of the clicked title and set it to the tab control property CurrentDesignTab
. Then, the UpdateDesignTimeHtml
will update the design view accordingly.
protected override void OnClick(DesignerRegionMouseEventArgs e)
{
if (e.Region == null)
return;
if (e.Region.Name.IndexOf(HEADER_PREFIX) != 0)
return;
if (e.Region.Name.Substring(HEADER_PREFIX.Length) !=
tabControl.CurrentDesignTab.ToString())
{
tabControl.CurrentDesignTab =
int.Parse(e.Region.Name.Substring(HEADER_PREFIX.Length));
base.UpdateDesignTimeHtml();
}
}
Persist tab body, and view it in the designer
While switching between tabs, the designer needs to get the active template which represents the active tab. From the active region name, we can get the tab index, and through getting the active tab from tabControl.TabPages[tabIndex].TabBody
and returning the HTML through the ControlPersister.PersistTemplate
method. See the method GetEditableDesignerRegionContent
.
public override string GetEditableDesignerRegionContent(EditableDesignerRegion region)
{
IDesignerHost host = (IDesignerHost)
Component.Site.GetService(typeof(IDesignerHost));
if (host != null && tabControl.TabPages.Count > 0)
{
ITemplate template = tabControl.TabPages[0].TabBody;
if (region.Name.StartsWith(CONTENT_PREFIX))
{
int tabIndex = int.Parse(region.Name.Substring(
CONTENT_PREFIX.Length));
template = tabControl.TabPages[tabIndex].TabBody;
}
if (template != null)
return ControlPersister.PersistTemplate(template, host);
}
return String.Empty;
}
Any changes on the template should be reflected to the TabBody
of the edited tab. The method ControlParser.ParseTemplate
will instantiate a template from the design contents, and by knowing the region name, we can get the tab index and then update the TabBody
with the content template. See the method SetEditableDesignerRegionContent
.
public override void SetEditableDesignerRegionContent(
EditableDesignerRegion region, string content)
{
if (content == null)
return;
IDesignerHost host = (IDesignerHost)
Component.Site.GetService(typeof(IDesignerHost));
if (host != null)
{
ITemplate template = ControlParser.ParseTemplate(host, content);
if (template != null)
{
if (region.Name.StartsWith(CONTENT_PREFIX))
{
int tabIndex = int.Parse(
region.Name.Substring(CONTENT_PREFIX.Length));
tabControl.TabPages[tabIndex].TabBody = template;
}
}
}
}
Finally
I didn't do much on coding this control, but I think I provided the basics of creating a web based tab control. I hope this article will be useful for you. Also, I will leave it for your creativity to improve the logic of rendering the control and switching between tabs. It would be good to develop a script object to manage the tab control at the client side, providing properties and the tabs collection, with some methods to automate the process of switching, showing, disabling, and enabling tabs.
In case you have other ideas, or ways to improve this code, please feel free to use this code and update it the way you like. I will appreciate it so much if you will update me with your enhancements.