Steve C. Orr

Software Engineer, Web Developer, Database Designer
 
  

 













































































































LANGUAGES: VB.NET | C#

ASP.NET VERSIONS: 1.x | 2.x

VisiPanel

Tour the Source Code of a Free, Colorful, Expanding Panel Web Control

By Steve C. Orr

A Web developer can never have too many good navigation controls around. Although ASP.NET 2.0 delivers some nice new options, it still doesn’t provide anything resembling the expanding panel controls that are popular these days. I haven’t seen many free ones around either, so I created one - VisiPanel. You can have it for free, and I’ll show you how it all works under the hood in case you’d like to learn from it or soup it up a bit.

VisiPanel is great for sidebars. It can act as a menu when filled with navigational links, or it can act as a command panel when filled with other kinds of controls. Figure 1 shows several instances of VisiPanel in action, and you're seeing it live at the left edge of this window now.

 
Figure 1: DirectX filters are responsible for VisiPanel’s colorful gradient display. Client-side code is in place to smoothly expand and contract the panel when the user clicks on the title bar (without requiring postbacks).

User Friendly

At design time the VisiPanel control acts very much like a standard Panel Web control. This is related to the fact that VisiPanel inherits from the Panel control and extends it with enhanced functionality. Any kind of control can be dropped into VisiPanel, and its contents can be arranged in standard ways.

Beyond the functionality of the base Panel control, VisiPanel adds several properties and one event (see Figure 2). VisiPanel’s OnExpandedChanged event is fired if the user has changed the dropdown state of the control between postbacks. The HeaderText property manages the text displayed in the header portion of the control at run time. The Expanded property toggles the initial dropdown state of the control, so it can be opened or closed programmatically. Finally, the GradientEndColor property can be combined with the BackColor property to provide an alluring background gradient coloring effect at run time.

Unique VisiPanel Members

Description

OnExpandedChanged event

The VisiPanel’s OnExpandedChanged event is fired when the user changes the dropdown state of the control between postbacks. 

HeaderText property

The HeaderText property specifies the text that should be displayed in the header portion of the control at runtime.

Expanded property

The Expanded property toggles the initial dropdown state of the control so it can be opened and closed programmatically.

GradientEndColor property

The GradientEndColor property can be mixed with the BackColor property to provide an alluring background gradient coloring.

Figure 2: Beyond the functionality of the underlying Panel control, VisiPanel adds several unique properties and one new event.

That’s about all you need to know to get started using the control. Download the control (see end of article for download details) or enter the code from Listing One into a new Web Control Library project in Visual Studio. To add the control to your Visual Studio toolbox, right click on the toolbox and follow the prompts to browse for VisiPanel.dll. Finally, drag the control from the toolbox onto any WebForm and configure its properties, or enter a declaration such as this into HTML view of the ASPX page: 

<cc1:VisiPanel id="VisiPanel1"

runat="server"

BackColor="Beige"

GradientEndColor="Tan"

HeaderText="My Header Text">

Hello World

</cc1:VisiPanel>

The rest of this article describes the inner workings of the VisiPanel control, so you can learn from it or extend the control with even more advanced functionality.

Eye Candy

If you read my DirectX article, then you’re already familiar with the DirectX filter technique VisiPanel is using for its colorful gradient rendering. The following style declaration does the trick:

style="display:block;FILTER:

progid:DXImageTransform.Microsoft.Gradient

(startColorstr='Blue', endColorstr='Red',gradientType='0');”

Only Internet Explorer supports this technique (although other browsers degrade nicely) and it’s very picky about syntax details, such as having white space and carriage returns in all the right places, so it’s nice to have that functionality wrapped into a control like this that can handle the HTML rendering perfectly every time.

Structural Integrity

The VisiPanel output is made up of two primary elements, one above the other. Figure 3 illustrates this fact. A single-rowed, three-celled HTML table is on top, followed by the inherited Panel control on bottom. The configurable header text is displayed in the first table cell, followed by two Webdings font characters in the final two cells to represent arrows. Only one of these final two cells is ever displayed at a time, depending on the current expanded or contracted state of the control.


Figure 3: VisiPanel is made up of two primary parts: An HTML table is on top; on the bottom is the output of a standard Panel Web control from which VisiPanel inherits.

The VisiPanel code manages the output of the top header table and adds a few attributes to the underlying Panel’s output for cosmetic purposes.

