For a .NET web-site development that we undertook, a requirement existed to develop web-forms with collapsible data entry areas, such as shown in Figure 1 and Figure 2 below. The end result was a custom ASP.NET web-control called DataPanel.

Figure 1: Two ASP.NET DataPanels shown in the collapsed state.

Figure 2: Two ASP.NET DataPanels shown in the expanded state.
Other important requirements for our use of such a control was
that the control should be developed in-house, there should be
minimal and controlled used of client-side scripts, and the control
should be developed using Microsoft ASP.NET. Further, the control
should support the use of cascading style sheet definitions.
From an application developer’s point-of-view, the following points
were considered important to serve the usual time and budget
constraints that restrict software development:
To leverage as much free code and experience that could be gained
from code-developer web sites, Microsoft
and other knowledge bases, etc.
To fast track by re-using the functionality of existing controls.
Since our control is a container control that should host other
controls much like the ASP.NET Panel control, it was decided to
apply the Panel control as a base-class for the DataPanel control.
The functionality of the control should maximize use of existing controls to make up the DataPanel control. The belief here is that these controls, by their own behaviour, can provide the required functionality when orchestrated together by the containing control. Microsoft calls this type of control a composite web-control.
DataPanel was developed using the control developer’s guidelines in
its MSDN knowledge base. The control was modelled on similar ASP.NET
controls found on the Internet, which unfortunately did not provide
source-code for our use, but provided a model for the control’s
functionality.
Figure 3 below illustrates the hierarchical layout of the DataPanel control.

