Collection properties in ASP.Net Server Controls (Building a TabStrip Control)
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","
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">< SPAN>ww:tabpage>
<ww:tabpage caption="Email" actionlink="default" tabpageclientid="Tab2">< SPAN>ww:tabpage>
<ww:tabpage caption="Configuration" actionlink="default"
tabpageclientid="Tab3">< SPAN>ww:tabpage>
< SPAN>tabpages>
< SPAN>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');","
AdminTabs.AddTab("Main","default","
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
The Voices of Reason
# re: Collection properties in ASP.Net Server Controls (Building a TabStrip Control)
# re: Collection properties in ASP.Net Server Controls (Building a TabStrip Control)
thanks.
# re: Collection properties in ASP.Net Server Controls (Building a TabStrip Control)
# re: Collection properties in ASP.Net Server Controls (Building a TabStrip Control)
# re: Collection properties in ASP.Net Server Controls (Building a TabStrip Control)
Thanks
--Dietrich
# re: Collection properties in ASP.Net Server Controls (Building a TabStrip Control)
# re: Collection properties in ASP.Net Server Controls (Building a TabStrip Control)
# re: Collection properties in ASP.Net Server Controls (Building a TabStrip Control)
.tabbutton, .selectedtabbutton
{
vertical-align: middle;
cursor: hand;
color: Black;
background-color: lightsteelblue;
text-align: center;
font-size: 8pt;
border-right: solid 2px white;
}
.selectedtabbutton
{
font-size: 10pt;
FONT-WEIGHT: bold;
COLOR: Cornsilk;
BACKGROUND-COLOR: #003399;
cursor:default;
}
# re: Collection properties in ASP.Net Server Controls (Building a TabStrip Control)
# re: Collection properties in ASP.Net Server Controls (Building a TabStrip Control)
# re: Collection properties in ASP.Net Server Controls (Building a TabStrip Control)
1. [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]<br>
2. [PersistenceMode(PersistenceMode.InnerProperty)]
Thnx
AB
# re: Collection properties in ASP.Net Server Controls (Building a TabStrip Control)
# re: Collection properties in ASP.Net Server Controls (Building a TabStrip Control)
# re: Collection properties in ASP.Net Server Controls (Building a TabStrip Control)
Again, you provide me with information that I couldn't find elsewhere! Thx!
- Joel
# re: Collection properties in ASP.Net Server Controls (Building a TabStrip Control)
It save lot of my time ....
Thanks ..........
# re: Collection properties in ASP.Net Server Controls (Building a TabStrip Control)
If the 1st part can be easy fixed (don't render of if you want to have this on client just hide it using css), I can't figure it out why Visible is not maintened.
# re: Collection properties in ASP.Net Server Controls (Building a TabStrip Control)
# re: Collection properties in ASP.Net Server Controls (Building a TabStrip Control)
# re: Collection properties in ASP.Net Server Controls (Building a TabStrip Control)
# re: Collection properties in ASP.Net Server Controls (Building a TabStrip Control)
# re: Collection properties in ASP.Net Server Controls (Building a TabStrip Control)
# re: Collection properties in ASP.Net Server Controls (Building a TabStrip Control)
In the "Item Implementation" section, you have a line that says "if you don't derive from collection" and then goes on to describe the usefulness of deriving from Control. Do you mean "Control" in the previous section, not "collection"? Because I prefer to use the generic list over implementing collections.
# re: Collection properties in ASP.Net Server Controls (Building a TabStrip Control)
{
base(AddParsedSubObject);
}
is needed if you want anything else to be rendered in your usercontrol.
# re: Collection properties in ASP.Net Server Controls (Building a TabStrip Control)
# re: Collection properties in ASP.Net Server Controls (Building a TabStrip Control)
# re: Collection properties in ASP.Net Server Controls (Building a TabStrip Control)
This is a very useful thing.. Thank You.
I needed some help. i put the tab in a Master Page. When i select a tab i move to another page. now how do i access that tab in the Master Page and set the selected tab.
If there is any other way to know which tab is selected (through Master Pages)please let me know.
Thank You
# re: Collection properties in ASP.Net Server Controls (Building a TabStrip Control)
# re: Collection properties in ASP.Net Server Controls (Building a TabStrip Control)
# re: Collection properties in ASP.Net Server Controls (Building a TabStrip Control)
can you explain me how can i have a property with nested properties ?
Something like the asp:menuitem.
Thanks in advance,
melnac.
# re: Collection properties in ASP.Net Server Controls (Building a TabStrip Control)
I want to do a collection property with multilevel nested child items.
<c1:MyControl>
<items>
<c1:MyItem>
<items>
<c1:MyItem>
<items>
... and so on
</items>
</c1:MyItem>
</items>
</c1:MyItem>
</items>
</c1:MyControl>
thanks in advance,
melnac.
# re: Collection properties in ASP.Net Server Controls (Building a TabStrip Control)
I´m getting nuts with the following error
MyControls.MyParametersCollection must have items of type MyControls.MyParameter.
MyControls.MyParameter is of type System.Web.HtmlControls.HtmlGenericControl
this is my code, please help ...
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Data;
using System.Data.SqlClient;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.ComponentModel;
using System.Drawing;
using System.Web.UI.HtmlControls;
using System.Web.UI.Design;
using System.Drawing.Design;
using System.ComponentModel;
using System.ComponentModel.Design;
namespace MyControls
{
public class MyParametersCollection : CollectionBase
{
public MyParametersCollection()
{
}
public void Add(MyParameter param)
{
List.Add(param);
}
public void Remove(MyParameter param)
{
List.Remove(param);
}
public MyParameter this[int index]
{
get { return (MyParameter)List[index]; }
set { base.List[index] = value; }
}
}
public class MyParameter
{
public enum ParameterType
{
Conditional,
External
}
public enum ParameterDataType
{
String,
Numeric,
Boolean,
Description
}
public MyParameter()
{
}
ParameterType _type = ParameterType.Conditional;
ParameterDataType _datatype = ParameterDataType.String;
string _value = "", _pagecontrol = "";
[Category("MyControl"), Description("Component Type"), NotifyParentProperty(true)]
public ParameterType Type
{
get { return _type; }
set { _type = value; }
}
[Category("MyControl"), Description("Component Data type"), NotifyParentProperty(true)]
public ParameterDataType DataType
{
get { return _datatype; }
set { _datatype = value; }
}
[Category("MyControl"), Description("Component value"), NotifyParentProperty(true)]
public object Value
{
get { return _value; }
set { _value = value; }
}
[Category("MyControl"), Description("Component id"), NotifyParentProperty(true)]
public string PageControl
{
get { return _pagecontrol; }
set { _pagecontrol = value; }
}
}
[DefaultProperty("CurrentParameters"), ToolboxData("<{0}:MyControl runat=server></{0}:MyControl>")]
[Designer(typeof(MyControlDesigner))]
[ParseChildren(true)]
[PersistChildren(false)]
public class MyControl : WebControl, INamingContainer
{
private SelectionComponentParametersCollection _parameters = new SelectionComponentParametersCollection();
[Category("Control"), Description("Defines control")]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
[PersistenceMode(PersistenceMode.InnerProperty)]
[NotifyParentProperty(true)]
public MyParametersCollection CurrentParameters
{
get { return _parameters; }
}
protected override void AddParsedSubObject(object obj)
{
if (obj is MyParameter) {
this.CurrentParametersAdd((MyParameter)obj);
return;
}
}
protected override void Render(HtmlTextWriter output)
{
EnsureChildControls();
base.Render(output);
}
}
public class MyControlDesigner : ControlDesigner
{
string widthcode,widthdescription;
public override bool AllowResize
{
get { return true; }
}
public override string GetDesignTimeHtml()
{
string designTimeHtml = "<input type=\"text\" style=\"height: 20px; width:200px"\" value=\"\">";
designTimeHtml += "<button style=\"color:white;background-color:navy;font-family:Verdana;font-size:XX-Small;\">?</button>";
designTimeHtml += "<input type=\"text\" style=\"height: 20px; width:"450px\" value=\"\">";
return designTimeHtml;
}
}
}
# re: Collection properties in ASP.Net Server Controls (Building a TabStrip Control)
private SelectionComponentParametersCollection _parameters = new SelectionComponentParametersCollection();
with
private MyParametersCollection _parameters = new MyParametersCollection();
sorry
# re: Collection properties in ASP.Net Server Controls (Building a TabStrip Control)
<%@ Register TagPrefix="btc" Namespace="MyControls"%>
<btc:MyControl id="MYCUSTOMER" runat="server" CssClass="fonteedicao">
<btc:CurrentParameters>
<btc:MyParameter Type="Conditional" DataType="String" Value="0001" PageControl="GRDCUSTOMERS" />
</btc:CurrentParameters>
</btc:MyControl>
Thanks
# re: Collection properties in ASP.Net Server Controls (Building a TabStrip Control)
1) Implements non-generic ICollection
2) Implements Add method (or extension Add method exists for it)
# re: Collection properties in ASP.Net Server Controls (Building a TabStrip Control)
# re: Collection properties in ASP.Net Server Controls (Building a TabStrip Control)
Do you have experience with collections in a complex property of a control? I have noticed that the collection elements do not persist in the HTML designer until another (simple) property is changed?
I am running into a similar problem with Smart Tags and action lists. The property setter for the action list is never called for a collection so the HTML designer is never updated until I update a simple string property in the Smart Tag.
Anyone had experience with this?
Thanks and great article Rick.... still valid after all these years. :)
# re: Collection properties in ASP.Net Server Controls (Building a TabStrip Control)
I was struggling for 2 days about multiple collections in one server component.
Thanks! Thanks! Thanks!
Sébastien
IT Development Consultant
sebastien@hollox.net
(438) 882-8687
# re: Collection properties in ASP.Net Server Controls (Building a TabStrip Control)
I would like to add to the fact that I have done two blog posts regarding collection management in server controls:
http://blogs.aspadvice.com/jlovell/archive/2004/03/03/688.aspx
http://blogs.aspadvice.com/jlovell/archive/2004/02/29/663.aspx
I do have simple code there that just "essentially" does the business of collections and server controls.
Cheers.