Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML

Creating a web based tab control

4.33/5 (12 votes)
15 May 20066 min read 1   1.6K  
Creating a web based tab control, with multible tabs.

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:

Title 1Title 2..Title n
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.

C#
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.

C#
protected override void OnPreRender(EventArgs e)
{
    base.OnPreRender(e);
    if (DesignMode)
    {
        _tabPages[_currentDesignTab].TabBody.InstantiateIn(this);
    }
}

protected override void CreateChildControls()
{
    // Always start with a clean form
    Controls.Clear();

    // Create a table using the control's declarative properties
    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");

    //keep the selected tab index in a an attribute
    tabControlTable.Attributes.Add("ActiveTabIdx", _selectedTab.ToString());

    BuildTitles(tabControlTable);

    BuildContentRows(tabControlTable);

    // Add the finished tabControlTable to the Controls collection
    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.

C#
private void BuildContentRows(Table tabControlTable)
{
    // Create content row(s)

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

            //only the selected tab body should be visible
            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.

JavaScript
<script type="text/javascript" language="javascript">
function ShowTab(tabTitleCell, idx)
{    
    //get the table which holds the tabs
    var tabsTable = tabTitleCell.parentElement.parentElement.parentElement;
    
    //what is the active tab index
    var activeTabIdx = Number(tabsTable.getAttribute("ActiveTabIdx"));
    
    //give the inactive appearance to the previous active tab
    tabsTable.rows[0].cells[activeTabIdx].style["backgroundColor"] = "inactiveborder";
    tabsTable.rows[0].cells[idx].style["backgroundColor"] = "darkgray";
    
    //since the tabs body contained in rows with 
    //index as the same of the tab title link plus 1,
    //then we can hide the row that holds the active tab.
    tabsTable.rows[activeTabIdx + 1].style.display = "none";
    
    //show the active tab body
    tabsTable.rows[idx + 1].style.display = "";
    
    //keep the new active tab in the attribute ActiveTabIdx
    tabsTable.setAttribute("ActiveTabIdx", idx);
}
<script>

Managing tab control templates and switching the design view

The tab control designer should take care of:

  1. Declaring the design area.
  2. Switching between tabs.
  3. 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.

C#
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.

C#
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.

C#
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.

C#
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.

C#
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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here