Steve C. Orr

Software Engineer, Web Developer, Database Designer
 
  

 













































































































LANGUAGES: VB.NET | C#

ASP.NET VERSIONS: 2.x

DropDown Calendar

Clean Up the Clutter with a Custom DropDownCalendar Control

By Steve C. Orr

The Calendar control included with ASP.NET provides an easy and intuitive way for users to visually select dates. However, it takes up so much screen real estate that you may not be able to fit many other controls on the page. The custom DropDownCalendar control detailed in this article solves the problem with a small footprint that expands only when needed.

The custom DropDownCalendar control described in this article inherits from the standard ASP.NET Calendar control and extends it with the ability to show and hide on demand. The control also provides useful new smart tags that make configuration easy. While touring the source code (available for download; see end of article for details), you can learn how to add smart tags to your own controls for a truly professional polish.

To use this control, download the sample code and add the included DropDownCalendar.dll to your Visual Studio 2005 toolbox. Then drag it onto any WebForm to get started using it. At first it will appear much like a standard DropDownList control. If you wish to drop down the calendar at design time, simply set the control’s Expanded property to True. If you leave this property set to True, the control will appear expanded by default when the page is loaded in the browser at run time. Other useful properties are shown in Figure 1.

Unique DropDownCalendar Properties

Description

Expanded

Sets or gets the initial dropdown state.  Default:  False

DisplayLongDate

Specifies whether the selected date should be displayed in the text area with a long or short date format.  Default:  True

TextAreaStyle

The style applied to the text area

ButtonBackColor

Sets or gets the color of the background of the button.  Default: White

ButtonForeColor

Sets or gets the color of the button arrow.  Default:  Black

Figure 1: The DropDownCalendar control provides several new and useful properties beyond those the base Calendar control already provides.

The ASPX declaration looks roughly like this (depending on how the control properties have been configured):

<cc1:DropDownCalendar ID="DropDownCalendar1" runat="server" ns="urn:schemas-microsoft-com:office:office"

  TextAreaStyle-Font-Underline="True"

  TextAreaStyle-ForeColor="Blue">

</cc1:DropDownCalendar>

At run time the control looks a lot like a dropdown list with an expandable calendar bolted onto the bottom, as shown in Figure 2. In design reality, the control is actually inherited directly from the Calendar control and the dropdown functionality is bolted onto the top.


Figure 2: The DropDownCalendar control’s calendar can hide and show on demand, much like the list portion of a DropDownList control.

Much like a standard DropDownList control, the calendar portion of this control can be shown and hidden on demand without requiring a postback. When the user clicks on a date, the form posts back and raises the SelectionChanged event (just like the standard Calendar control) and then the dropdown portion will be automatically hidden. Of course, the user can always expand the calendar again and pick a different date if you choose to let them.

Inside Out

Alright, it’s time to turn this control inside out and take a look at the code that makes it work. The basic structure of the control is outlined in Figure 3. This structure is fairly standard for any custom Web control. The Designer attribute is there to support the new smart tag functionality, which will be explained in more detail later.

<ToolboxData("<{0}:DDCal runat=server></{0}:DDCal>"), _
<Designer(GetType(DropdownCalendarDesigner))> _
Public Class DropDownCalendar
  Inherits Calendar
 
      'Constructor…
 
      'Public Properties…
 
                'Overriden Subroutines…
 
End Class
Figure 3: The DropDownCalendar control inherits directly from the Calendar control, declares a designer for smart tag support, extends the control with some custom properties, and overrides a few base subroutines.

The DropDownCalendar control overrides three subroutines of the base Calendar control. The most significant of these is the Render subroutine, which is where nearly all the real work happens to create the DropDownCalendar control (see Figure 4).

Protected Overrides Sub Render(ByVal _

  writer As HtmlTextWriter)

 

  'display calendar on top of other elements

  MyBase.Style(HtmlTextWriterStyle.Position) = "absolute"

 

  'hide or show the calendar initially

  If Me.Expanded Then

      Me.Style(HtmlTextWriterStyle.Display) = "block"

  Else

      Me.Style(HtmlTextWriterStyle.Display) = "none"

  End If

 

  'create the containing table

  Dim tbl As New Table

  tbl.Enabled = Me.Enabled

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

  tbl.BorderColor = Drawing.Color.Black

  tbl.CellSpacing = 1

  tbl.Width = Me.Width

  tbl.ID = Me.ClientID & "_tbl"

  If Me.Enabled Then

      tbl.Style(HtmlTextWriterStyle.Cursor) = "hand"

      tbl.Attributes.Add("Onclick", OnClickJS() )

  End If

 

  'create the text area

  Dim tr As New TableRow

  tbl.Rows.Add(tr)

  Dim td As New TableCell

  tr.Cells.Add(td)

  td.Width = New Unit(100, UnitType.Percentage)

  td.ApplyStyle(Me.TextAreaStyle)

  Dim dt As String = IIf(Me.DisplayLongDate, _

      Me.SelectedDate.ToLongDateString, _

      Me.SelectedDate.ToShortDateString)

  td.Controls.Add(New LiteralControl(dt))

 

  'create the (simulated) button

  Dim td2 As TableCell = New TableCell

  tr.Cells.Add(td2)

  td2.BorderStyle = WebControls.BorderStyle.Outset

  td2.BorderWidth = 2

  td2.BackColor = Me.ButtonBackColor

  td2.ForeColor = Me.ButtonForeColor

  td2.Font.Name = "Webdings"

  td2.Controls.Add(New LiteralControl("6"))

 

  'render the containing table

  tbl.RenderControl(writer)

 

  'render the base calendar control

  If Me.DesignMode = False OrElse Me.Expanded Then

      MyBase.Render(writer)

  End If

