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

A Web Tab Control Enhanced With Design Time Support

4.28/5 (9 votes)
22 Feb 2007CPOL6 min read 1   577  
A web Tab Control that is full customizable.

shTabControl

Introduction

Two months ago, I wrote my first article in CodeProject, shSimplePanel, a control that has good design time support. Now, I have finished writing other control called shTabControl. This web control is written with ASP.NET 2.0, and is based on the Web Tab Control by Mohammed Mahmoud and shSimplePanel, but this control has enhanced design time support and a better look and feel. I hope it will be useful in your projects. And, if you find any errors in my code or find my English poor, please don't hesitate to email me.

Background

If you look at Mohammed Mahmoud's article, you can see that I have made this control with the same architecture, but using only the Table, TableRow, and TableCell. Take a look at this:

Arch of shTabControl

shTabControl is a composite with a Table control as the main container. The first row is the RowHeader that contains a TableCell for each tab page added, and in the end, I put an empty TableCell for showing a blank space. Next, a TableRow - TableCell is added for each tab page, because it is the container for the child controls. With this architecture, we can add a nice property for setting the TitlePosition as Top or Bottom easily.

Furthermore, we can add some styles inside the CellPage like opacity background, mouse-over, image on tab page enabled, and image on tab page disabled effects for each tab page added. I will explain those properties later.

Using the code

Implementing the code is easy, with the IntelliSense of VS.NET 2005. The shTabControl contains another control called shTabPage that persists the child controls in the Controls property inside its ContentTemplate property.

ASP.NET
<shw:shTabControl ID="TabControl1" runat="server" CurrentPage="0" 
Height="350px" HeightCell="26px" TitlePosition="Top" Width="515px">
<TabPages>
   <shw:shTabPage ID="TabPage0" runat="server" BackColor="#C0C000" 
                  Height="350px"Text="Page0" Width="81px">
      <ContentTemplate>
           The contents and childs control here...
      </ContentTemplate>
   </shw:shTabPage>
</TabPages>
</shw:shTabControl>

The shTabControl has the following properties that control its look and feel at design time:

  • TabPages: Collection property of the shTabPage class.
  • HeightCell: Height of all CellHeaders.
  • TitlePosition: Position of the CellHeader. Must be Top or Bottom.
  • ShowBorderCell: Flag that indicates if the CellHeader can show its borders.
  • ImageHeader: Background image on the first (top) or last (bottom) RowPage.
  • CurrentPage: Current page selected.

For making this control, I used a little JavaScript for generating the client-side behavior and postback :-). The shTabControl implements the collection properties of the shTabPage with an UITypeEditor. All those concepts I will explain here.

SetVisiblePage JavaScript Client-Side Behavior

The SetVisiblePage is a JavaScript function for generating a postback and showing the page selected after postback.

HTML
<script language="javascript">
function SetVisiblePage(shTabName, value)
{      
    var obj= document.getElementById(shTabName + "_" + "hdCurrentPage");    
    if(obj!=null)
    {
        obj.value = value;
        obj.form.submit();
    }   
}
</script>

This function is used on the OnClick event of each _CellHeader, where the shTabName is a string that contains the ClientID of the shTabControl and the value parameter is the current page selected. I inserted this string inside a resource of the project. This function is rendered inside the protected Render method on runtime.

C#
//On run time write the javascript

if (!DesignMode)
{
 string script = ShWebTabControl.Properties.Resources.SetVisiblePageScript;
 script = script.Replace("SetVisiblePage", ClientID + "SetVisiblePage");
 writer.Write(script);
 ... ...
}

I used the Replace method for adding the ClientID property of shTabControl, because I want to generate a unique function when the page loads; but if you want, you can delete it. The implementation of this function with _CellHeader is integrated inside a private method called CreateCellHeader.

C#
private TableCell CreateCellHeader(shTabPage item, int nTabPage)
{
 TableCell CellHeader = new TableCell();            
 CellHeader.ID = "_CellHeader" + nTabPage.ToString();
 //Please see the code for more information

 CellHeader.Attributes["onclick"] = ClientID + "SetVisiblePage(\"" + ClientID.ToString() + 
                                    "\"," + nTabPage.ToString() + ");";
 //Please see the code for more information

 return CellHeader;
}

CreateCellHeader creates the struct of a TableCell from a shTabPage. All the CellHeaders have a unique ID. The shTabPage class implements all the properties for configuring each CellHeader.

shTabPage Class

The shTabPage class contain all the properties for configuring each tab page of a shTabControl. This class inherits from WebControl, INamingContainer, and IStateManager for implementing the ViewState of properties, and persists the changes at design and runtime.

C#
[ToolboxData("<{0}:shTabPage runat="server">")]
[NonVisualControlAttribute()]
[ParseChildren(true)]
[PersistChildren(false)]
public class shTabPage : WebControl, IStateManager, INamingContainer
{
    private bool _isTrackingViewState;
    private ITemplate _contentTemplate;
    public shTabPage():base(HtmlTextWriterTag.Div)  {  }
    //Properties Sections

         ...
    //IStateManager Members

    //Please see the code for more methods of IStateManager