The bottom portion of the control is the (slightly modified) output of a standard Panel Web control. ASP.NET usually chooses to render the Panel as a standard <div> HTML tag. The base Panel control manages all the child controls, so you’ll find no child control management code within VisiPanel at all - even though this functionality works great at run time and design time.

Figure 4 lists the custom JavaScript code that’s rendered to handle the client-side OnClick event of the header table. This code toggles the visibility of the Panel’s output and the arrow table cells. The final line writes the current dropdown state to a hidden field. Without this line, the control would resort to its default dropdown state every time the page posts back, thereby annoying the user by undoing their action. You might think of this as a kind of a home-grown ViewState (standard ViewState wouldn’t work because it cannot be directly accessed on the client side).

//get references to the panel and the two arrow buttons

var oPnl=document.getElementById('VisiPanel1');

var oDown=document.getElementById('VisiPanel1_ButtonDown');

var oUp= document.getElementById('VisiPanel1_ButtonUp');

 

//toggle the visibility of these 3 elements          

if (oPnl.style.display == 'none')

{

  oPnl.style.display = 'block';

  oDown.style.display='none';

  oUp.style.display='block';

}

else

{

  oPnl.style.display = 'none';

  oDown.style.display='block';

  oUp.style.display='none';

}

//store current visible state for server side processing

document.getElementById('VisiPanel1_hidden').value =

  oPnl.style.display;

Figure 4: This is the client-side JavaScript code that gets executed when the end user clicks the header table of the VisiPanel control. It toggles the Display style of the arrow cells and panel, then writes the state to a hidden textbox so server-side code will be able to determine the dropdown state upon the next postback.

The stored state information is also used by the control’s server-side code the next time the page is posted back to determine if the user toggled the dropdown state, and, if so, raises the OnExpandedChanged event. You can find this code in the OnInit event shown in Listing One.

VisiPanel’s constructor (Sub New) sets some appropriate defaults for the base Panel control, specifying the initial size and some other cosmetic details.

Rendering Outperforms Composition

To generate the three-celled header table, I could’ve used Composition. That is, I could have instantiated a Table object and added three TableCell objects to its TabelRow object. This would generally be done within the CreateChildControls event of the server control. Although Composition is a great way to keep development quick and simple, there is a performance cost associated with instantiating all those objects. For smaller Web sites with less traffic, this likely isn’t a big deal. However, if you’re developing controls for a highly scalable Web site, you should be aware that Rendering outperforms Composition by a significant amount. That’s why I chose Rendering instead of Composition. Rendering is done by overriding the Render event of the base server control and outputting the HTML in a comparatively manual fashion.

Listing One shows the overridden Render event, which makes extensive use of the HTMLTextWriter parameter that I’ve abbreviated with the variable name “w”. The first code block uses a StringBuilder object to efficiently concatenate together the required JavaScript, such as that listed in Figure 4.

The second code block of the Render event generates the hidden textbox mentioned earlier. It is assigned Name and ID attributes so it can be more easily referenced from client-side code. I could’ve used code similar to this to generate the hidden textbox:

w.Write(“<input type=hidden id=whatever>”)

However, hard-coding HTML in this fashion is asking for future maintenance problems. With XHTML coming on strong in the future, and handheld devices of every kind supporting varying forms of HTML, letting ASP.NET make decisions about HTML generation details is usually a good idea. Because Microsoft practically defines what is “proper” HTML, it’s a good idea to trust their judgment about what precisely should be generated for whichever device is making the request. By using methods such as AddAttribute and RenderBeginTag, ASP.NET decides the precise syntax that is output. Of course, there are many ways to adjust the output in cases where Microsoft’s rendering technology has made a decision that contradicts your personal preferences.

(advertisement)

The next four code blocks of the Render event use similar techniques to generate the header table and the three cells contained within. The mouse cursor style is set to “hand” to make it evident to the end user that this area is clickable. The JavaScript is assigned to the client-side OnClick event of the table and cosmetic attributes are added to ensure an attractive output. The final two cells are specified to use the Webdings font so the arrow characters will show appropriately. Only one of these arrow cells will be displayed at a time, depending on the current Expanded state of the control.

The final two code blocks of the Render event add attributes to the output of the underlying Panel control. First, it must be determined whether the panel will initially be displayed or hidden depending on the current Expanded state of the control. Finally, the base Panel control is instructed to render after the gradient color filter is applied.

Conclusion