End Sub
Figure 4: The Render event draws the text area and the button that drops down the calendar. The rendering of the calendar is delegated to the base Calendar control.

The first line is important. By ensuring the calendar is rendered with absolute positioning, this will allow the calendar to hover over any controls that are placed just below the DropDownCalendar control when expanded, just like the list portion of a DropDownList control.

The second code block specifies the Display style of the calendar, which specifies whether it should initially be visible (block) or invisible (none). When the arrow button or text area is clicked by the user at run time, this value will be toggled by client-side code to hide and show the calendar on demand. This JavaScript is output at the end of the third code block, and is assigned to the client-side OnClick event.

(advertisement)

The fourth code block shown in Figure 4 continues creating the HTML table that positions everything, and creates the text area that will display the selected date in the configured long or short string format. The fifth code block styles one of the cells to look like a button so the user will understand this is a clickable area.

To wrap things up, the containing HTML table is rendered, which renders all the other controls that have been instantiated so far. Finally, the base Calendar control is then commanded to render itself.

To polish things nicely, the OnVisibleMonthChanged event is overridden to keep the calendar portion expanded between postbacks when the user changes months:

Protected Overrides Sub OnVisibleMonthChanged(ByVal _ ns="urn:schemas-microsoft-com:office:office"

    newDate As Date, ByVal previousDate As Date)

 

    'keep calendar expanded after month change

    Me.Expanded = True

    MyBase.OnVisibleMonthChanged(newDate, previousDate)

End Sub

Similarly, the OnSelectedChanged event is overridden to ensure the calendar portion automatically hides once the user has selected a date:

Protected Overrides Sub OnSelectionChanged()

    'hide calendar by default after selection

    Me.Expanded = False

    MyBase.OnSelectionChanged()

End Sub

When overriding base control events, it’s always good practice to call the base method to ensure the underlying control performs normally its portion of the functionality.

Smart Tags

Three things must be done to enhance a custom control with smart tags. First, the control class needs a Designer attribute that refers to a custom Designer class. This is demonstrated in Figure 3. Second, the custom Designer class must add a DesignerActionList (full of smart tag actions) to its DesignerActionListCollection, as shown in Figure 5. Finally, the DesignerActionList class (shown in Listing One) must programmatically add each desired DesignerAction. For all this to work, the control’s project must have a reference to System.Design.dll.

Public Class DropdownCalendarDesigner

  Inherits Web.UI.Design.WebControls.CalendarDesigner

 

  Private o_ActionLists As DesignerActionListCollection

 

  Public Overrides ReadOnly Property ActionLists() As _

   System.ComponentModel.Design.DesignerActionListCollection

    Get

      If o_ActionLists Is Nothing Then

        o_ActionLists = New DesignerActionListCollection

        Dim ctl As DropDownCalendar = _

          CType(Component, DropDownCalendar)

        o_ActionLists.Add(New DropdownCalendarActions(ctl))

      End If

      Return o_ActionLists

    End Get

  End Property

End Class

Figure 5: The Designer for the DropDownCalendar control inherits and extends the CalendarDesigner, extending it with smart tag actions that are defined in the custom DropDownCalendarActions class.

There’s not much code to the Designer class in Figure 5 because it is only implementing smart tag support. However, Designer classes can also unlock other advanced design-time features, as well, such as support for control templates and auto-formatting. The code in Figure 5 essentially does little more than add a reference to the DropDownCalendarActions class, which is shown in Listing One.

The primary function shown in Listing One, GetSortedActionItems, creates each item that will be shown in the smart tags dialog box, which is illustrated in Figure 6. A new DesignerActionItemCollection object is instantiated and filled with smart tags, otherwise known as DesignerActionItems. There are four kinds of DesignerActionItems: Headers, Properties, Methods, and Text. All but the latter are demonstrated in Listing One.


Figure 6: The smart tags for the DropDownCalendar control were added programmatically via a Designer class and a DesignerActionList class. Notice that the method item smart tags also show up automatically at the bottom of the Properties window.

Headers are shown in bold text, properties are displayed as editable fields, and commands tend to show up as hyperlinks that execute custom methods on demand. Properties — or more specifically, DesignActionPropertyItems — are quite versatile in their display; for example, automatically showing up as textboxes when representing strings, dropdownlists when representing enumerations, or checkboxes when representing Boolean values.