    void IStateManager.TrackViewState()
    {
        _isTrackingViewState = true;
        if (ViewState != null)
           ((IStateManager)ViewState).TrackViewState();
    }
    internal void SetDirty()
    {
        if (ViewState != null)
        {
            ICollection Keys = ViewState.Keys;
            foreach (string key in Keys)
            {
                ViewState.SetItemDirty(key, true);
            }
        }
    }
}

This class is based on a DIV. This class is used for getting its properties and then applying it to shTabControl. The IStateManager interface is used because when implementing the shTabPage collection, we will need to force the ViewState. The SetDirty method does that action. Some properties of shTabPage are shown here:

  • Text: Title text of CellHeader.
  • TitleWidth: Width of CellHeader.
  • Opacity: Opacity of CellPage.
  • ImageEnable: Image when the CellHeader is selected.
  • ImageDisable: Image when the CellHeader is unselected.

shTabPageCollection Class

This class inherits from CollectionBase and implements the IStateManager interface. The IStateManager is used because we want to save the states of each shTabPage. The LoadViewState and SaveViewState do the principal job. Please see the code for more information:

C#
public int Add(shTabPage shtabpage)
{
 List.Add(shtabpage);            
 if (_isTrackingViewState)
 {                
  ((IStateManager)shtabpage).TrackViewState();
  shtabpage.SetDirty();
 }
 return List.Count - 1;
}
object IStateManager.SaveViewState()
{
 if (_saveAll == true)
 {
  object[] states = new object[Count];
  for (int i = 0; i < Count; i++)
  {
   shTabPage shtabpage = (shTabPage)List[i];
   shtabpage.SetDirty();
   states[i] = ((IStateManager)shtabpage).SaveViewState();
  }
  if (Count > 0)                
    return states;                
  else                
    return null;                
 }
 else
 {
  ArrayList indices = new ArrayList();                
  ArrayList states = new ArrayList();
  for (int i = 0; i < Count; i++)
  {
   shTabPage shtabpage = (shTabPage)List[i];
   object state = ((IStateManager)shtabpage).SaveViewState();
   if (state != null)
   {
    states.Add(state);
    indices.Add(i);                        
   }
  }
  if (indices.Count > 0)                
   return new Pair(indices, states);                
  return null;
 }
}
void IStateManager.TrackViewState()
{
  _isTrackingViewState = true;
  for(int i=0; i < Count; i++)
  {
   shTabPage shtabpage = (shTabPage)List[i];                
   ((IStateManager)shtabpage).TrackViewState();
  }
}

Some methods of this class check if _isTrackViewState is true. When this flag is turned on, the TrackViewState method is invoked and then the SetDirty method (see the Add, Clear, Insert methods), which causes the state to be persisted in the view state of shTabPageCollection.

SaveViewSate saves all items inside an array of objects when _saveAll is true. When _saveAll is false, SaveViewState saves only the changed items inside two ArrayLists: the first holds the indexes of changed items, and the second holds the state and then returns a Pair object that holds the view state of shTabPageCollection.

The LoadViewState performs the inverse logic of this method. Please see the code for more information.

shTabControl Class

This class is the main class that contains the shTabPageCollection property.

C#
public shTabPageCollection TabPages
{
 get{
      if (_TabPages == null){
         _TabPages = new shTabPageCollection();
         if (IsTrackingViewState)
            ((IStateManager)_TabPages).TrackViewState();
      }
      return _TabPages;
    }
}

This property is saved within the ViewState of shTabControl. the implementation is the same that I used in shSimplePanel; see that for more information about Custom State Management.

shTabControl inherits from CompositeControl and IPostBackDataHandler, because we want to implement events when the CurrentPage property changes. This class uses a hidden field for saving this property. This control is a HtmlInputHidden called hdCurrentPage that is used on the SetVisiblePage Script and IPostBackDataHandler.

C#
protected virtual void OnCurrentPageChanged(EventArgs e)
{
 EventHandler currentPageChangedHandler = (EventHandler)Events[EventCurrentPageChanged];
 if (currentPageChangedHandler != null)
 {
  currentPageChangedHandler(this, e);
 }
}

bool IPostBackDataHandler.LoadPostData(string postDataKey,
System.Collections.Specialized.NameValueCollection postCollection){
 int current = CurrentPage;
 string posted = postCollection[postDataKey + "$hdCurrentPage"];
            
 if((posted != null) && (posted.Length>0))
    CurrentPage = Convert.ToInt32(posted);

 //CurrentPage state has changed

 if (current != CurrentPage)
    return true;
 return false;
}

void IPostBackDataHandler.RaisePostDataChangedEvent()
{
 //raise our event notifying the CurrentPage change

 OnCurrentPageChanged(EventArgs.Empty);              
}

When the user clicks on the tab page, the script performs a submit, and the LoadPostData method checks if the value of hdCurrentPage is different than the CurrentPage property; if yes, then RaisePostDataChangedEvent is launched. The hidden field changes its name when a postback is performed. I saw this message when I ran the page on debug mode:

Image 3

The CreateChildControls method of shTabControl is shown here:

