ControlFreak
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.
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
|
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 run time.
|
|
Expanded property
|
Expanded property toggles the initial dropdown state of
the control so it can be opened and closed programmatically.
|
|
GradientEndColor property
|
The 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 Eye Candy 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.
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 source code for the VisiPanel control is available for
download.
Steve C. Orr is an
MCSD and a Microsoft MVP in ASP.NET. He s been developing software solutions
for leading companies in the Seattle
area for more than a decade. When he s not busy designing software systems or
writing about them, he can often be found loitering at local user groups and
habitually lurking in the ASP.NET newsgroup. Find out more about him at
http://SteveOrr.net or e-mail him at mailto:Steve@Orr.net.
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