Introduction
The same old story: I needed a date-time picker that could display a blank value, and I did not want to have to train my users that a grayed-out date with no check in the box really means there is no date. After looking through the many controls here at CodeProject, I got a bit dismayed. I made my own stab at it using VB and Visual Studio 2008, and I got something I liked without having to do a lot of work.
Using the code
The base control itself is pretty straightforward: a UserControl
containing a MaskedEditBox
and a Panel
. The MaskedEditBox
is masked for a short date, and the user can type in a date or clear the control. Docked on the inside right edge of the MaskedEditBox
is a Panel
control that acts as a button. If the user clicks on it, an extended version of VS 2008's MonthCalendar
control pops up. If the MaskedEditBox
has a date, that date will be pre-selected on the pop-up. Selecting a date will copy that date into the MaskedEditBox
, or the user can click on the "Clear date" button which clears the MaskedEditBox
. Either choice will close the pop-up. The user also has the option of closing the pop-up without changing the date, by clicking on the "Close" button.
The code defining the pop-up is embedded in the control as a private class. Writing it was very straightforward: I created a regular Form
, added a MonthCalendar
and two Label
controls, set various properties to my liking, then copied the InitializeComponent
code into the class' New
method. This was the result, after cleaning it up a bit:
Protected Class Calendar
Inherits System.Windows.Forms.Form
Private MyPicker As NullableDateTimePicker
Private WithEvents Label1 As Label
Private WithEvents Label2 As Label
Private WithEvents MonthCalendar1 As MonthCalendar
Public Sub New(ByRef Picker As NullableDateTimePicker)
MyPicker = Picker
Me.MonthCalendar1 = New MonthCalendar
Me.Label1 = New Label
Me.Label2 = New Label
Me.SuspendLayout()
Me.MonthCalendar1.Location = New Point(0, 0)
Me.MonthCalendar1.Margin = New Padding(0)
Me.MonthCalendar1.MaxSelectionCount = 1
Me.MonthCalendar1.Name = "MonthCalendar1"
Me.MonthCalendar1.ShowTodayCircle = False
Me.MonthCalendar1.TabIndex = 0
Me.Label1.Font = New Font("Microsoft Sans Serif", 8.25!, _
FontStyle.Underline, GraphicsUnit.Point, CType(0, Byte))
Me.Label1.ForeColor = SystemColors.HotTrack
Me.Label1.Location = New Point(2, 163)
Me.Label1.Name = "Label1"
Me.Label1.Size = New Size(55, 13)
Me.Label1.TabIndex = 1
Me.Label1.Text = "Clear date"
Me.label2.AutoSize = True
Me.label2.Font = New Font("Microsoft Sans Serif", 8.25!, _
FontStyle.Underline, GraphicsUnit.Point, CType(0, Byte))
Me.label2.ForeColor = System.Drawing.SystemColors.HotTrack
Me.label2.Location = New System.Drawing.Point(192, 163)
Me.label2.Name = "Label2"
Me.label2.Size = New System.Drawing.Size(33, 13)
Me.label2.TabIndex = 2
Me.label2.Text = "Close"
Me.AutoScaleDimensions = New SizeF(6.0!, 13.0!)
Me.AutoScaleMode = AutoScaleMode.Font
Me.ClientSize = New Size(228, 184)
Me.ControlBox = False
Me.Controls.Add(Me.Label1)
Me.Controls.Add(Me.Label2)
Me.Controls.Add(Me.MonthCalendar1)
Me.FormBorderStyle = FormBorderStyle.FixedToolWindow
Me.MaximizeBox = False
Me.MinimizeBox = False
Me.Name = "CalendarPopup"
Me.ShowIcon = False
Me.ShowInTaskbar = False
Me.StartPosition = FormStartPosition.Manual
Me.ResumeLayout(False)
End Sub
Public Property SelectedDate() As Date?
Get
If Information.IsDate(MonthCalendar1.SelectionStart) Then
Return MonthCalendar1.SelectionStart
Else
Return Nothing
End If
End Get
Set(ByVal value As Date?)
If Information.IsDate(value) Then
MonthCalendar1.SetDate(Convert.ToDateTime(value))
Else
MonthCalendar1.SetDate(DateTime.Now)
End If
End Set
End Property
Private Sub Label1_Click(ByVal sender As Object, ByVal e As EventArgs) _
Handles Label1.Click
MyPicker.Value = Nothing
Me.Close()
End Sub
Private Sub Label2_Click(ByVal sender As Object, ByVal e As EventArgs) _
Handles Label2.Click
Me.Close()
End Sub
Private Sub MonthCalendar1_DateSelected(ByVal sender As Object, _
ByVal e As DateRangeEventArgs) _
Handles MonthCalendar1.DateSelected
MyPicker.Value = e.Start
Me.Close()
End Sub
End Class
The NullableDateTimePicker
has a private variable, Cal
, which serves as the control's instance of the pop-up form. When the control's button is clicked, Cal
is instantiated, if necessary, set to either the value in the masked edit box (if it is a valid date) or the current date (if it is not), and displayed as a modal form.
Private Sub Button1_MouseDown(ByVal sender As Object, ByVal e As MouseEventArgs) _
Handles Button1.MouseDown
If Cal Is Nothing Then Cal = New Calendar(Me)
If Cal.Visible Then Exit Sub
If Information.IsDate(MaskedTextBox1.Text) Then
Cal.SelectedDate = Convert.ToDateTime(MaskedTextBox1.Text)
Else
Cal.SelectedDate = Nothing
End If
Cal.Location = Button1.PointToScreen(New Point(0, 19))
Cal.ShowDialog()
End Sub
Getting the date out of the control is pretty straightforward. I have implemented HasDate
, which indicates whether or not the control is displaying a valid date, Text
, which returns the Text
property of the masked edit box, and Value
which returns a Nullable(Of Date)
value (abbreviated in VB 2008 as Date?
).
Public ReadOnly Property HasDate() As Boolean
Get
Return Information.IsDate(MaskedTextBox1)
End Get
End Property
Public Overrides Property Text() As String
Get
Return MaskedTextBox1.Text
End Get
Set(ByVal value As String)
MaskedTextBox1.Text = value
End Set
End Property
Public Property Value() As Date?
Get
If Me.HasDate Then
Return Convert.ToDateTime(MaskedTextBox1.Text)
Else
Return Nothing
End If
End Get
Set(ByVal value As Date?)
If Information.IsDate(value) Then
MaskedTextBox1.Text = _
Convert.ToDateTime(value).ToString("MM/dd/yyyy")
Else
MaskedTextBox1.Text = ""
End If
End Set
End Property
Room for improvement
This control is pretty basic, but it does what I need: allows the user to easily select a date, to manually enter a date, and to represent the absence of a date. Making it data-bound should be pretty easy. Properties to change the appearance of the calendar pop-up would also be handy.
Points of interest
Note the use of Information.IsDate
above. Information
is a static class found in the Microsoft.VisualBasic
namespace. I wrote the code this way to help C# programmers who might want to translate this control; I don't think C# has anything that is really equivalent to IsDate
. To use this function (and some other useful tools), add a reference to the namespace in your project.
As noted above, Visual Basic 2008 has added a shorthand notation for Nullable(Of T)
where T
is a value type. This is indicated by adding a ?
after either the variable name or the variable type in the declaration. Thus, my use of Date?
is effectively the same as using Nullable(Of Date)
; in fact, I believe, either way compiles the same.
History
- 2008-05-15 - Added a second label to the pop-up control which allows the user to close the form without changing the displayed value, and changed the text of the article to reflect this. I also expanded a bit on the new
Nullable
shorthand notation in VB.