LANGUAGES: VB.NET | C#
ASP.NET VERSIONS: 1.x | 2.x
VisiPanel
Tour the Source Code of a Free, Colorful, Expanding Panel Web Control
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).
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