ControlFreak
LANGUAGES:VB.NET | C#
ASP.NETVERSIONS: 1.x | 2.x
VisiPanel
Tour the Source Code of a Free, Colorful, ExpandingPanel Web Control
By Steve C. Orr
A Web developer can never have too many good navigationcontrols around. Although ASP.NET 2.0 delivers some nice new options, it stilldoesn?t provide anything resembling the expanding panel controls that arepopular these days. I haven?t seen many free ones around either, so I createdone - VisiPanel. You can have it for free, and I?ll show you how it all worksunder 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 whenfilled with navigational links, or it can act as a command panel when filledwith other kinds of controls. Figure 1 shows several instances of VisiPanel inaction.
Figure 1: DirectX filters areresponsible for VisiPanel?s colorful gradient display. Client-side code is inplace to smoothly expand and contract the panel when the user clicks on thetitle bar (without requiring postbacks).
User Friendly
At design time the VisiPanel control acts very much like astandard Panel Web control. This is related to the fact that VisiPanel inheritsfrom the Panel control and extends it with enhanced functionality. Any kind ofcontrol can be dropped into VisiPanel, and its contents can be arranged instandard ways.
Beyond the functionality of the base Panel control,VisiPanel adds several properties and one event (see Figure 2). VisiPanel?sOnExpandedChanged event is fired if the user has changed the dropdown state ofthe control between postbacks. The HeaderText property manages the textdisplayed in the header portion of the control at run time. The Expandedproperty toggles the initial dropdown state of the control, so it can be openedor closed programmatically. Finally, the GradientEndColor property can becombined with the BackColor property to provide an alluring background gradientcoloring 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: Beyondthe functionality of the underlying Panel control, VisiPanel adds severalunique properties and one new event.
That?s about all you need to know to get started using thecontrol. Download the control (see end of article for download details) orenter the code from Listing One into a new WebControl Library project in Visual Studio. To add the control to your VisualStudio toolbox, right click on the toolbox and follow the prompts to browse forVisiPanel.dll. Finally, drag the control from the toolbox onto any WebForm andconfigure its properties, or enter a declaration such as this into HTML view ofthe ASPX page:
<cc1:VisiPanel id="VisiPanel1"
runat="server"
BackColor="Beige"
GradientEndColor="Tan"
HeaderText="MyHeader Text">
Hello World
</cc1:VisiPanel>
The rest of this article describes the inner workings ofthe VisiPanel control, so you can learn from it or extend the control with evenmore advanced functionality.
Eye Candy
If you read my Eye Candy article, then you?re alreadyfamiliar with the DirectX filter technique VisiPanel is using for its colorfulgradient 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 (althoughother browsers degrade nicely) and it?s very picky about syntax details, suchas having white space and carriage returns in all the right places, so it?snice to have that functionality wrapped into a control like this that canhandle 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 onbottom. The configurable header text is displayed in the first table cell,followed by two Webdings font characters in the final two cells to representarrows. 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 oftwo primary parts: An HTML table is on top; on the bottom is the output of astandard Panel Web control from which VisiPanel inherits.
The VisiPanel code manages the output of the top headertable and adds a few attributes to the underlying Panel?s output for cosmeticpurposes.
The bottom portion of the control is the (slightlymodified) output of a standard Panel Web control. ASP.NET usually chooses torender the Panel as a standard <div> HTML tag. The base Panel controlmanages all the child controls, so you?ll find no child control management codewithin VisiPanel at all - even though this functionality works great at runtime and design time.
Figure 4 lists the custom JavaScript code that?s renderedto handle the client-side OnClick event of the header table. This code togglesthe visibility of the Panel?s output and the arrow table cells. The final linewrites the current dropdown state to a hidden field. Without this line, thecontrol would resort to its default dropdown state every time the page postsback, thereby annoying the user by undoing their action. You might think ofthis as a kind of a home-grown ViewState (standard ViewState wouldn?t workbecause 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 isthe client-side JavaScript code that gets executed when the end user clicks theheader table of the VisiPanel control. It toggles the Display style of thearrow cells and panel, then writes the state to a hidden textbox so server-sidecode will be able to determine the dropdown state upon the next postback.
The stored state information is also used by the control?sserver-side code the next time the page is posted back to determine if the usertoggled the dropdown state, and, if so, raises the OnExpandedChanged event. Youcan find this code in the OnInit event shown in Listing One.
VisiPanel?s constructor (Sub New) sets some appropriatedefaults for the base Panel control, specifying the initial size and some othercosmetic details.
Rendering Outperforms Composition
To generate the three-celled header table, I could?ve usedComposition. That is, I could have instantiated a Table object and added threeTableCell objects to its TabelRow object. This would generally be done withinthe CreateChildControls event of the server control. Although Composition is agreat way to keep development quick and simple, there is a performance costassociated with instantiating all those objects. For smaller Web sites withless traffic, this likely isn?t a big deal. However, if you?re developingcontrols for a highly scalable Web site, you should be aware that Renderingoutperforms Composition by a significant amount. That?s why I chose Renderinginstead of Composition. Rendering is done by overriding the Render event of thebase server control and outputting the HTML in a comparatively manual fashion.
Listing One shows the overridden Render event, which makesextensive use of the HTMLTextWriter parameter that I?ve abbreviated with thevariable name ?w?. The first code block uses a StringBuilder object toefficiently concatenate together the required JavaScript, such as that listedin Figure 4.
The second code block of the Render event generates thehidden textbox mentioned earlier. It is assigned Name and ID attributes so itcan be more easily referenced from client-side code. I could?ve used codesimilar to this to generate the hidden textbox:
w.Write("<input type=hidden id=whatever>")
However, hard-coding HTML in this fashion is asking forfuture maintenance problems. With XHTML coming on strong in the future, andhandheld devices of every kind supporting varying forms of HTML, lettingASP.NET make decisions about HTML generation details is usually a good idea. BecauseMicrosoft practically defines what is ?proper? HTML, it?s a good idea to trusttheir judgment about what precisely should be generated for whichever device ismaking the request. By using methods such as AddAttribute and RenderBeginTag,ASP.NET decides the precise syntax that is output. Of course, there are manyways to adjust the output in cases where Microsoft?s rendering technology hasmade a decision that contradicts your personal preferences.
The next four code blocks of the Render event use similartechniques 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 thatthis area is clickable. The JavaScript is assigned to the client-side OnClickevent of the table and cosmetic attributes are added to ensure an attractiveoutput. The final two cells are specified to use the Webdings font so the arrowcharacters will show appropriately. Only one of these arrow cells will bedisplayed at a time, depending on the current Expanded state of the control.
The final two code blocks of the Render event addattributes to the output of the underlying Panel control. First, it must bedetermined whether the panel will initially be displayed or hidden depending onthe current Expanded state of the control. Finally, the base Panel control isinstructed to render after the gradient color filter is applied.
Conclusion
What lessons have been learned here? By inheriting andextending the existing Panel control, we were able to implement a lot offunctionality with surprisingly little code. DirectX filters can be used tospruce up the UI of nearly any existing control. A little JavaScript can go along way toward improving the performance of Web controls. Renderingoutperforms Composition, even though Composition is a somewhat simpler approachfrom a development perspective.
VisiPanel is an attractive control, capable of performingoptimally under a heavy load. Expanding panels are a popular and intuitive UImetaphor these days, and adding them to a Web site can be an efficient use ofscreen real estate. Take the code and use it or extend it. If you findinteresting ways to improve upon it, I?d love to hear about them!
The source code for the VisiPanel control is available fordownload.
Steve C. Orr is anMCSD and a Microsoft MVP in ASP.NET. He?s been developing software solutionsfor leading companies in the Seattlearea for more than a decade. When he?s not busy designing software systems orwriting about them, he can often be found loitering at local user groups andhabitually lurking in the ASP.NET newsgroup. Find out more about him athttp://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
InheritsSystem.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 AsString)
_headerText =Value
End Set
End Property
Private_GradientEndColor As Drawing.Color
<Bindable(True),Category("Appearance")> _
Public PropertyGradientEndColor() As Color
Get
Return_GradientEndColor
End Get
Set(ByVal Value AsColor)
_GradientEndColor = Value
End Set
End Property
Private _Expanded AsBoolean = True
<Bindable(True),Category("Appearance"), _
DefaultValue("1")> _
Public PropertyExpanded() As Boolean
Get
Return _Expanded
End Get
Set(ByVal Value AsBoolean)
_Expanded =Value
End Set
End Property
#End Region
#Region " Public Events "
Public EventOnExpandedChanged(ByVal sender _
As System.Object, ByVale As System.EventArgs)
Protected Overrides SubOnInit(ByVal e As _
System.EventArgs)
If Page.IsPostBackThen
'Determine if theuser expanded or contracted
'the VisiPanel andfire an
'OnExpandedChangedevent if they did
Dim vis As String =Page.Request(Me.ClientID & _
"_hidden").ToString().ToLower
If (Not vis IsNothing) 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
RaiseEventOnExpandedChanged(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 = NewUnit(150, UnitType.Pixel)
MyBase.Height = NewUnit(75, UnitType.Pixel)
MyBase.BorderColor =Color.Black
End Sub
Protected Overrides SubRender(ByVal w As _
System.Web.UI.HtmlTextWriter)
'build the javascriptshow/hide code
Dim sb As NewSystem.Text.StringBuilder
sb.Append("varobj= document.getElementById('")
sb.Append(Me.ClientID +"');")
sb.Append("varobjDown= document.getElementById('")
sb.Append(Me.ClientID +"_ButtonDown');")
sb.Append("varobjUp= 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 fieldto 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 VisiPanelheader 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 VisiPanelheader 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 Thenw.AddStyleAttribute("display", _
"none")Else w.AddStyleAttribute("display", "block")
w.RenderBeginTag(HtmlTextWriterTag.Td) '<td>
w.Write(Chr(54)) 'downarrow
w.RenderEndTag() '</td>
'output the VisiPanelheader 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 Thenw.AddStyleAttribute("display", _
"block")Else w.AddStyleAttribute("display", "none")
w.RenderBeginTag(HtmlTextWriterTag.Td) '<td>
w.Write(Chr(53)) 'uparrow
w.RenderEndTag() '</td>
'close the table tags
w.RenderEndTag() '</tr>
w.RenderEndTag() '</table>
'specify the visibilityof 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 colorgradient 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