I’ve been struggling for the last few days with putting the finishing touch on a simple client side Tab control that I had built to provide the ability to break large pages into smaller pages that can be controlled via a simple tabstrip at the top. The control itself is super simple – basically a table with OnClick handlers, along with some generated script functions that activate the appropriate page using ID tags in the document.
The control works well and requires a handful of lines of code added to a page. Something as simple as this:
AdminTabs.AddTab("Main","default","Main");
AdminTabs.AddTab("Email","javascript:ActivateTab(this);ShowTabPage('Email');","Email");
AdminTabs.AddTab("CC Processing","default","CreditCard");
AdminTabs.AddTab("Configuration","default","Configuration");
if (!this.IsPostBack)
AdminTabs.SelectedTab = "Main"; // or set this in the designer
The tab control works all client-side, so there’s no server side event handling. The only thing that happens server side is the rendering and the state management of tracking the currently active page via a hidden variable on the form (rather than ViewState because the var is set on the client side via script code).
Creating this control was a snap and using ASP.Net’s object model was a clean and efficient affair. It basically involved creating a TabPage class, a TabPageCollection class (based on CollectionBase) and then implementing the actual control and rendering mechanism.
However, what wasn’t so easy was to create a design time interface. The tab control uses a collection of a TabPage class and getting this collection to properly persist into the HTML from the designer took some guess work and the help of several people who had gone through this before. I couldn't find all this in one place so I'm providing here what I found I needed.
The control should work like this:
<ww:wwwebtabcontrol id="WwWebTabControl2" runat="server" selectedtab="Tab2">
<tabpages>
<ww:tabpage caption="Main" actionlink="default" tabpageclientid="Tab1">ww:tabpage>
<ww:tabpage caption="Email" actionlink="default" tabpageclientid="Tab2">ww:tabpage>
<ww:tabpage caption="Configuration" actionlink="default"
tabpageclientid="Tab3">ww:tabpage>
tabpages>
ww:wwwebtabcontrol>
Just using the Collection on the control will not give you the above automatically. In fact the collection will not even be loaded with this data if you try to open the form with the collection. It will not even show up on the property sheet.
Specifying how complex control members such as objects and collections persist is driven through attributes that must be specified on the control itself and on any complex members. For a collection this means something like this:
Control Level
[ToolboxData("<{0}:wwWebTabControl runat=server>")]
[ToolboxBitmap(typeof(System.Web.UI.WebControls.Image))]
[ParseChildren(true)]
[PersistChildren(false)]
public class wwWebTabControl : Control
ParseChildren tells the control that it needs to run through the items and add them to the collection. Several people actually suggested to me that this attribute should be set to false but that caused items not to be parsed into the actual collection when the form first loaded. PersistChildren set to false indicates that the items are to be persisted as nested elements rather than as attributes on the control itself (using control-subproperty syntax). Both of these are vital!
The control should also implement the AddParsedSubObject method which makes sure that the inner items can be parsed into the collection:
protected override void AddParsedSubObject(object obj)
{
if (obj is TabPage )
{
this.TabPages.Add((TabPage) obj);
return;
}
}
Collection Member Level
The collection property on the control itself also requies a couple of attributes to make it work properly.
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
[PersistenceMode(PersistenceMode.InnerProperty)]
public TabPageCollection TabPages
{
get { return Tabs; }
}
private TabPageCollection Tabs = new TabPageCollection();
DesignerSerializationVisibility(DesignerSerializationVisibility.Content) determines how code is generated for this object. Specifically the above visibility creates individual TabPage object references in ASPX code behind file. Using visibility of Visible does not create code instances of the TabPages – which is actually desirable in this situation. You can always reference each of the pages dynamically through the collection itself.
The PersistenceMode attribute determines how the designer persists the collection’s content so it can load it at design or run time. In this case I chose InnerProperty which means it creates child controls (TabPages) that contain all their content as attributes. You can also use InnerDefaultProperty for objects that have default properties and that persist as the element content – a asp:ListItem is a good example of this type of item. Generally if you use a collection use InnerProperty. In addition you can also use Attribute which persists the object as attributes in the parent control. This won’t work for a collection, but it is useful for sub objects which can persist as with special syntax into the parent object by using ParentName-ChildProperty=’value’ syntax.
Item Implementation
When you use the above attributes VS.Net provides its default Collection Editor for editing the individual items – TabPage items in this case. To get items to display in the editor it’s easiest to derive them from Control as Control implements the default type converter interfaces needed for the CollectionEditor to create the items and assign property values and be able to serialize the collection into the HTML.
If you don’t derive from collection you need to implement a TypeConverter for your class and use the TypeConverter attribute to specify it. Using Control or any Control derived class is definitely easier although it adds a little overhead to your collection items.
Designer Rendering
One of the really cool things about ASP.Net (WinForms too) is that you can render controls at design time, so the user can actually see what the control will look like in the designer while building the control within the full page layout.
What I wanted to do is have the user see the control right from the start rather than just the default display which is basically the name of the control as a string. ASP. Net controls in the designer still call the Render method to draw themselves. This means as soon as I add tabs to my control the control will actually display them in real time with all the attribute settings reflected in the rendering.
Problem is that when there are no tabs, nothing gets rendered. So in order to do this I decided why not add a couple of dummy items to the collections when there are no items, render them, then remove them at the end of the process. The code to do this looks like this:
protected override void Render(HtmlTextWriter writer)
{
bool NoTabs = false;
string Selected = null;
// *** If no tabs have been defined in design mode write a canned HTML display
if (this.DesignMode && (this.TabPages == null || this.TabPages.Count == 0) )
{
NoTabs = true;
this.AddTab("No Tabs","default","Tab1");
this.AddTab("No Tabs 2","default","Tab2");
Selected = this.SelectedTab;
this.SelectedTab="Tab2";
}
// *** Render the actual control
this.RenderControl();
// *** Dump the output into the ASP out stream
writer.Write(this.Output);
// *** Call the base to let it output the writer's output
base.Render (writer);
if (NoTabs)
{
this.TabPages.Clear();
this.SelectedTab = Selected;
}
}
Figuring out whether we are in DesignMode can be done by checking (HttpContext.Current == null) which is assigned here to a private property of the object.
Complete Tab Control Example Source
For completeness and reference’s sake I’m providing the code for the wwWebTabControl in its entirety here so you can see all the points above put together and in action.
This control works on the concept of hiding and displaying content based on ID tags in the client HTML document. When you click a button Id tags are activated and deactivated hiding or displaying content depending on the tab selection. You need to match up the TabPageClientId on each TabPage with an ID in the document that is to be displayed. For example:
AdminTabs.AddTab("Main","javascript:ActivateTab(this);ShowTabPage('Main');","Main");
AdminTabs.AddTab("Main","default","Main");
Main specifies that the tab caption. The second parameter is the script or Url that fires in response to a click. Here the code calls two generated functions which activate the tab specified and then show the the content that is wrapped in the Main ID tag in the HTML page. Main is the TabPageClientId that maps this tab to an ID in the HTML page.
The two statements above are identical – default is merely a shortcut for the first line which is the most common thing you’ll want to do – Activate the tab selected and display the content that is related to it.
In order to use the control you will likely want to add it your Tools window. From there you can simply drag and drop the control and start adding Tab Pages through the designer.
You can download the control along with a simple example page from here:
http://www.west-wind.com/files/tools/wwWebTabControl.zip