What lessons have been learned here? By inheriting and extending the existing Panel control, we were able to implement a lot of functionality with surprisingly little code. DirectX filters can be used to spruce up the UI of nearly any existing control. A little JavaScript can go a long way toward improving the performance of Web controls. Rendering outperforms Composition, even though Composition is a somewhat simpler approach from a development perspective.

VisiPanel is an attractive control, capable of performing optimally under a heavy load. Expanding panels are a popular and intuitive UI metaphor these days, and adding them to a Web site can be an efficient use of screen real estate. Take the code and use it or extend it. If you find interesting ways to improve upon it, I’d love to hear about them!

The sample code in this article is available for download.

This article was originally published in ASP.NET Pro Magazine.

BEGIN LISTING ONE

 

Imports System.ComponentModel

Imports System.Web.UI

Imports System.Web.UI.WebControls

Imports System.Drawing

 

<DefaultProperty("Text"), _

ToolboxData("<{0}:vp runat=server></{0}:vp>"), _

DefaultEvent("OnExpandedChanged")> _

Public Class VisiPanel

    Inherits System.Web.UI.WebControls.Panel

 

#Region " Public Properties "

    Private _headerText As String = Me.ID

    <Bindable(True), Category("Appearance")> _

    Property [HeaderText]() As String

        Get

            Return _headerText

        End Get

 

        Set(ByVal Value As String)

            _headerText = Value

        End Set

    End Property

 

    Private _GradientEndColor As Drawing.Color

    <Bindable(True), Category("Appearance")> _

    Public Property GradientEndColor() As Color

        Get

            Return _GradientEndColor

        End Get

        Set(ByVal Value As Color)

            _GradientEndColor = Value

        End Set

    End Property

 

    Private _Expanded As Boolean = True

    <Bindable(True), Category("Appearance"), _

    DefaultValue("1")> _

    Public Property Expanded() As Boolean

        Get

            Return _Expanded

        End Get

 

        Set(ByVal Value As Boolean)

            _Expanded = Value

        End Set

    End Property

#End Region

 

#Region " Public Events "

    Public Event OnExpandedChanged(ByVal sender _

    As System.Object, ByVal e As System.EventArgs)

 

    Protected Overrides Sub OnInit(ByVal e As _

    System.EventArgs)

      If Page.IsPostBack Then

        'Determine if the user expanded or contracted

        'the VisiPanel and fire an

        'OnExpandedChanged event if they did

        Dim vis As String = Page.Request(Me.ClientID & _

            "_hidden").ToString().ToLower

 

        If (Not vis Is Nothing) Then

          If vis = "none" AndAlso _Expanded <> False Then

            _Expanded = False

            RaiseEvent OnExpandedChanged(Me, Nothing)

          End If

          If vis = "block" AndAlso _Expanded <> True Then

            _Expanded = True

            RaiseEvent OnExpandedChanged(Me, Nothing)

          End If

        End If

      End If

    End Sub