Figure 3: The DataPanel hierarchical layout.
The composite hierarchy of the control is described below:
The HeaderPanel (System.Web.UI.WebControls.Panel) is the outer control which is rendered, using the same Control.ID as the design-time DataPanel control.
The ControlTable (System.Web.UI.HtmlControls.HtmlTable) is the contained within the HeaderPanel, and provides the layout structure for the DataPanel control.
The ControlTable contains two rows, the HeaderRow and ContentRow (System.Web.UI.HtmlControls.HtmlTableRow). The HeaderRow provides the structure for the control title bar and action link, while the ContentRow contains the actual user content as defined during design time.
The HeaderRow contains two cells, the TitleCell and ActionCell (System.Web.UI.HtmlControls.HtmlTableCell). The TitleCell provides the title bar (and optional expand/collapse) link for the title, while the ActionCell provides the optional action link and image for collapsing/expanding the control.
The ContentRow contains a ContentCell (System.Web.UI.HtmlControls.HtmlTableCell), which in turn provides the container for the user content added to the panel during design time.
The [User Content] is the user content added to the panel during design time.
Version 1.0 of this control attempted to use a panel control (ContentPanel) and in the CreateChildControls() method actually removed the design-time user controls from the DataPanel control and added them to the content panel control. Visually, this worked correctly and required no specific rendering of the control but this approach caused extensive problems:
Many of the child web-controls added during design time did not retain their Viewstate's correctly.
The validation web-controls added into the DataPanel at design time did not work at all.
A fairly simple but elegant work-around solves this problem:
First the rendering method for the ContentCell is intercepted by specifying a render delegate method as follows:
_contentCell.SetRenderMethodDelegate(new RenderMethod(RenderPanelContent));
Then the render delegate for the ContentCell is:
private void RenderPanelContent(HtmlTextWriter Writer, Control Ctl)
{
this.RenderContents(Writer);
}
This method renders this DataPanel's contents at this time, effectively making the panel contents children of the ContentCell. Note that the panel itself is not rendered, but all the panel child controls are rendered when the RenderContents method is called. This renders the control visually correct, and also does not compromise the correct working of the child controls. To ensure that rendering works correctly, and remembering that the outer container for the DataPanel control is actually the HeaderPanel, the actual Render method for the control is as follows:
protected override void Render(HtmlTextWriter writer)
{
_headerPanel.ID = this.ID;
_headerPanel.RenderControl(writer);
_headerPanel.ID = this.HeaderPanelId;
}
To correctly render the DataPanel control, the HeaderPanel (the real container) is rendered using the DataPanel's ID. To complete the picture, the following method is the CreateChildControls override:
protected override void CreateChildControls()
{// Ensures that child view states are correctly updated.
this.ClearChildViewState();
// Creates the child controls of this control.
this.CreateControlComponents();
// Sets the parent/child relationships.
this.SetControlHierarchy();
// Updates the visual state of the control.
this.UpdateControlsState();
}
The control is collapsed/expanded on the browser by clicking on the optional title and/or action hyperlinks. These links execute javascript methods that has been rendered to the browser from the ASP.NET code-behind methods. The script consists of two methods: the first method is used when the action-link does not consist of an image hyperlink:
function DataPanel_ExpandCollapse(hd, cht, cha, st, tc, te)
{
// Check if panel contents is expanded.
if(document.getElementById(hd).style.display == '')
{
// Expanded - hide the contents.
document.getElementById(hd).style.display = 'none';
// If the title is a link, set the tooltip to the
// expand-text string.
if(cht != '')
{
document.getElementById(cht).title = te;
}
// If the action-link is active, set the display text
// and the tooltip to the expand-text string.
if(document.getElementById(cha) != null)
{
document.getElementById(cha).innerHTML = te;
document.getElementById(cha).title = te;
}
// Set the collapsed state hidden field to 'true'.
document.getElementById(st).value = 'true';
}
else
{
// Collapsed - show the contents.
document.getElementById(hd).style.display = '';
// If the title is a link, set the tooltip to the
// collapse-text string.
if(cht != '')
{
document.getElementById(cht).title = tc;
}
// If the action-link is active, set the display text
// and the tooltip to the collapse-text string.
if(document.getElementById(cha) != null)
{
document.getElementById(cha).innerHTML = tc;
document.getElementById(cha).title = tc;
}
// Set the collapsed state hidden field to 'false'.
document.getElementById(st).value = 'false';
}
}
For a DataPanel control with an ID 'DataPanel1', a typical hyperlink is generated by the code-behind as follows:
javascript:DataPanel_ExpandCollapseImage('DataPanel1_ContentRow','DataPanel1_TitleLink',
'DataPanel1_ActionLink',DataPanel1_CurrentState','Collapse','Expand');
If an image hyper-link is used, the script must make provision for the image to change depending on the collapsed state of the control. If the control is collapsed, an 'expand' image should be displayed. Conversely, if the control is expanded, a 'collapse' image should be displayed. This is done by specifying URL's for the images in the ExpandImageUrl and CollapseImageUrl respectively. The second method manages the collapsing/expanding of the control if image hyper-links are used:
function DataPanel_ExpandCollapseImage(hd, cht, cha, st, ex, cl, tc, te)
{
// Get the image element.
var elImg = (document.getElementById(cha)).getElementsByTagName("img");
// Check if panel contents is expanded.
if(document.getElementById(hd).style.display == '')
{
// Expanded - hide the contents.
document.getElementById(hd).style.display = 'none';
// If the title is a link, set the tooltip to the
// expand-text string.
if(cht != '')
{
document.getElementById(cht).title = te;
}
// Set the image URL to the 'expand' image.
elImg[0].src = ex;
// Set the image tooltip to the expand-text string.
elImg[0].alt = te;
// Set the collapsed state hidden field to 'true'.
document.getElementById(st).value = 'true';
}
else
{
// Collapsed - show the contents.
document.getElementById(hd).style.display = '';
// If the title is a link, set the tooltip to the
// collapse-text string.
if(cht != '')
{
document.getElementById(cht).title = tc;
}
// Set the image URL to the 'collapse' image.
elImg[0].src = cl;
// Set the image tooltip to the collapse-text string.
elImg[0].alt = tc;
// Set the collapsed state hidden field to 'false'.
document.getElementById(st).value = 'false';
}
}
For a DataPanel control with an ID 'DataPanel1', for the image hyper-link case, a typical hyperlink is generated by the code-behind as follows:
javascript:DataPanel_ExpandCollapseImage('DataPanel1_ContentRow','DataPanel1_TitleLink',
'DataPanel1_ActionLink',DataPanel1_CurrentState','expand.gif','collapse.gif',
'Collapse','Expand');
The script is generated from the code-behind module: The script is kept in a separate project file 'DataPanel.js' and set as an embedded resource. In the control's OnPreRender method the following method is called to obtain the script from the embedded resource and register the script so that it can be rendered to the client:
private void RegisterControlScript()
{
// Check if script already registered.
if( ! Page.IsClientScriptBlockRegistered(_dataPanelScript))
{
// Not registered - read the script into a string.
string resName = "BWare.UI.Web.WebControls.DataPanel.js";
Stream stream =
this.GetType().Assembly.GetManifestResourceStream(resName);
StreamReader rdr = new StreamReader(stream);
string scriptStr =
"\r\n<script language=\"javascript\">\r\n" +
rdr.ReadToEnd() +
"\r\n</script>";
rdr.Close();
stream.Close();
// Register the script.
Page.RegisterClientScriptBlock(_dataPanelScript, scriptStr);
}
}
The Collapsed state of the control is managed using a hidden input field in the control. The value of the field is set to 'true' if the control is collapsed, else 'false' if expanded. The collpased state is usually changed via client script when the links on the control are clicked and is evident in the script methods above. On a post-back event, the value of the input field must be read in order to update the Collapsed property of the control. This is done by having the control implement the IPostBackDataHandler interface. The RaisePostDataChangedEvent method is not implemented as no post-back event notification is required by the DataPanel control. The LoadPostData method is implemented as follows and allows the DataPanel control to read the input field and update the Collapsed property:
public bool LoadPostData(string postDataKey, NameValueCollection postCollection)
{
// Retrieve the hidden input field value from the incoming values.
string collapsedState = postCollection[this.CollapsedStateId];
if(collapsedState != null)
{
// Set the collapsed property.
this.Collapsed = (collapsedState == "false") ? false : true;
}
// Ensure postback-event is not raised for this control.
return false;
}
In order for the DataPanel control to receive the LoadPostData call-back ahead of of any execution of control-associated code-behind code, the control registers for post-back as follows, by overriding the OnInit method:
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
if(this.Page != null)
{
this.Page.RegisterRequiresPostBack(this);
}
}
For all of the properties other than the TitleStyle property, the control ViewState object is used to persist the property between post-backs. The property for the control TitleText is defined as follows, and most of the other properties are persisted in a similar way:
public string TitleText
{
get
{
if(this.ViewState[_titleText] == null) return "";
return this.ViewState[_titleText].ToString();
}
set { this.ViewState[_titleText] = value; }
}
For the TitleStyle property, the Style SaveViewState and LoadViewState methods are used to persist the data between post-backs as follows, by overriding the same methods for the DataPanel control:
protected override object SaveViewState()
{
// Save the base state.
object baseState = base.SaveViewState();
// Save the view-state for the style object.
object titleStyle = (_titleStyle != null) ?
((IStateManager)_titleStyle).SaveViewState() : null;
// Create a new state array and return it.
object[] state = new object[2]{ baseState, titleStyle };
return state;
}
and:
protected override void LoadViewState(object savedState)
{
if(savedState != null)
{
// Get the state array.
object[] state = (object[])savedState;
// Load the base state.
if( (state.Length > 0) && (state[0] != null) )
base.LoadViewState(state[0]);
// Load the style object state.
if( (state.Length > 1) && (state[1] != null) )
((IStateManager)_titleStyle).LoadViewState(state[1]);
}
}
The property for the TitleStyle then is as follows:
public Style TitleStyle
{
get
{
if(_titleStyle == null)
{
_titleStyle = new Style();
}
// Check if the control is saving changes to it view-state.
if(this.IsTrackingViewState)
{
// Ensure view-state tracking is on.
((IStateManager)_titleStyle).TrackViewState();
// Force the complete style to be saved in viewstate
// every time.
_titleStyle.BackColor = _titleStyle.BackColor;
_titleStyle.BorderColor = _titleStyle.BorderColor;
_titleStyle.BorderStyle = _titleStyle.BorderStyle;
_titleStyle.BorderWidth = _titleStyle.BorderWidth;
_titleStyle.CssClass = _titleStyle.CssClass;
_titleStyle.Font.Bold = _titleStyle.Font.Bold;
_titleStyle.Font.Italic = _titleStyle.Font.Italic;
_titleStyle.Font.Name = _titleStyle.Font.Name;
_titleStyle.Font.Names = _titleStyle.Font.Names;
_titleStyle.Font.Overline = _titleStyle.Font.Overline;
_titleStyle.Font.Size = _titleStyle.Font.Size;
_titleStyle.Font.Strikeout = _titleStyle.Font.Strikeout;
_titleStyle.Font.Underline = _titleStyle.Font.Underline;
_titleStyle.ForeColor = _titleStyle.ForeColor;
_titleStyle.Height = _titleStyle.Height;
_titleStyle.Width = _titleStyle.Width;
}
return _titleStyle;
}
}
An interesting problem was discovered if the lines of code that set the properties of the Style objects to their own values are excluded. If a property of the Style is set in the code-behind, only that property is set in the state array of the control's view-state collection returned by LoadViewState. If, on another post-back if the property is not explicitly set again, it is reset as its value is marked as unchanged and not included in the state array. To force retention of these values, the values of all the properties of Style are re-set to force them to be saved.