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:
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.
<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 CellHeader
s.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.
<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.
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
.
private TableCell CreateCellHeader(shTabPage item, int nTabPage)
{
TableCell CellHeader = new TableCell();
CellHeader.ID = "_CellHeader" + nTabPage.ToString();
CellHeader.Attributes["onclick"] = ClientID + "SetVisiblePage(\"" + ClientID.ToString() +
"\"," + nTabPage.ToString() + ");";
return CellHeader;
}
CreateCellHeader
creates the struct of a TableCell
from a shTabPage
. All the CellHeader
s 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.
[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) { }
...
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:
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 object
s when _saveAll
is true
. When _saveAll
is false
, SaveViewState
saves only the changed items inside two ArrayList
s: 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.
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
.
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);
if (current != CurrentPage)
return true;
return false;
}
void IPostBackDataHandler.RaisePostDataChangedEvent()
{
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:
The CreateChildControls
method of shTabControl
is shown here:
protected override void CreateChildControls()
{
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;
}
TableCell CellHeaderEmpty = CreateCellEmpty();
RowHeader.Cells.AddAt(shTabPageIndex, CellHeaderEmpty);
if (TitlePosition == shTitlesPosition.Top)
cTableMain.Rows.AddAt(0, RowHeader);
else
cTableMain.Rows.Add(RowHeader);
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
:
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;
}
}
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
.
protected override void CreateChildControls()
{
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();
}
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 :-)