DataPanel Version 2.0: Article submitted 01 Nov 2004

DataPanel - An expandable/collapsible ASP.NET custom web control

Author: Willem J Fourie

Downloads:

 

Documentation:

 


Table of Contents

 

 

Introduction

 

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:

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.

 

How it works

Rendering the Control

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:

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:

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();
}

Client-side Scripting

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);
    }
}

Managing the Collapsed State

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);
    }
}

Managing the Property Viewstates

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.

 

History