#End Region

 

  Public Sub New()

    MyBase.New()

    MyBase.BorderStyle = WebControls.BorderStyle.Solid

    MyBase.BorderWidth = New Unit(1, UnitType.Pixel)

    MyBase.Width = New Unit(150, UnitType.Pixel)

    MyBase.Height = New Unit(75, UnitType.Pixel)

    MyBase.BorderColor = Color.Black

  End Sub

 

  Protected Overrides Sub Render(ByVal w As _

  System.Web.UI.HtmlTextWriter)

    'build the javascript show/hide code

    Dim sb As New System.Text.StringBuilder

    sb.Append("var obj= document.getElementById('")

    sb.Append(Me.ClientID + "');")

    sb.Append("var objDown= document.getElementById('")

    sb.Append(Me.ClientID + "_ButtonDown');")

    sb.Append("var objUp= document.getElementById('")

    sb.Append(Me.ClientID + "_ButtonUp');")

    sb.Append("if (obj.style.display == 'none')")

    sb.Append("{obj.style.display = 'block';")

    sb.Append("objDown.style.display='none';")

    sb.Append("objUp.style.display='block';}")

    sb.Append("else {obj.style.display = 'none';")

    sb.Append("objDown.style.display='block';")

    sb.Append("objUp.style.display='none';}")

    sb.Append("document.getElementById('")

    sb.Append(Me.ClientID + "_hidden')")

    sb.Append(".value=obj.style.display;")

    Dim js As String = sb.ToString()

 

    'render a hidden field to hold the expanded status

    w.AddAttribute(HtmlTextWriterAttribute.Id, _

        Me.ClientID & "_hidden")

    w.AddAttribute(HtmlTextWriterAttribute.Name, _

        Me.ClientID & "_hidden")

    w.AddAttribute(HtmlTextWriterAttribute.Type, "hidden")

    w.RenderBeginTag(HtmlTextWriterTag.Input)

    w.RenderEndTag()

 

    'output the VisiPanel header in the form of a table

    w.AddStyleAttribute("Cursor", "hand")

    w.AddAttribute(HtmlTextWriterAttribute.Onclick, js)

    w.AddAttribute(HtmlTextWriterAttribute.Id, _

        Me.ClientID & "_header")

    w.AddAttribute(HtmlTextWriterAttribute.Class, _

        "VisiPanelHeader")

    w.AddStyleAttribute(HtmlTextWriterStyle.BorderWidth, _

        MyBase.BorderWidth.ToString)

    w.AddStyleAttribute(HtmlTextWriterStyle.BorderStyle, _

        MyBase.BorderStyle.ToString)

    w.AddStyleAttribute(HtmlTextWriterStyle.BorderColor, _

        MyBase.BorderColor.ToKnownColor.ToString)

    w.AddStyleAttribute(HtmlTextWriterStyle.BackgroundColor, _

        MyBase.BackColor.ToKnownColor.ToString)

    w.AddStyleAttribute(HtmlTextWriterStyle.Width, _

        MyBase.Width.ToString)

    w.RenderBeginTag(HtmlTextWriterTag.Table) '<table>

    w.RenderBeginTag(HtmlTextWriterTag.Tr) '<tr>

    w.RenderBeginTag(HtmlTextWriterTag.Td) '<td>

    w.Write(Me.HeaderText)

    w.RenderEndTag() '</td>

 

    'output the VisiPanel header down button

    w.AddAttribute(HtmlTextWriterAttribute.Id, _

        Me.ClientID & "_ButtonDown")

    w.AddAttribute(HtmlTextWriterAttribute.Class, _

        "VisiPanelHeaderButtonDown")

    w.AddStyleAttribute(HtmlTextWriterStyle.FontFamily, _

        "WebDings")

    w.AddAttribute(HtmlTextWriterAttribute.Align, "right")

    w.AddStyleAttribute(HtmlTextWriterStyle.Width, "1%")

    If _Expanded Then w.AddStyleAttribute("display", _

        "none") Else w.AddStyleAttribute("display", "block")

    w.RenderBeginTag(HtmlTextWriterTag.Td) '<td>

    w.Write(Chr(54)) 'down arrow

    w.RenderEndTag() '</td>

 

    'output the VisiPanel header up button

    w.AddAttribute(HtmlTextWriterAttribute.Id, _

        Me.ClientID & "_ButtonUp")

    w.AddAttribute(HtmlTextWriterAttribute.Class, _

        "VisiPanelHeaderButtonUp")

    w.AddStyleAttribute(HtmlTextWriterStyle.FontFamily, _

        "WebDings")

    w.AddAttribute(HtmlTextWriterAttribute.Align, "right")

    w.AddStyleAttribute(HtmlTextWriterStyle.Width, "1%")

    If _Expanded Then w.AddStyleAttribute("display", _

        "block") Else w.AddStyleAttribute("display", "none")

    w.RenderBeginTag(HtmlTextWriterTag.Td) '<td>

    w.Write(Chr(53)) 'up arrow

    w.RenderEndTag() '</td>

 

    'close the table tags

    w.RenderEndTag() '</tr>

    w.RenderEndTag() '</table>

 

    'specify the visibility of the base panel control

    Dim vis As String

    If _Expanded Then vis = "block" Else vis = "none"

    w.AddStyleAttribute("display", vis)

    w.AddAttribute(HtmlTextWriterAttribute.Class, _

        "VisiPanel")

 

    'output the color gradient effect for the panel

    If _GradientEndColor.ToKnownColor.ToString <> "0" Then

        w.AddStyleAttribute("FILTER", _

            System.Environment.NewLine & _

            "progid:DXImageTransform.Microsoft.Gradient" & _

            "(startColorstr='" & _

            BackColor.ToKnownColor.ToString & _

            "', endColorstr='" & _

            GradientEndColor.ToKnownColor.ToString & _

            "', gradientType='0')")

    End If

    MyBase.Render(w) 'render the base panel control

 

  End Sub

End Class

 

END LISTING ONE

 

 

(advertisement)