Contents
The DateTimePicker
control shipped with .NET has a very nice look and feel but lacks one important functionality: you can't enter null values. That means if you bind your DateTimePicker
control to a DataSet
, you run into trouble. Because in a database, it's normal to have nullable DateTime
values. Searching the Internet, I found several solutions but I had problems with each of it:
- Yet another DateTime...Slicker: This control from Nils Jonsson uses the
CheckBox
of the DateTimePicker
to indicate null values. I don't like this solution because I wanted a solution consistent with, for example, the TextBox
. If you display a null value in a TextBox
, you don't use a CheckBox
to indicate when it's null. You just display nothing.
- Nullable DateTimePicker: This control from Pham Minh Tri uses another approach. Null values are indicated with an empty
DateTimePicker
. That's the way I like it but I ran into several problems with this control where things didn't work as expected. Especially I got an ugly exception as soon as I placed the control on a TabPage
and tried to use databinding. Whenever the control was initialized with a null value, this exception occurred: "An unhandled exception of type 'System.ComponentModel.Win32Exception' occurred in system.windows.forms.dll". When it was initialized with a valid DateTime
value, it worked.
Well, I had controls with the right look and feel but they didn't work on TabPage
s, and of course I needed them to work on TabPage
s because most of my controls are placed on TabPage
s. So I started to investigate the exception. After a while, I was sure it had something to do with the change of the DateTimePicker.Format
property to CustomFormat
during initialization. And then I found this explanation from Brett Zimmerann:
"It is the change in DateTimePicker.Format
when you are switching between a null date and a non-null date (or vice versa). The change in the value of the Format
causes the component to raise a notification which is normally okay. But in this case, the control is not visible yet and the Format
notification fails internally".
So I came up with my on NullableDateTimePicker
control based on the work of the other NullableDateTimePicker
controls but that avoids these problems.
The NullableDateTimePicker
control is inherited from the DateTimePicker
control to get all the nice things of it. The Format
property of the base DateTimePicker
is always set to DateTimePickerFormat.Custom
and will never change (to avoid the exception when the control is used on a TabPage
). To still have the possibility to change the format the same way as in the original DateTimPicker
, the NullableDateTimePicker
has to emulate the whole stuff around the Format
and CustomFormat
properties of the DateTimePicker
. So let's see how the implementation of that looks.
First, we need some new private
fields to store different values used by the NullableDateTimePicker
:
private bool _isNull;
private string _nullValue;
private DateTimePickerFormat _format = DateTimePickerFormat.Long;
private string _customFormat;
private string _formatAsString;
As already mentioned, the Format
and CustomFormat
properties can't be used from the DateTimePicker
anymore so we have to implement new properties replacing them:
public new String CustomFormat
{
get { return _customFormat; }
set { _customFormat = value; }
}
public new DateTimePickerFormat Format
{
get { return _format; }
set
{
_format = value;
if (!_isNull)
SetFormat();
OnFormatChanged(EventArgs.Empty);
}
}
When the setter of the Format
property is called, the new DateTimePickerFormat
value is stored in _value
. Now the format of the NullableDateTimePicker
has to be changed to this new format. This is done in the SetFormat()
method.
Because the parent DateTimePicker
's Format
property is always set to DateTimePickerFormat.Custom
, we have to map the Format
value of our class to a string representing this format. This string can then be set as the CustomFormat
of the base DateTimePicker
control. But how do we map a DateTimePickerFormat
value to its string representation? We have to get the current CultureInfo
. From the CultureInfo
, we get a DateTimeFormatInfo
that gives us the correct format strings for the different DateTimePickerFormat
s.
private void SetFormat()
{
CultureInfo ci = Thread.CurrentThread.CurrentCulture;
DateTimeFormatInfo dtf = ci.DateTimeFormat;
switch (_format)
{
case DateTimePickerFormat.Long:
FormatAsString = dtf.LongDatePattern;
break;
case DateTimePickerFormat.Short:
FormatAsString = dtf.ShortDatePattern;
break;
case DateTimePickerFormat.Time:
FormatAsString = dtf.ShortTimePattern;
break;
case DateTimePickerFormat.Custom:
FormatAsString = this.CustomFormat;
break;
}
}
In the above method, we assign the string representation of the format to the FormatAsString
property which is a private
property of our control.
private string FormatAsString
{
get { return _formatAsString; }
set
{
_formatAsString = value;
base.CustomFormat = value;
}
}
In the setter of the FormatAsString
property, we assign the format string to base.CustomFormat
to finally change the format of the parent DateTimePicker
class.
Up to now, we have hidden the whole format stuff behind our new properties with the same names. So for the user of the control, everything is still the same. Let's go one step further now and implement a new Value
property allowing us to not only set DateTime
values but also null
values. The getter just returns the Value
of the base control or null
if the control shows a null value. The setter sets the control to null
if the value is null
or DBNull.Value
by calling SetToNullValue()
. If the value is a DateTime
value, it sets the Value
of the base control and calls SetToDateTimeFormat()
to correctly show the DateTime
value.
public new Object Value
{
get
{
if (_isNull)
return null;
else
return base.Value;
}
set
{
if (value == null || value == DBNull.Value)
{
SetToNullValue();
}
else
{
SetToDateTimeValue();
base.Value = (DateTime)value;
}
}
}
If the control showed a null
value and now has to show a DateTime
value, the SetToDateTimeValue()
method sets the format to the currently used DateTimePickerFormat
by calling SetFormat()
, and then calls OnValueChanged()
to fire the ValueChanged
event.
private void SetToDateTimeValue()
{
if (_isNull)
{
SetFormat();
_isNull = false;
base.OnValueChanged(new EventArgs());
}
}
SetToNullValue
displays the NullValue
in the control instead of a DateTime
value. NullValue
is a string property that can be set by the developer. So you have the possibility to either show an empty DateTimePicker
or you can display some specific string like "<Select date please>" or whatever you want.
private void SetToNullValue()
{
_isNull = true;
base.CustomFormat = (_nullValue == null || _nullValue == String.Empty)
? " " : "'" + NullValue + "'";
}
public String NullValue
{
get { return _nullValue; }
set { _nullValue = value; }
}
When the DateTimePicker
shows a null
value and the user then selects a valid DateTime
value (by either dropping down the control and selecting a value or by selecting a value using the up/down arrows if the control is in ShowUpDown
mode), we have to set the Format
back to the correct DateTimePickerFormat
so the control doesn't show the empty value any longer.
To achieve this, I first tried to override the OnCloseUp()
method and change the format from there. To distinguish if a value was selected or if the user clicked beside the control, I found out I can verify if Control.MouseButtons == MouseButtons.None
. If so, a valid value was selected. But it turned out later that if the user drops down the control and selects Today
, Controls.MouseButtons
was equal to MouseButtons.Left
, which was the same value as if the user clicked beside the control. So there was no possibility to find out whether the user clicked on Today
or beside the control.
Some debugging later, I found out I have to use WndProc()
instead of OnCloseUp()
. When the user selects a value, the control sends a DTN_CLOSEUP
notification, or if the control is in ShowUpDown
mode a notification I don't know the name of but the value is -722
. So when one of these two notifications is sent, I call SetToDateTimeValue()
to allow the control to show the correct DateTime
value instead of an empty value.
protected override void WndProc(ref Message m)
{
if (m.Msg == 0x4e)
{
NMHDR nm = (NMHDR)m.GetLParam(typeof(NMHDR));
if (nm.Code == -746 || nm.Code == -722)
SetToDateTimeValue();
}
base.WndProc(ref m);
}
[StructLayout(LayoutKind.Sequential)]
private struct NMHDR
{
public IntPtr HwndFrom;
public int IdFrom;
public int Code;
}
What's missing up to now is a possibility for the user to set the value of the DateTimePicker
to null
. It works by databinding but not by user interaction. The DateTimePicker
shall be nullable by pressing the Delete key. For that, override the OnKeyUp
event and set the Value
to NullValue
.
protected override void OnKeyUp(KeyEventArgs e)
{
if (e.KeyCode == Keys.Delete)
{
this.Value = _dateTimeNullValue;
OnValueChanged(EventArgs.Empty);
}
base.OnKeyUp(e);
}
Finally, let's implement the constructor. In the constructor, we set the Format
property of the base class to DateTimePickerFormat.Custom
and our own Format
property to DateTimePickerFormat.Long
(Long
is the default value of the DateTimePicker
control).
public NullableDateTimePicker() : base()
{
base.Format = DateTimePickerFormat.Custom;
NullValue = " ";
this.Format = DateTimePickerFormat.Long;
}
The usage is exactly the same as with .NET's DateTimePicker
. The only difference is when accessing the Value
property because it's of type Object
instead of type DateTime
. Thus you have to cast it to DateTime
(but check for null
before).
NullableDateTimePicker _dtp = new NullableDateTimePicker();
_dtp.NullValue = "Select Date";
DateTime _date = new DateTime();
if (_dtp.Value != null)
_date = (DateTime)_dtp.Value;
DataSet _ds = new DataSet();
DataTable _dt = _ds.Tables.Add("Table");
_dt.Columns.Add("DateTimeColumn", typeof(DateTime));
_dt.Columns[0].AllowDBNull = true;
_dtp.DataBindings.Add("Value", _ds, "Table.DateTimeColumn");
I'm using CSLA.NET, an open source framework, for developing multi-tier applications. CSLA.NET comes with its own DateTime
type, a so called SmartDate
. SmartDate
converts DBNull.Values
to either DateTime.MinValue
or DateTime.MaxValue
. So when working with CSLA.NET, the NullableDateTimePicker
as described above is not interesting, because when working with the SmartDate
class, the DateTimePicker
should show the NullValue
when the DateTime
value of the SmartDate
is either DateTime.MinValue
or DateTime.MaxValue
.
I've written another NullableDateTimePicker
handling this situation. I wrote an article about it in my personal blog. Go to nullable DateTimePicker for CSLA.NET.
According to Rono1, the databinding does not work correctly when used with a Nullable<DateTime>
data type. He suggests to add the following changes to fix that problem (not verified by me):
Add this line to the constructor:
this.DataBindings.CollectionChanged += new
CollectionChangeEventHandler(DataBindings_CollectionChanged);
Add these two methods:
private void DataBindings_CollectionChanged(object sender,
CollectionChangeEventArgs e)
{
if (e.Action == CollectionChangeAction.Add)
this.DataBindings[this.DataBindings.Count - 1].Parse +=
new ConvertEventHandler(NullableDateTimePicker_Parse);
}
private void NullableDateTimePicker_Parse(object sender, ConvertEventArgs e)
{
if (_isNull)
e.Value = null;
}
- 04/18/2005: Initial version.
- 05/17/2005: First update.
- Bug fix:
Format
property always called SetFormat()
instead of just calling it when the DateTimePicker
showed an empty value.
- Bug fix: When the
DateTimePicker
was empty and the user selected Today
, the control stayed empty instead of showing the selected DateTime
value. Thanks to tvbusy for reporting this bug.
- Bug fix: When the
DateTimePicker
was empty and in ShowUpDown
mode and the user changed its value using the up/down buttons, the control stayed empty. Thanks to Rono1 for reporting this bug and the solution.
Known issue: This bug is only partly solved. When the control is in the ShowUpDown
mode and you set it to null
, that works. When you then click one of the arrows, that works as well as a result of the bug fix. But when you then select again one of the arrow buttons, nothing happens. The user first has to click into the DateTimePicker
to select a part of the DateTime
value. I tried several ways to solve this problem but did not succeed till now.
- Change: Added some more attributes to the
Value
property to optimize its behavior in the designer. Thanks to Phil Kan for reporting these optimizations.
- Added: A section about required changes when working with .NET 2.0 (not part of the source code actually).