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 |
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:
runat="server" BackColor="Beige" GradientEndColor="Tan" HeaderText="My
Header Text"> Hello World
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
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("")
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:[email protected].
Imports System.ComponentModel
Imports System.Web.UI
Imports System.Web.UI.WebControls
Imports System.Drawing
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 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 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 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)
' w.RenderBeginTag(HtmlTextWriterTag.Tr) ' w.RenderBeginTag(HtmlTextWriterTag.Td) ' w.Write(Me.HeaderText) w.RenderEndTag() ' '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) ' w.Write(Chr(54)) 'down
arrow w.RenderEndTag() ' '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) ' w.Write(Chr(53)) 'up
arrow w.RenderEndTag() ' 'close the table tags w.RenderEndTag() ' w.RenderEndTag() ' '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