Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Yet another nullable DateTimePicker control

0.00/5 (No votes)
26 May 2005 49  
An article about developing a nullable DateTimePicker control.

Sample Image

Contents

Introduction

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 TabPages, and of course I needed them to work on TabPages because most of my controls are placed on TabPages. 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.

Solution

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.

Implementation

First, we need some new private fields to store different values used by the NullableDateTimePicker:

// true, when no date shall be displayed (empty DateTimePicker)

private bool _isNull;

// If _isNull = true, this value is shown in the DTP

private string _nullValue;

// The format of the DateTimePicker control

private DateTimePickerFormat _format = DateTimePickerFormat.Long;

// The custom format of the DateTimePicker control

private string _customFormat;

// The format of the DateTimePicker control as string

private string _formatAsString;

Formatting

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 DateTimePickerFormats.

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.

Value

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; }
}

Events

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)                         // WM_NOTIFY

  {
    NMHDR nm = (NMHDR)m.GetLParam(typeof(NMHDR));
    if (nm.Code == -746 || nm.Code == -722)  // DTN_CLOSEUP || DTN_?

      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);
}

Constructor

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;
}

Usage

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";
// Set the null value of the control (default is an empty string)


//  use of the NullableDateTimePicker without databinding:

DateTime _date = new DateTime();
if (_dtp.Value != null)
  _date = (DateTime)_dtp.Value;

// use of the NullableDateTimePicker

// with databinding (Bind it to a DataSet):

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");

For users of CSLA.NET

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.

For users of .NET 2.0

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)
{
  //saves null values to the object

  if (_isNull)
  e.Value = null;
}

History

  • 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).

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here