Introduction
I know, I know�you�re thinking, �Yet another DateTimePicker?� DateTimeSlicker
is different and better. If you must, love it only for its oh-so-clever name.
With multiple DateTimePicker
alternatives available, from .NET discussion group samples to $400 commercial controls, why would I spend time on this? My motivation�centered on improving upon the Windows Forms DateTimePicker
�was threefold:
Others have reimplemented DateTimePicker
, adding and removing features to produce a result that doesn�t appeal to me. DateTimePicker
�s existing capabilities are many: empty values, keyboard and mouse input, culture sensitivity, XP (Luna) theming, and more. I didn�t want to take anything away; I just wanted to fix what�s broken.
Focus rectangle is drawn when it should not be
DateTimePicker
paints a focus cue on its check box when the control is created, and any time the value of its Checked
property changes. This means that a focus cue can appear while the control doesn�t actually have focus. This is unacceptable behavior in a well-mannered WinForms control! It�s confusing to have a control appear focused when it really isn�t.
This bug has been with us since DateTimePicker
was introduced years ago, and yet it doesn�t seem to get any airtime. I got tired of waiting for Redmond to fix the bug, so I decided to do something about it myself.
Lack of data-binding support for DBNull values
I hate how you can�t easily bind a DateTimePicker
to a nullable ADO.NET DataColumn
. Because a nullable column can contain a DBNull
value, you can�t bind to DateTimePicker
�s Text
property nor to its Value
property; these properties can accept only string
values and DateTime
values, respectively.
You can set DateTimePicker
�s Text property
to null
(Nothing
in Visual Basic), but doing so doesn�t clear the check box; the current date and time are displayed instead. (I used Lutz Roeder�s .NET Reflector to verify this by decompiling the Windows Forms library. If you don�t have this terrific tool, download it now.)
The wonders of binding in Avalon are still months and months away, so I wanted these problems solved now in WinForms.
Unpopular display of empty values
DateTimePicker
never appears empty, even when the check box is cleared. Instead, DateTimePicker
�s Checked
property�in addition to checking and clearing the check box�enables and disables (or �grays out�) the text. Many people despise this behavior. I myself am not fond of it because disabled text usually doesn�t look different enough from enabled text to avoid ambiguity, especially if your monitor�s contrast is not good. Users can easily mistake the disabled-text value in a DateTimePicker
for an enabled-text value. It would be better to hide the text when the check box is cleared.
Some have gone so far as to eliminate the check box from their DateTimePicker
alternatives. I think this is a mistake, because in the effort to fix one usability problem they introduce others. How does the user clear a DateTimePicker
if he can�t select all the text displayed in it? Do you really want to give up the behavior that allows only one element of the date/time to be selected at once? I toyed with the idea of replacing the check box with a delete button next to the dropdown button, but that didn�t feel right, either. Better to leave well enough alone, I think, and just try to hide the text when the check box is cleared.
Abortive attempts
Thomas Edison experimented with hundreds and hundreds of materials before he got his light bulb to work with a carbon filament. (The problem at hand is much simpler, of course, and so is the author.) In the effort to resolve all three of the aforementioned issues with DateTimePicker
, I explored and abandoned multiple approaches that ranged from inheritance to containment in a UserControl
to a combination of both. The root of the difficulty is that DateTimePicker
�s Checked
property is unreliable; in certain situations, it returns true
while the check box appears to be cleared.
I tried listening to the Windows message stream (that is to say, I overrode DateTimePicker
�s WndProc()
method) in the attempt to keep track of the state of the check box, but the messages generated by a mouse click do not seem to correlate with the state of the check box. The message stream is exactly the same whether the check box is being checked or cleared.
I tried using reflection to monitor the state of private fields in DateTimePicker
. There are two boolean fields that look promising (again, use .NET Reflector to decompile .NET executables). But neither of these, nor any other fields I could observe via reflection, reveals whether the check box is checked or cleared.
I also briefly considered writing my own DateTimePicker
from scratch, but that was only a momentary lapse of reason. Losing type compatibility with the existing DateTimePicker
would have been a disadvantage. More importantly, rewriting the DateTimePicker
would have entailed too much work; I would have got mired in dark areas of Win32 development that I didn�t want to get into just now. Besides, it seems like overkill to reinvent the wheel when all that is needed is a little grease.
The implementation
I decided to handle the user input myself so that I would know for sure whether the check box was checked or cleared. I overrode WndProc() and handled the WM_CHAR
and WM_LMOUSEDOWN
messages. DateTimeSlicker
hides the text by setting DateTimePicker
�s Format
and CustomFormat
properties to show an empty string when the check box is cleared. Problem #3 solved.
There are some legitimate uses for the controversial OOP feature of �shadowing�, or hiding, a base class member using the new
modifier (the Shadows
keyword in Visual Basic). DateTimeSlicker
shadows DateTimePicker
�s Value property so that it can accept either a DateTime
value or a DBNull
value. When the Value property is DBNull
, DateTimeSlicker
sets the Checked property to false
. Problem #2 solved.
Important note: Although shadowing does the job here, it only works properly if you call the new
members from the interface in which they�re declared. For example, if I call the Checked
, CustomFormat
, Format
and Value
properties on a variable of type DateTimePicker
, the new
members will not be called, regardless of whether the object underlying my DateTimePicker
variable is a DateTimeSlicker
.
KlugeFocusBug() grabs focus and then focuses the previously active control again. This is admittedly not the most elegant solution (what workaround is?). But I was surprised to discover by experimentation that my managed-code approach is cleaner than posting Windows messages in order to manipulate focus. Problem #1 solved.
Finally, because I interfered with some of DateTimePicker
�s behavior, some events no longer fired as expected. I added code to suppress certain events and fire others.
The code
Here�s the nitty-gritty.
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Security.Permissions;
using System.Windows.Forms;
namespace Umbrae.Windows.Forms
{
[DefaultEvent("ValueChanged")]
[DefaultProperty("Value")]
[ToolboxItemFilter("System.Windows.Forms")]
public class DateTimeSlicker : DateTimePicker
{
public DateTimeSlicker() : base()
{
this.ParentChanged += new EventHandler(this.this_ParentChanged);
this.ShowCheckBox = true;
this.customFormat = base.CustomFormat;
this.format = base.Format;
}
[Category("Property Changed")]
[Description("Occurs when the Checked property value is changed.")]
public event EventHandler CheckedChanged
{
add { this.checkedChanged += value; }
remove { this.checkedChanged -= value; }
}
[Category("Property Changed")]
[Description("Occurs when the CustomFormat property value is changed.")]
public event EventHandler CustomFormatChanged
{
add { this.customFormatChanged += value; }
remove { this.customFormatChanged -= value; }
}
true)>
[Category("Behavior")]
[DefaultValue(true)]
[Description("Determines if the check box is checked, indicating that "
+ "the user has selected a value.")]
public new bool Checked
{
get { return base.Checked; }
set
{
if (value == this.Checked)
return;
this.SetCheckBoxState(value);
this.OnCheckedChanged();
this.KlugeFocusBug();
}
}
[Category("Behavior")]
[DefaultValue(null)]
[Description("The custom format string used to format the date and/or "
+ "time displayed.")]
[RefreshProperties(RefreshProperties.Repaint)]
public new string CustomFormat
{
get { return this.customFormat; }
set
{
if (value == this.CustomFormat)
return;
if (this.Checked)
base.CustomFormat = value;
this.customFormat = value;
this.OnCustomFormatChanged();
}
}
[Category("Appearance")]
[DefaultValue(DateTimePickerFormat.Long)]
[Description("Determines whether the date and/or time is displayed "
+ "using standard or custom formatting.")]
[RefreshProperties(RefreshProperties.Repaint)]
public new DateTimePickerFormat Format
{
get { return this.format; }
set
{
if (value == this.Format)
return;
if (this.Checked)
{
base.Format = value;
}
this.format = value;
if (!(this.Checked))
this.OnFormatChanged();
}
}
false)>
[DesignerSerializationVisibility(
DesignerSerializationVisibility.Hidden)]
[EditorBrowsable(EditorBrowsableState.Advanced)]
public override string Text
{
get
{
if (this.Checked)
return base.Text;
else
return string.Empty;
}
set
{
if (value == null)
{
throw new ArgumentNullException(null,
"Argument cannot be null.");
}
if (value == this.Text)
return;
if (value == string.Empty)
{
this.OnTextChanged();
this.Checked = false;
}
else
{
base.Text = value;
}
}
}
true)>
[Category("Behavior")]
[Description("The date and/or time value (also can be DBNull).")]
[RefreshProperties(RefreshProperties.All)]
public new object Value
{
get
{
if (this.Checked)
return base.Value;
else
return DBNull.Value;
}
set
{
if (value == null)
{
throw new ArgumentNullException(null,
"Argument cannot be null.");
}
if (value == this.Value)
return;
if (value == DBNull.Value)
{
this.Checked = false;
this.OnValueChanged();
}
else if (value is DateTime)
{
base.Value = (DateTime)value;
this.Checked = true;
}
else
{
throw new ArgumentException(
"Argument must be a DateTime or DBNull.Value.");
}
}
}
private int GetCheckBoxExtent()
{
return
(int)this.CreateGraphics().MeasureString("X", this.Font).Height;
}
[UIPermission(SecurityAction.Demand)]
private void KlugeFocusBug()
{
if (this.Focused)
return;
Control parent = this.Parent;
while ((parent != null) && !(parent is ContainerControl))
parent = parent.Parent;
ContainerControl parentContainer = parent as ContainerControl;
if (parentContainer == null)
return;
Control activeControl = parentContainer.ActiveControl;
this.Focus();
if (activeControl != null)
activeControl.Focus();
}
protected virtual void OnCheckedChanged(EventArgs e)
{
if (e == null)
{
throw new ArgumentNullException("e",
"Argument e cannot be null.");
}
if (this.checkedChanged != null)
this.checkedChanged(this, e);
}
private void OnCheckedChanged()
{
this.OnCheckedChanged(EventArgs.Empty);
}
protected virtual void OnCustomFormatChanged(EventArgs e)
{
if (e == null)
{
throw new ArgumentNullException("e",
"Argument e cannot be null.");
}
if (this.customFormatChanged != null)
this.customFormatChanged(this, e);
}
private void OnCustomFormatChanged()
{
this.OnCustomFormatChanged(EventArgs.Empty);
}
protected override void OnFormatChanged(EventArgs e)
{
if (!(this.showingOrHidingText))
base.OnFormatChanged(e);
}
private void OnFormatChanged()
{
this.OnFormatChanged(EventArgs.Empty);
}
protected override void OnKeyDown(KeyEventArgs e)
{
if (((e.KeyCode == Keys.F4) && !(e.Alt))
|| (e.KeyCode == Keys.Down) && e.Alt)
{
this.Checked = true;
}
base.OnKeyDown(e);
}
private void OnTextChanged()
{
this.OnTextChanged(EventArgs.Empty);
}
private void OnValueChanged()
{
this.OnValueChanged(EventArgs.Empty);
}
private void SetCheckBoxState(bool value)
{
base.Checked = value;
if (this.Checked)
{
this.showingOrHidingText = true;
base.CustomFormat = this.customFormat;
base.Format = this.format;
this.showingOrHidingText = false;
}
else
{
this.showingOrHidingText = true;
base.CustomFormat = " ";
base.Format = DateTimePickerFormat.Custom;
this.showingOrHidingText = false;
}
}
private void Parent_Paint(object sender, PaintEventArgs e)
{
Debug.Assert(sender == this.Parent,
string.Format("Unexpected sender {0}.", sender));
this.Parent.Paint -= new PaintEventHandler(this.Parent_Paint);
this.KlugeFocusBug();
}
private void this_ParentChanged(object sender, EventArgs e)
{
Debug.Assert(sender == this,
string.Format("Unexpected sender {0}.", sender));
if (this.Parent != null)
this.Parent.Paint += new PaintEventHandler(this.Parent_Paint);
}
[SecurityPermission(SecurityAction.Demand)]
protected override void WndProc(ref Message m)
{
switch (m.Msg)
{
case 0x102:
int charAsInt32 = m.WParam.ToInt32();
char charAsChar = (char)charAsInt32;
if (charAsInt32 == (int)Keys.Space)
{
this.OnKeyPress(new KeyPressEventArgs(charAsChar));
this.Checked = !(this.Checked);
}
else
{
base.WndProc(ref m);
}
break;
case 0x201:
int x = (m.LParam.ToInt32() << 16) >> 16;
int y = m.LParam.ToInt32() >> 16;
if (x <= this.GetCheckBoxExtent())
{
this.OnMouseDown(
new MouseEventArgs(MouseButtons.Left, 1, x, y, 0));
this.Checked = !(this.Checked);
this.Focus();
}
else
{
bool checkedChange = !(this.Checked);
if (checkedChange)
{
this.SetCheckBoxState(true);
}
base.WndProc(ref m);
if (checkedChange)
this.OnCheckedChanged();
}
break;
default:
base.WndProc(ref m);
break;
}
}
private bool showingOrHidingText;
private EventHandler checkedChanged;
private EventHandler customFormatChanged;
private string customFormat;
private DateTimePickerFormat format;
}
}
History
Monday, November 24th, 2003: Article first submitted.
Thursday, November 27th, 2003: Fixed a problem with dropping down and navigating the calendar via the keyboard (F4 or Alt+Down) when the check box is not checked. The control�s response to a drop-down request via the keyboard is now consistent with its response to a drop-down request via the mouse; the check box becomes checked if it is not checked, and the user must issue a second drop-down request in order to get the drop-down to occur. This behavior is not identical to that of DateTimePicker, but it�s close�I�ll get around to conforming it at some point.