C#
protected override void CreateChildControls()
{            
  //See the code please

  //Creates all Cell Header from tab pages 
  //and the Row of Content, then add to Main Table

  int shTabPageIndex = 0;
  TableRow RowHeader = new TableRow();
  RowHeader.ID = "_RowHeader";
  foreach (shTabPage item in TabPages)
  {
   TableCell CellHeader = CreateCellHeader(item, shTabPageIndex);
   RowHeader.Cells.AddAt(shTabPageIndex, CellHeader);
   TableRow RowContent = CreateRowContent(item, shTabPageIndex);
   cTableMain.Rows.Add(RowContent);                  
   ++shTabPageIndex;
  }
  //Add a empty cell to Row Header

  TableCell CellHeaderEmpty = CreateCellEmpty();
  RowHeader.Cells.AddAt(shTabPageIndex, CellHeaderEmpty);            
  //Add on Top or Bottom the Row Header created

  if (TitlePosition == shTitlesPosition.Top)
     cTableMain.Rows.AddAt(0, RowHeader);
  else
     cTableMain.Rows.Add(RowHeader);
  //Add the controls to Controls Property            

  Controls.Add(hdCurrentPage); 
  Controls.AddAt(0,cTableMain);
}

This method creates the struct of shTabControl. The foreach performd a CreateCellHeader, CreateRowContent, and at the end, a CreateCellEmpty. Those private methods are for creating the cell header of RowHeader and for creating the content of RowPage-CellPage. Finally, CreateHeaderEmpty creates a cell with no border at the end of all cell headers.

A method called SetTabPage is called inside the Render method for setting the RowPage-CellPage visible when the user clicks on design/run time. Inside this method, I implemented the rollover effects.

Finally, I will show the shTabControlControlDesigner. This class is inherited from CompositeControlDesigner and is based on Mohammed Mahmoud's control and shSimplePanel. First, I implemented a TemplateGroups:

C#
public override TemplateGroupCollection TemplateGroups
{
 get{
   TemplateGroupCollection collection = new TemplateGroupCollection();
   TemplateGroup group = new TemplateGroup(_shTabControl.ID);
   for (int i = 0; i < _shTabControl.TabPages.Count; i++){                    
    TemplateDefinition definition = new TemplateDefinition(
                                  this,HEADER_PREFIX + i.ToString(), 
                                   _shTabControl.TabPages[i], "ContentTemplate", false);
    group.AddTemplateDefinition(definition);                    
   }
   collection.Add(group);
   return collection;
 }
}

Image 4

Now, at design time, we set the regions used as templates in the CreateChildControls method. Inside this method, we check if the TitlePosition property is Top or Bottom for setting up the source of CellHeader. If it is Top, then the first row is the RowHeader; if not, then the last row is the RowHeader. When the user does a click, the OnClick is launched and we check if the click was on the HeaderCell. Here, we invoke the GetDesignTimeHtml method when calling UpdateDesignTimeHtml within the OnClick event. The GetDesignTimeHtml method sets an editable region based on the CurrentPage.

C#
protected override void CreateChildControls()
{
 //See the code ...

 for (int i = 0; i < _shTabControl.TabPages.Count; i++){
   if(_shTabControl.TitlePosition == shTabControl.shTitlesPosition.Top)
      Tbl.Rows[0].Cells[i].Attributes[
          DesignerRegion.DesignerRegionAttributeName] = i.ToString();
   else
      Tbl.Rows[Tbl.Rows.Count-1].Cells[i].Attributes[
               DesignerRegion.DesignerRegionAttributeName]=i.ToString();
 }
 //set the editable region

 if (_shTabControl.CurrentPage != -1){
  if(_shTabControl.TitlePosition == shTabControl.shTitlesPosition.Top)
    Tbl.Rows[1 + _shTabControl.CurrentPage].Cells[0].Attributes[
             DesignerRegion.DesignerRegionAttributeName]=
             _shTabControl.TabPages.Count.ToString();
  else
    Tbl.Rows[_shTabControl.CurrentPage].Cells[0].Attributes[
             DesignerRegion.DesignerRegionAttributeName]=
             _shTabControl.TabPages.Count.ToString();
 }
}

public override string GetDesignTimeHtml(DesignerRegionCollection regions)
{
 this.CreateChildControls();
 for (int i = 0; i < _shTabControl.TabPages.Count; i++)
     regions.Add(new DesignerRegion(this, HEADER_PREFIX + i.ToString()));

 if (_shTabControl.CurrentPage != -1){
  regions.Add(new EditableDesignerRegion(this, 
             CONTENT_PREFIX + _shTabControl.CurrentPage.ToString(),false));
  regions[_shTabControl.CurrentPage].Highlight = true;
 }
 return base.GetDesignTimeHtml(regions);
}

Conclusion

This control is an example implementation of using viewstate and ControlDesigner for support at design time. The WebTabControl was the main idea of shTabControl. I hope that this control will be useful in your projects. Have a nice coding :-)

Updates

  • 22-02-2006: I rewrote the shTabPage class for the StateBag variable. Please try to set the property EnableViewState to false for controls inside a TabPage. Please check it out :-)

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)