Notice that the properties and methods all have the header name passed as a constructor parameter so Visual Studio knows how to group them. Also notice the first parameter for each property item refers to a property declaration further down in Listing One, which does the work of setting and getting the property value from the underlying DropDownCalendar control. Similarly, the first constructor parameter for each method item refers to a function listed further down in Listing One. These methods do the real work. Although the examples here are simple, they could be as complex as necessary — launching Windows forms, fancy wizards, or whatever else you can imagine.

(advertisement)

Conclusion

Smart tags are a modern design-time feature that can enhance any control with a professional polish. They are a user-friendly way to allow application developers to configure controls in quick, intuitive ways. There is a significant amount of code involved, but most of it is simple and boilerplate.

Nearly any large control can be similarly enhanced with dropdown functionality so it can hide away when not immediately needed. You could try inheriting from a GridView control instead of a Calendar control, for example, which is a common way to create multi-column DropDownLists. Similarly, I’ve seen people place a TreeView control into a dropdown area to decrease the footprint of that otherwise bulky control. I can also imagine a dropdown CheckBoxList or RadioButtonList. How about a dropdown Login control, or maybe even a dropdown Crystal Report? I encourage you to combine your imagination and your skills with the information you’ve learned here to come up with a custom control that saves your company time and money.

The sample code in this article is available for download.

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

 

BEGIN LISTING ONE

 

Public Class DropdownCalendarActions

 Inherits System.ComponentModel.Design.DesignerActionList

 

 Private ctl As DropdownCalendar

 

 Public Overrides Function GetSortedActionItems() As _

  System.ComponentModel.Design.DesignerActionItemCollection

 

  Dim o_Items As _

    DesignerActionItemCollection = _

    New DesignerActionItemCollection

 

    ' create headers

    o_Items.Add(New _

      DesignerActionHeaderItem("Properties"))

    Dim Header2 As String = "Developed by Steve C. Orr"

    o_Items.Add(New _

      DesignerActionHeaderItem(Header2))

 

    'add property items

    o_Items.Add(New _

      DesignerActionPropertyItem("DayNameFormat", _

      "Day Name Format", "Properties"))

    o_Items.Add(New _

      DesignerActionPropertyItem("SelectedDate", _

      "Selected Date", "Properties"))

    o_Items.Add(New _

      DesignerActionPropertyItem("DisplayLongDate", _

      "Display Long Date Format", "Properties"))

 

    'add method items

    o_Items.Add(New _

    DesignerActionMethodItem(Me, "SelectJan1", _

      "Select January 1st", "Properties", _

      "Selects January First", True))

    o_Items.Add(New _

      DesignerActionMethodItem(Me, "ToggleExpanded", _

      "Toggle Expanded property", "Properties", _

      "Toggles the dropdown state", True))

    o_Items.Add(New _

      DesignerActionMethodItem(Me, "LaunchWebSite", _

      "http://SteveOrr.net", Header2, _

      "Launches Steve Orr's web site.", True))

 

    Return o_Items

 

 End Function

 

#Region " Properties "

 

 Public Property DayNameFormat() _

   As System.Web.UI.WebControls.DayNameFormat

  Get

    Return ctl.DayNameFormat

  End Get

  Set(ByVal value As DayNameFormat)

    GetProperty("DayNameFormat").SetValue(ctl, value)

  End Set

 End Property

 

 Public Property SelectedDate() As Date

   Get

     Return ctl.SelectedDate

   End Get

   Set(ByVal value As Date)

     GetProperty("SelectedDate").SetValue(ctl, value)

   End Set

 End Property

 

 Public Property DisplayLongDate() As Boolean

   Get

     Return ctl.DisplayLongDate

   End Get

   Set(ByVal value As Boolean)

     GetProperty("DisplayLongDate").SetValue(ctl, value)

   End Set

 End Property

#End Region

 

#Region " Methods "

  Public Sub New(ByVal ctlCal As DropDownCalendar)

    MyBase.New(ctlCal)

    ctl = ctlCal

  End Sub

 

  Public Sub LaunchWebSite()

    Dim sURL As String = "http://SteveOrr.net"

    Try

      System.Diagnostics.Process.Start(sURL)

    Catch

    End Try

  End Sub

 

  Public Sub ToggleExpanded()

    GetProperty("Expanded").SetValue(ctl, Not ctl.Expanded)

  End Sub

 

  Public Sub SelectJan1()

    GetProperty("SelectedDate").SetValue(ctl, _

      Date.Parse("Jan 1 " & Date.Now.Year.ToString))

  End Sub

 

  Private Function GetProperty(ByVal _

    propertyName As String) As PropertyDescriptor

    Dim o_Property As PropertyDescriptor = _

      TypeDescriptor.GetProperties(ctl).Item(propertyName)

    If o_Property IsNot Nothing Then

      Return o_Property

    Else

      Throw New _

        ArgumentException("Invalid property", _

          propertyName)

    End If

  End Function

#End Region

 

End Class

 

END LISTING ONE


 

(advertisement)